MCBBS Wiki欢迎您共同参与编辑!在参与编辑之前请先阅读Wiki方针

如果在编辑的过程中遇到了什么问题,可以去讨论板提问。

为了您能够无阻碍地参与编辑 未验证/绑定过邮箱的用户,请尽快绑定/验证

MCBBS Wiki GitHub群组已上线!

您可以在回声洞中发表吐槽!

服务器状态监控。点击进入

本站由MCBBS用户自行搭建,与MCBBS及东银河系漫游指南(北京)科技有限公司没有从属关系。点此了解 MCBBS Wiki 不是什么>>

用户:MashKJo/1.21.1模组开发教程/3.注册:修订间差异

来自MCBBS Wiki
跳到导航 跳到搜索
无编辑摘要
 
第64行: 第64行:
  public class RegistryHandler {
  public class RegistryHandler {
   
   
    //这里T即为你的注册项类型,可以为Item、Block等等。
    //这里T即为你的注册项类型,可以为Item、Block等等。
   
   
    //DeferredRegister字段的命名一般习惯上是这样的:把注册项类型名称全大写,再加S,如DeferredRegister<Item>一般会被命名为ITEMS。
    //DeferredRegister字段的命名一般习惯上是这样的:把注册项类型名称全大写,再加S,如DeferredRegister<Item>一般会被命名为ITEMS。
   
   
    //create方法的第一个参数可以是ResourceKey<Registry<T>>、ResourceLocation或Registry<T>。
    //create方法的第一个参数可以是ResourceKey<Registry<T>>、ResourceLocation或Registry<T>。
   
   
    public static final DeferredRegister<T> DEFERRED_REGISTER_T = DeferredRegister.create(BuiltInRegistries.xxx, TutorialMod.MODID);
    public static final DeferredRegister<T> DEFERRED_REGISTER_T = DeferredRegister.create(BuiltInRegistries.xxx, TutorialMod.MODID);
   
   
    //第一个参数为该注册对象的注册名,第二个参数可以是一个Function<ResourceLocation, ? extends S>,也可以是一个Supplier<? extends S>。
    //第一个参数为该注册对象的注册名,第二个参数可以是一个Function<ResourceLocation, ? extends S>,也可以是一个Supplier<? extends S>。
   
   
    //一般而言填一个Supplier更为常见。
    //一般而言填一个Supplier更为常见。
   
   
    //这个lambda就是DeferredRegister的精髓所在了:实现游戏对象的惰性初始化,或许这个字段被加载得较早,但是lambda里的方法要到RegisterEvent被发布才被调用,这个DeferredHolder才真正可用。
    //这个lambda就是DeferredRegister的精髓所在了:实现游戏对象的惰性初始化,或许这个字段被加载得较早,但是lambda里的方法要到RegisterEvent被发布才被调用,这个DeferredHolder才真正可用。
   
   
    public static final DeferredHolder<T, S extends T> MY_REGISTRY_OBJECT = DEFERRED_REGISTER_T.register("example_name", () -> ...);
    public static final DeferredHolder<T, S extends T> MY_REGISTRY_OBJECT = DEFERRED_REGISTER_T.register("example_name", () -> ...);
   
   
    ...
    ...
   
   
    //将我们的DeferredRegister注册进MOD总线,一般是在Mod主类构造器中调用它。
    //将我们的DeferredRegister注册进MOD总线,一般是在Mod主类构造器中调用它。
   
   
    public static void register(IEventBus bus) {
    public static void register(IEventBus bus) {
        DEFERRED_REGISTER_T.register(bus);
        DEFERRED_REGISTER_T.register(bus);
    }
    }
  }
  }



2025年9月6日 (六) 22:59的最新版本

Minecraft中充满了各种游戏元素:物品(Item)、方块(Block)、实体(Entity)、附魔(Enchantment)等等,为了方便地统筹与管理这些游戏元素,注册表(Registry)应运而生。

需要注意的是,注册进注册表的都是游戏对象的类型,即享元对象。如“64个苹果”是一种游戏对象(其实就是ItemStack),而“苹果”是它的类型,即为需要注册进物品注册表的对象。

理解了这一点,我们就可以开始了。

固有注册表与可写注册表

