【Minecraft Forge】从零开始学习1.20.1模组开发 (一):Forge的注册系统

发布时间 2023-07-21 13:49:20作者: dudujerry

脑海忽起一念,欲制一mod以自娱。。。然网上之教程或不详,或既后于版本,故自查Forge之官方文档(https://docs.minecraftforge.net/en/1.20.x/),撰此系列以记之。其间艰深晦涩之处,多问于Bing,若有遗漏错误,则为AI之所为也~~望诸君告之

本教程面向有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)