【Minecraft Forge】从零开始学习1.20.1模组开发 (一):Forge的注册系统
本教程面向有Java或面向对象基础,但对Minecraft Forge的开发毫无了解的同学;
所以,本文力求事无巨细,尽量以通俗的方法说明白注册系统;
名词较多可回看~~
俗话说工欲善其事必先利其器,而注册系统就是Forge的”器“:要想修改游戏系统,就必须添加新内容不可;而新内容正是通过注册系统(Register System)实现的。
若把注册系统看作操作系统,如下几个部件对理解注册系统的工作流程很重要:
外部存储————待注册对象(Object),我们要加入的游戏内容,例如一个泥土方块;
内存地址————资源地址(ResourceLocation),保存游戏资源的唯一标识符,为了简洁有时候直接叫它xx名;
进程————注册器(Registry),用来关联对象和它的资源地址。(有时候也翻译为注册表,但我认为翻译成注册器更能体现出它与一般注册信息的不同,因为它是一个可复用的工具)
用户————Minecraft游戏程序
假设我们现在有一个奇思妙想,并且为它创建了一个类。这个类,现在就是我们的外部存储。
目标是:让它被操作系统成功装载,呈现给用户。
第一步,把它装载到内存里,赋予它一个内存地址;
第二步,选取进程,将进程信息连同目标类的内存地址一同交给操作系统;
第三步,操作系统运行进程,把加工好的类呈递给用户。
翻译回原来的样子,这三步就是:
第一步,赋予对象资源地址;
第二步,选用注册器,准备将该注册器和类一同注册到注册系统中;
第三步,注册器的注册方法被调用,目标对象被提交给Minecraft游戏程序。
让我们按照顺序进行讲解:
一般对象的资源地址看起来像这样:minecraft:dirt,这是泥土的资源地址。
而且,所有的注册器也都拥有资源地址,例如方块注册器的资源地址是:minecraft:block,它与对象的资源地址不会重复,但除了命名以外也没有特殊标识。
在实际使用中,注册器一般意味着注册对象的基本类型,例如方块(Block),物品(Item)等等。
在一同注册的过程中,注册器会与对象进行绑定,其中使用了一种数据结构:注册器映射(ResourceKey),[1]
它可以绑定两个不同级的资源,例如注册器和对象。
这种数据结构包含两个资源地址,
对于注册器与对象的绑定来说,其中一个资源地址保存了注册器的地址,另一个资源地址则保存了对象的地址。
对于子注册器与父注册器的绑定来说,其中一个资源地址保存了子注册器的地址,另一个资源地址则保存了父注册器的地址,一般为minecraft:root。
最后,注册器不仅关联着对象和资源地址(特殊地,根注册器(在minecraft:root)关联着所有注册器与它们的资源地址),还负责与注册管理器(RegisterManager)进行交互,以注册指定的对象。
接下来,我们进行以上内容的代码分析。
注意,在阅读以下内容的时候,请确保你已经正确安装了Forge开发环境(【Minecraft Forge】从零开始学习1.20.1模组开发 (零):配置开发环境 - dudujerry - 博客园 (cnblogs.com));
我使用的是均MDK官方开发包中的示例代码,若你已经配置好了Forge开发环境则可阅读相同的完整源代码,因此我在此给出的代码仅为寻章摘句,方便你找到目标代码。
让我们来以注册一个方块的例子来管中窥豹。(in ExampleMod.java -> public class ExampleMod)
public static final DeferredRegister<Block> BLOCKS = DeferredRegister.create(ForgeRegistries.BLOCKS, MODID);
... ... 先从最主要的类DeferredRegister说起,一般译作延迟注册类,它提供了一种提前创建注册对象,并在注册事件中统一注册的办法。刚才这句话的其他概念会在(二)中有关事件系统的部分中讲到(url),即对于目前的分析不重要。
重要的是它的功能:指定注册器,指定要注册的对象,注册进注册表。[3]这几乎就是注册流程的全部了,但本篇文章仅会涉及到加粗部分 ... ...
ctrl加左键点进DeferredRegister类定义一探究竟。
public static <B> DeferredRegister<B> create(IForgeRegistry<B> reg, String modid)
迎面而来便是我们刚才示例代码中的create方法,它的第一个类型为IForgeRegistry<B>的参数reg便是我们提到过的注册器。
... ... 这里注册器的神秘面纱终于揭开,
IForgeRegistry类就是注册器对象在代码中的模样,它是一个接口,可以被不同类型的注册器实现,由此实现了多种多样的注册器;
例如,方块注册器的类型就是IForgeRegistry<Block>。
现在我们知道了注册器是一个对象,但我们不可能每次要提到它的时候都要创建一个实例。
所以,我们把这些对象的工厂(在Forge中,Supplier作为工厂实现)统一保存到注册器管理器(RegisterManager)中,需要用的时候就实例化一个。
那么,怎么找到我要实例化的注册器呢?这里终于引入注册器的判别机制,即前文提到过的 资源映射(ResourceKey)[1] ... ...
一步步往实现里点,发现create方法兜兜转转,最终来到了一个构造函数。
private DeferredRegister(ResourceKey<? extends Registry<T>> registryKey, String modid, boolean optionalRegistry)
{
this.registryKey = registryKey;
this.modid = modid;
this.optionalRegistry = optionalRegistry;
}
赫然发现,参数类型IForgeRegistry变成ResourceKey了!
这是因为,过程中的一个构造函数使用了
this(reg.getRegistryKey(), modid, false);
这其实就是获取了IForgeRegistry这个注册器对象的 资源映射形式的 标识符,并把它传给最后的构造函数保存。
毕竟,不可能每注册一个方块都要在DeferredRegister类里保存它的完整实例吧。利用ResourceKey(资源映射)保存既经济又安全。
ResourceKey包含两个资源地址(ResourceLocation),而所有注册器的资源映射一般be like:
[minecraft:root / minecraft:block]
这意思是说,所有的注册器都是根注册器的子注册器。
值得注意的是,不仅仅注册器拥有资源映射,注册好了的对象也拥有资源映射,只不过不保存在对象内部,而是需要通过它的注册器获取。
例如,获取泥土的资源映射:
Optional<ResourceKey<Block>> reskey = ForgeRegistries.BLOCKS.getResourceKey(Blocks.DIRT);
长这样:[minecraft:block / minecraft:dirt]
总而言之,言而总之,DeferredRegister类在这个最终的构造函数保存了使用的注册器(的资源映射),即示例中所使用的第一个参数ForgeRegistries.BLOCKS(.getRestryKey() )。
而最开始获得方块注册器时(ForgeRegistries.BLOCKS)用到的ForgeRegistries是一个静态单例类,保存了Forge预定义的所有注册器,方块、物品、创造标签等等都在里头。
以下内容是为想要了解注册系统的深层机制的同学准备的,需要一定耐心,可以选择跳过。
好奇的同学点进ForgeRegistries会发现如下内容:
public class ForgeRegistries
{
static { init(); } // This must be above the fields so we guarantee it's run before getRegistry is called. Yay static initializers
// Game objects
public static final IForgeRegistry<Block> BLOCKS = RegistryManager.ACTIVE.getRegistry(Keys.BLOCKS);
public static final IForgeRegistry<Fluid> FLUIDS = RegistryManager.ACTIVE.getRegistry(Keys.FLUIDS);
public static final IForgeRegistry<Item> ITEMS = RegistryManager.ACTIVE.getRegistry(Keys.ITEMS);
public static final IForgeRegistry<MobEffect> MOB_EFFECTS = RegistryManager.ACTIVE.getRegistry(Keys.MOB_EFFECTS);
......
所有的预定义注册器整齐列队。
显眼的RegistryManager便是Forge内部的注册管理器,它管理一切注册器,因此在这里见到它也无足为怪。
其中的Keys.BLOCKS,实际上它是一个ResourceKey<Registry<Block>>类型的值,又是我们之前说过的资源映射。[2]
这些排比的代码实际上就是从RegistryManager(注册管理器)中根据ResourceKey(资源映射)获取到IForgeRegistry(注册器对象),对,IForgeRegistry就是注册器在代码中的表现形式,找到、判断、识别一个注册器通常利用ResourceKey(资源映射)而非资源地址。
(与注册器有关的行为基本不会用到资源地址(ResourceLocation),而是使用绑定了父级资源的资源映射(ResourceKey)。
例如,在注册事件最终的注册行为,调用的方法是这样的:(in RegisterEvent.class)
@SuppressWarnings({ "unchecked", "rawtypes" })
public <T> void register(ResourceKey<? extends Registry<T>> registryKey, ResourceLocation name, Supplier<T> valueSupplier)
{
if (this.registryKey.equals(registryKey))
{
if (this.forgeRegistry != null)
((IForgeRegistry) this.forgeRegistry).register(name, valueSupplier.get());
else if (this.vanillaRegistry != null)
Registry.register((Registry) this.vanillaRegistry, name, valueSupplier.get());
}
}
其中第一句,
if (this.registryKey.equals(registryKey) )
其中的registryKey均为资源映射(ResourceKey),这句话就是确认区分对应的注册器用的。而注册器本身保存在RegisterEvent类成员里。可见,它并未直接使用资源地址(ResourceLocation)。)
至于ForgeRegistries的ResourceKey(资源映射)[2]又是哪来的,答案是现场构造的:(in ForgeRegistries.class)
public static final class Keys {
//Vanilla
public static final ResourceKey<Registry<Block>> BLOCKS = key("block");
......
private static <T> ResourceKey<Registry<T>> key(String name)
{
return ResourceKey.createRegistryKey(new ResourceLocation(name));
}
......
key方法中为了构造资源映射(ResourceKey)而调用的ResoiurceKey#createRegistryKey方法只有一个参数,类型为资源地址(ResourceLocation),内容是注册器名
(此处传入的仅为"block",然而不指定域的资源地址构造时会自动把域视作minecraft,所以此处的资源地址是minecraft:block,正是方块的注册器地址)。
然而我们知道ResourceKey是有两个资源地址的数据结构,所以另一个必定藏在该方法里。记性好的同学会记得,一切注册器都来源于根注册器minecraft:root,所以这里补全的资源地址正是minecraft:root!!
所以key中构造的资源映射内容应为:[根注册器名 / 注册器名],在此例中为[minecraft:root / minecraft:block]
(注意前面提到,xx名指代的就是xx的资源地址)
想要直观地看到资源映射的内容,我建议在模组主类的构造函数(in ExampleMod.java -> public class ExampleMod -> ExampleMod() )中书写下如下内容:
ResourceKey<Registry<Block>> blk = ForgeRegistries.BLOCKS.getRegistryKey();
LOGGER.info(blk.toString());
这样就获取了BLOCKS注册器名并在日志里输出,我自己运行的结果是这样的:
[modloading-worker-0/INFO] [com.mymod.common.ExampleMod/]: ResourceKey[minecraft:root / minecraft:block]
可以看到理论与实践重合了。另外,关于日志,我在文末会提供一个小方法来检索你自己mod的日志输出免得看花眼。。
好了,非好奇的同学和好奇完了的同学把思维挂载到分割线以上。。
至此,我们已经了解了DeferredRegister类的初始化流程了,其实抛去Forge用于标识注册器、对象的地址机制,无非就做了一件事:
保存注册器。
说的有点多,这篇就此打住。请喝口水,下一篇将进入DeferredRegister功能的 后两个逗号[3]。
下一篇:url(施工中)
关于筛选自己mod打印的日志的问题,提供一个python脚本,放在日志目录里(如果你使用gradle中的runClient直接运行的话,默认在 项目文件夹/run/logs/。否则你需要去你运行mod的客户端找到日志,一般在.minecraft/logs/。)运行,生成的result.log即为你mod生成的日志。
modName = "com.mymod.common.ExampleMod" #改成你自己的mod名称
res = ""
with open("latest.log", "r") as f:
for line in f:
if line.find(modName) != -1:
res += line
with open("result.log", "w+") as f:
f.write(res)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)