游戏内有几十种注册表,它们分别都有不同的作用。这些注册表中,可以分为2类:
固有注册表(Built-in Registry):游戏硬编码的注册表,内部数据无法通过任何方式修改。这些注册表在各个世界中都通用。
可写注册表(Writable Registry):游戏读取世界中的数据包获得这些注册表的信息,游戏代码内部并不存在这些注册表的数据。这些注册表与世界绑定,根据世界不同数据也有可能不同。

显而易见,我们在代码层面能直接操作并添加新元素的,就是固有注册表;至于可写注册表,我们应该以数据包的形式添加新元素。

所有固有注册表都能在BuiltInRegistries类中找到;而对于可写注册表,由于它会随着世界的不同而变动,因此你需要一个Level上下文,通过该Level对象获取一个RegistryAccess:

  • 对于服务端世界,请调用ServerLevel#registryAccess。
  • 对于客户端世界,请调用Minecraft.getInstance().getConnection().registryAccess()。

请注意:只有当你的客户端实际进入了一个世界(本地存档/远程服务器)的时候,它才会返回一个有效的值,否则会直接报NPE。

而且不同于固有注册表,并非所有可写注册表都会同步所有数据至客户端,请格外注意这一点。

之后,我们就可以调用方法#registryOrThrow来获取我们想要的注册表了,注意到其要求的唯一参数的类型是ResourceKey<? extends Registry<? extends E>>,这个实际上就是确定我们到底想要哪个注册项的注册表——体现为第二层泛型参数。实际上所有注册表都需要这么一个ResourceKey<Registry<?>>(注册表资源键,Registry Resource Key),用于标识它对应的注册项类型。所有注册表资源键(包括固有注册表和可写注册表)都可以在Registries类下找到。

ResourceKey和ResourceLocation

ResourceKey分为两个部分:registryName和location,二者均为ResourceLocation。registryName我们暂且不做过多探讨,读者只需知道所有ResourceKey<Registry<?>>的registryName均为minecraft:root;location则代表该ResourceKey在这一registryName下的“唯一标识符”——是的,别被“location”“ResourceLocation”这种字眼蒙在鼓里了。

ResourceLocation分为两个String:namespace和path,转换成字符串形式即为<namespace>:<path>。默认的namespace即为minecraft,你在模组中自定义ResourceLocation的时候,该用的namespace就是你自己的模组的MODID了。

你可以通过ResourceLocation.fromNamespaceAndPath(namespace, path)来构造出一个ResourceLocation。特别地,如果你想指定namespace为默认值minecraft,可以直接调用#withDefaultNamespace。

ResourceLocation意为“资源路径”,它当然有指代一个具体资源文件的作用——具体地说,一个字符串形式为namespace:path的ResourceLocation实际上在通常情况下,代表一个位于路径assets/namespace/path的资源文件;例如,minecraft:textures/item/apple.png则代表路径为assets/minecraft/textures/item/apple.png的一个文件——即苹果的纹理文件。

当然,上面讲的只是一般的通用情况,实际应用中,Minecraft相关代码基本都会对ResourceLocation做一些中间处理,由此你可以根据当前的代码上下文,省略文件后缀名乃至一部分中间路径。因此读者遇到这种情况时,如果拿不准,最好先看看Minecraft的代码是怎么处理这个ResourceLocation的。

而当ResourceLocation应用于一个ResourceKey的location时,它则是起到“作为唯一标识符”的作用了。例如,物品注册表的资源键的location即为minecraft:item。也正因为这个用途,ResourceLocation在Yarn映射表中的名称为“Identifier”。

遍历注册表

使用for-each循环即可。

注册对象

注册对象资源键

没错,所有注册对象也同样需要一个ResourceKey<?>,以便于在注册表中查找它们。注册对象资源键的泛型参数即为注册对象的类型,如Item、Block等;其registryName为对应注册表资源键的location,而注册对象资源键的location,即为喜闻乐见的通常意义上的“注册名”了,如minecraft:apple、minecraft:barrier等。

ResourceKey<?> key = ResourceKey.create(resourceKey, location);
    //一般是通过这个方式得到某个注册对象的资源键,resourceKey即为注册对象对应注册表的ResouceKey。
    //从这里可以看出,ResourceKey在某种程度上有点“继承”的概念。

另外,ResourceKey是有内部缓存的,也就是说我们可以用==运算符来比较2个ResourceKey。

通过注册对象资源键,我们可以便捷地在注册表中查询注册对象。

Holder

在实际代码中,注册对象多数情况下并非直接出现的,而是被一层Holder<?>包裹起来,如在涉及到有关附魔对象的代码时,一般出现的都是Holder<Enchantment>而非直接的Enchantment。

我们可以直接用Holder#value来获取封装起来的值,也可以通过Holder#direct、Registry#wrapAsHolder、Registry#getHolderOrThrow来获取一个Holder。注意:Holder#direct获取的实际上是一个实现了Holder<?>的record:Holder.Direct,正如其名,我们直接把一个注册对象传入进去,直接包装成一个Holder,对value()的调用时机没有任何限制。然而还存在另一个实现类:Holder.Reference,后两者获取的实际类型便是它,牵扯到注册表的一些机制,它是有点类似于惰性加载的(但加载的时机实际不受Modder控制),所以过早地(指在RegisterEvent发布前)调用它的value()(即forward reference)方法会导致NPE崩游戏,且会提示Trying to access unbounded value...。通过调用#kind可以知道一个Holder的实际类型。

Holder的意义在于更好地对注册对象进行管理,如:判断其是否属于某个标签(Tag)。

延迟注册(DeferredRegister)

上述介绍的都是属于原版的注册机制。对于可写注册表,我们用数据包的.json格式添加新元素即可,这个作者就不多说了。

那么固有注册表呢?NeoForge提供了2种方案:监听注册事件(RegisterEvent),与延迟注册(DeferredRegister)机制。推荐使用后者(如果你没有动态注册的需求的话),实际上延迟注册机制就是基于注册事件设计的,它能帮你避免很多因类加载顺序产生的错误,详见TeaCon的一篇文章:为什么你不该在静态初始化块里创建游戏对象 - TeaCon Blog

具体地说,使用DeferredRegister注册注册项的流程是这样的:

public class RegistryHandler {

   //这里T即为你的注册项类型,可以为Item、Block等等。

   //DeferredRegister字段的命名一般习惯上是这样的:把注册项类型名称全大写,再加S,如DeferredRegister<Item>一般会被命名为ITEMS。

   //create方法的第一个参数可以是ResourceKey<Registry<T>>、ResourceLocation或Registry<T>。

   public static final DeferredRegister<T> DEFERRED_REGISTER_T = DeferredRegister.create(BuiltInRegistries.xxx, TutorialMod.MODID);

   //第一个参数为该注册对象的注册名,第二个参数可以是一个Function<ResourceLocation, ? extends S>,也可以是一个Supplier<? extends S>。

   //一般而言填一个Supplier更为常见。

   //这个lambda就是DeferredRegister的精髓所在了:实现游戏对象的惰性初始化,或许这个字段被加载得较早,但是lambda里的方法要到RegisterEvent被发布才被调用,这个DeferredHolder才真正可用。

   public static final DeferredHolder<T, S extends T> MY_REGISTRY_OBJECT = DEFERRED_REGISTER_T.register("example_name", () -> ...);

   ...

   //将我们的DeferredRegister注册进MOD总线,一般是在Mod主类构造器中调用它。

   public static void register(IEventBus bus) {
       DEFERRED_REGISTER_T.register(bus);
   }
}

DeferredHolder是一种特殊的Holder,它用于弥补Holder的不足——Holder的泛型参数只能是该种注册项的上限类,如对于物品的Holder,泛型参数只能是Item而不能是PickaxeItem这种子类。我们看看DeferredHolder的类定义就知道:public class DeferredHolder<R, T extends R> implements Holder<R>, Supplier<T>。因此一个DeferredHolder同时也是一个Supplier。

上述介绍的是注册注册对象的一般步骤,但实际上NeoForge考虑到物品(Item)、方块(Block)和物品堆叠组件类型(DataComponentType<?>)的注册非常常见,因此提供了DeferredRegister的三个子类:DeferredRegister.Items、DeferredRegister.Blocks和DeferredRegister.DataComponents,以及DeferredHolder的两个子类:DeferredItem和DeferredBlock。并以此为基础提供了很多便捷的方法。

但是注意,这些DeferredRegister子类的某些注册方法会把构建新对象时的某一部分移到lambda外边(以此来满足使用方法引用的条件),因此如果你的注册对象依赖于非原版的某些注册对象,这么做就炸了,这时候还是老老实实地用DeferredRegister#register为好,把初始化包进lambda。