Skip to main content
Version: 1.21.4

注册表(Registries)

注册(Registration)指的是将模组(mod)中的对象(比如 物品方块、实体等)让游戏知晓的过程。注册非常重要,如果没有注册,游戏根本无法识别这些对象,这会导致各种莫名其妙的问题甚至崩溃。

注册表(Registry)本质上就是一个对映射(map)的封装,它把注册名(registry name,后文会详细说明)映射到已注册的对象,这些对象通常被称为注册项(registry entry)。注册名在同一个注册表中必须唯一,但同一个注册名可以出现在不同的注册表中。最常见的例子就是方块(在 BLOCKS 注册表中)和它对应的物品形态(在 ITEMS 注册表中),它们拥有相同的注册名。

每个已注册的对象都有一个唯一的名字,称为注册名(registry name)。这个名字由 ResourceLocation 表示。例如,泥土方块的注册名是 minecraft:dirt,僵尸的注册名是 minecraft:zombie。模组对象当然不会使用 minecraft 这个命名空间,而是会用自己的模组 id。

原版与模组(Vanilla vs. Modded)

为了理解 NeoForge 注册表系统背后的一些设计决策,我们先看看 Minecraft 自己是怎么做的。我们以方块注册表为例,因为大多数其他注册表的原理也是类似的。

注册表通常注册的是 单例。也就是说,所有注册项在内存中都只有一份。例如,你在游戏中看到的所有石头方块,其实都是同一个石头方块对象,在不同位置被多次展示而已。如果你需要获取石头方块,只需要引用已经注册过的那个方块实例。

Minecraft 把所有方块都注册在 Blocks 类中。通过 register 方法,调用了 Registry#register(),第一个参数就是在 BuiltInRegistries.BLOCK 里的方块注册表。所有方块注册完成后,Minecraft 会基于方块列表执行各种检查,比如自检,确保每个方块都正确加载了模型。

之所以这一切能顺利进行,是因为 Minecraft 会足够早地加载(classload)Blocks 这个类。而模组的类并不会被 Minecraft 自动加载,因此需要用一些变通方法来实现注册。

注册的方法(Methods for Registering)

NeoForge 提供了两种注册对象的方法:DeferredRegister 类和 RegisterEvent。需要注意的是,前者其实是对后者的封装,推荐优先使用 DeferredRegister,这样能有效避免出错。

DeferredRegister

我们首先来创建一个 DeferredRegister

public static final DeferredRegister<Block> BLOCKS = DeferredRegister.create(
// 我们要使用的注册表(Registry)。
// Minecraft 的注册表可以在 BuiltInRegistries 中找到,NeoForge 的注册表可以在 NeoForgeRegistries 中找到。
// 其他模组也可能会添加自己的注册表,具体请参考对应模组的文档或源码。
BuiltInRegistries.BLOCKS,
// 我们的模组 ID。
ExampleMod.MOD_ID
);

接下来,我们可以使用以下方法之一,将注册表(Registry)条目作为静态 final 字段添加(关于 new Block() 需要传入哪些参数,请参考 方块 相关文章):

public static final DeferredHolder<Block, Block> EXAMPLE_BLOCK_1 = BLOCKS.register(
// 我们的注册名称。
"example_block",
// 我们想要注册的对象的提供者(Supplier)。
() -> new Block(...)
);

public static final DeferredHolder<Block, SlabBlock> EXAMPLE_BLOCK_2 = BLOCKS.register(
// 我们的注册名称。
"example_block",
// 一个用于创建我们要注册对象的函数,
// 参数为它的注册名称(ResourceLocation)。
registryName -> new SlabBlock(...)
);

DeferredHolder<R, T extends R> 这个类用于保存我们注册的对象。类型参数 R 表示我们要注册到的注册表(Registry)类型(在本例中为 Block)。类型参数 T 表示我们提供的对象类型。由于在第一个例子中我们直接注册了一个 Block,所以第二个参数也用 Block。如果我们注册的是 Block 的子类对象,比如 SlabBlock(如第二个例子所示),那么这里就用 SlabBlock

DeferredHolder<R, T extends R>Supplier<T> 的子类。当我们需要获取注册好的对象时,可以调用 DeferredHolder#get() 方法。由于 DeferredHolder 继承自 Supplier,我们也可以直接将字段类型声明为 Supplier。这样,上面的代码块可以简化为:

public static final Supplier<Block> EXAMPLE_BLOCK_1 = BLOCKS.register(
// 我们的注册名称。
"example_block",
// 我们想要注册的对象的提供者(Supplier)。
() -> new Block(...)
);

public static final Supplier<SlabBlock> EXAMPLE_BLOCK_2 = BLOCKS.register(
// 我们的注册名称。
"example_block",
// 一个用于创建我们要注册对象的函数,
// 参数为它的注册名称(ResourceLocation)。
registryName -> new SlabBlock(...)
);
note

请注意,有些地方明确要求使用 HolderDeferredHolder,而不会接受任意的 Supplier。如果你需要这两者之一,建议将你的 Supplier 字段类型按需改回 HolderDeferredHolder

最后,由于整个系统实际上是对注册表(Registry)事件的封装,我们需要让 DeferredRegister 按需绑定到注册表事件上:

// 这是我们的 Mod 构造函数
public ExampleMod(IEventBus modBus) {
ExampleBlocksClass.BLOCKS.register(modBus);
// 这里可以添加其他内容
}
info

对于方块、物品和数据组件,DeferredRegister 有专门的变体,提供了便捷方法:DeferredRegister.BlocksDeferredRegister.ItemsDeferredRegister.DataComponents

RegisterEvent

RegisterEvent 是注册对象的第二种方式。这个 事件 会针对每一个注册表(Registry)被触发,发生在 mod 构造函数之后(因为 DeferredRegister 会在构造函数中注册它们自己的事件处理器),并且在配置加载之前。RegisterEvent 会在 mod 事件总线上触发。

@SubscribeEvent
public void register(RegisterEvent event) {
event.register(
// 这是注册表的注册表键(registry key)。
// 对于原版注册表,可以从 BuiltInRegistries 获取,
// 对于 NeoForge 注册表,可以从 NeoForgeRegistries.Keys 获取。
BuiltInRegistries.BLOCKS,
// 在这里注册你的对象。
registry -> {
registry.register(ResourceLocation.fromNamespaceAndPath(MODID, "example_block_1"), new Block(...));
registry.register(ResourceLocation.fromNamespaceAndPath(MODID, "example_block_2"), new Block(...));
registry.register(ResourceLocation.fromNamespaceAndPath(MODID, "example_block_3"), new Block(...));
}
);
}

查询注册表(Querying Registries)

有时候,你可能需要根据给定的 id 获取已注册的对象,或者想要获取某个已注册对象对应的 id。由于注册表(Registry)本质上是 id(ResourceLocation) 到对象的映射(也就是一种可逆映射),所以这两种操作都是可行的:

BuiltInRegistries.BLOCKS.getValue(ResourceLocation.fromNamespaceAndPath("minecraft", "dirt")); // 返回泥土方块
BuiltInRegistries.BLOCKS.getKey(Blocks.DIRT); // 返回资源位置 "minecraft:dirt"

// 假设 ExampleBlocksClass.EXAMPLE_BLOCK.get() 是一个 id 为 "yourmodid:example_block" 的 Supplier<Block>
BuiltInRegistries.BLOCKS.getValue(ResourceLocation.fromNamespaceAndPath("yourmodid", "example_block")); // 返回 example block
BuiltInRegistries.BLOCKS.getKey(ExampleBlocksClass.EXAMPLE_BLOCK.get()); // 返回资源位置 "yourmodid:example_block"

如果你只是想检查某个对象是否存在,这也是可以做到的,不过只能通过键来判断:

BuiltInRegistries.BLOCKS.containsKey(ResourceLocation.fromNamespaceAndPath("minecraft", "dirt")); // true
BuiltInRegistries.BLOCKS.containsKey(ResourceLocation.fromNamespaceAndPath("create", "brass_ingot")); // 只有当 Create 已安装时才为 true

正如上面的例子所示,这种方式可以用于任意 mod id,因此是检测其它 mod 是否存在特定物品的理想方法。 我们还可以遍历注册表(Registry)中的所有条目,无论是遍历键(key),还是遍历条目(entry,条目采用 Java 的 Map.Entry 类型):

for (ResourceLocation id : BuiltInRegistries.BLOCKS.keySet()) {
// ...
}
for (Map.Entry<ResourceKey<Block>, Block> entry : BuiltInRegistries.BLOCKS.entrySet()) {
// ...
}
note

查询操作始终使用原版的 Registry,而不是 DeferredRegister。这是因为 DeferredRegister 只是用于注册的工具。

danger

只有在注册完成后,查询操作才是安全的。切勿在注册过程中查询注册表!

自定义注册表(Custom Registries)

自定义注册表允许你为其他模组(mod)的附加内容提供可扩展的系统。例如,如果你的模组添加了法术(spells),你可以将法术作为一个注册表,这样其他模组就能向你的模组添加法术,而你无需做额外的处理。此外,这还允许你自动完成一些任务,比如同步条目。

我们先来创建 注册表键(registry key) 和注册表本身:

// 这里我们以法术(spells)作为注册表示例,具体法术是什么并不重要。
// 当然,所有“法术”相关的内容都可以也应该替换为你实际需要的注册表内容。
public static final ResourceKey<Registry<Spell>> SPELL_REGISTRY_KEY = ResourceKey.createRegistryKey(ResourceLocation.fromNamespaceAndPath("yourmodid", "spells"));
public static final Registry<YourRegistryContents> SPELL_REGISTRY = new RegistryBuilder<>(SPELL_REGISTRY_KEY)
// 如果你希望启用整数 ID 同步,用于网络通信。
// 这些选项只应在网络相关场景下使用,比如数据包或专用于网络的 NBT 数据。
.sync(true)
// 默认键。类似于方块的 minecraft:air,这是可选项。
.defaultKey(ResourceLocation.fromNamespaceAndPath("yourmodid", "empty"))
// 有效地限制最大数量。一般不推荐,但在某些网络场景下可能有意义。
.maxId(256)
// 构建注册表。
.create();

接下来,通过在 NewRegistryEvent 中注册到根注册表,告知游戏该注册表的存在:

@SubscribeEvent
static void registerRegistries(NewRegistryEvent event) {
event.register(SPELL_REGISTRY);
}

现在你可以像操作其他注册表一样,通过 DeferredRegisterRegisterEvent 注册新的注册表内容:

public static final DeferredRegister<Spell> SPELLS = DeferredRegister.create(SPELL_REGISTRY, "yourmodid");
public static final Supplier<Spell> EXAMPLE_SPELL = SPELLS.register("example_spell", () -> new Spell(...));

// 另一种方式:
@SubscribeEvent
public static void register(RegisterEvent event) {
event.register(SPELL_REGISTRY_KEY, registry -> {
registry.register(ResourceLocation.fromNamespaceAndPath("yourmodid", "example_spell"), () -> new Spell(...));
});
}

数据包注册表(Datapack Registries)

数据包注册表(Datapack Registry,也称为动态注册表(dynamic registry),或因其主要用途又被称为世界生成注册表(worldgen registry))是一类特殊的注册表(Registry)。它在世界加载时,从 数据包 的 JSON 文件中加载数据(因此得名),而不是在游戏启动时加载。最典型的默认数据包注册表包括大多数世界生成相关的注册表,以及其他少量注册表。

数据包注册表允许你在 JSON 文件中定义其内容。这意味着你无需编写任何代码(除非你不想手动编写 JSON 文件,可以使用 数据生成)。每个数据包注册表都关联有一个 Codec,用于序列化和反序列化,并且每个注册表的 ID 决定了它在数据包中的路径:

  • Minecraft 的数据包注册表采用 data/yourmodid/registrypath 格式(例如 data/yourmodid/worldgen/biome,其中 worldgen/biome 是注册表路径)。
  • 其他所有数据包注册表(NeoForge 或其他模组)采用 data/yourmodid/registrynamespace/registrypath 格式(例如 data/yourmodid/neoforge/biome_modifier,其中 neoforge 是注册表命名空间,biome_modifier 是注册表路径)。

你可以通过 RegistryAccess 获取数据包注册表。这个 RegistryAccess 可以在服务器端通过调用 ServerLevel#registryAccess() 获得,在客户端则通过 Minecraft.getInstance().getConnection()#registryAccess() 获得(注意,只有当你实际连接到某个世界时,客户端的连接才不会为 null)。拿到这些对象后,你就可以像操作其他注册表一样,获取特定元素或遍历其所有内容。

自定义数据包注册表(Custom Datapack Registries)

自定义数据包注册表不需要显式构造一个 Registry 实例。你只需要一个注册表键(registry key)以及至少一个用于(反)序列化内容的 Codec。以前文中的法术(spell)示例为例,将我们的法术注册表注册为数据包注册表大致如下:

public static final ResourceKey<Registry<Spell>> SPELL_REGISTRY_KEY = ResourceKey.createRegistryKey(ResourceLocation.fromNamespaceAndPath("yourmodid", "spells"));

@SubscribeEvent
public static void registerDatapackRegistries(DataPackRegistryEvent.NewRegistry event) {
event.dataPackRegistry(
// 注册表(Registry)的键(key)。
SPELL_REGISTRY_KEY,
// 注册表内容的编解码器(codec)。
Spell.CODEC,
// 注册表内容的网络编解码器。通常与普通编解码器相同。
// 也可以是去除了一些客户端不需要数据的精简版编解码器。
// 可以为 null。如果为 null,注册表条目将不会同步到客户端。
// 也可以省略,省略效果等同于传入 null(会调用只有两个参数的方法重载,自动传 null 给三个参数的方法)。
Spell.CODEC,
// 一个用于通过 RegistryBuilder 配置已构建注册表的消费者(consumer)。
// 可以省略,省略效果等同于传入 builder -> {}。
builder -> builder.maxId(256)
);
}

数据包注册表的数据生成(Data Generation for Datapack Registries)

由于手动编写所有 JSON 文件既繁琐又容易出错,NeoForge 提供了一个 数据提供器(data provider),可以帮你自动生成这些 JSON 文件。这个机制既适用于内置注册表,也适用于你自定义的数据包注册表。

首先,我们需要创建一个 RegistrySetBuilder,并将我们的条目添加进去(一个 RegistrySetBuilder 可以同时管理多个注册表的条目):

new RegistrySetBuilder()
.add(Registries.CONFIGURED_FEATURE, bootstrap -> {
// 通过 bootstrap 上下文注册已配置特性(详见下文)
})
.add(Registries.PLACED_FEATURE, bootstrap -> {
// 通过 bootstrap 上下文注册已放置特性(详见下文)
});

bootstrap 这个 lambda 参数就是我们实际用来注册对象的工具。它的类型是 BootstrapContext。要注册一个对象,只需调用它的 #register 方法,如下所示:

// 我们对象的资源键(resource key)。
public static final ResourceKey<ConfiguredFeature<?, ?>> EXAMPLE_CONFIGURED_FEATURE = ResourceKey.create(
Registries.CONFIGURED_FEATURE,
ResourceLocation.fromNamespaceAndPath(MOD_ID, "example_configured_feature")
);

new RegistrySetBuilder()
.add(Registries.CONFIGURED_FEATURE, bootstrap -> {
bootstrap.register(
// 已配置特性的资源键。
EXAMPLE_CONFIGURED_FEATURE,
// 实际的已配置特性对象。
new ConfiguredFeature<>(Feature.ORE, new OreConfiguration(...))
);
})
.add(Registries.PLACED_FEATURE, bootstrap -> {
// ...
});

BootstrapContext 还可以用来在需要时从其他注册表中查找条目:

public static final ResourceKey<ConfiguredFeature<?, ?>> EXAMPLE_CONFIGURED_FEATURE = ResourceKey.create(
Registries.CONFIGURED_FEATURE,
ResourceLocation.fromNamespaceAndPath(MOD_ID, "example_configured_feature")
);
public static final ResourceKey<PlacedFeature> EXAMPLE_PLACED_FEATURE = ResourceKey.create(
Registries.PLACED_FEATURE,
ResourceLocation.fromNamespaceAndPath(MOD_ID, "example_placed_feature")
);

new RegistrySetBuilder()
.add(Registries.CONFIGURED_FEATURE, bootstrap -> {
bootstrap.register(EXAMPLE_CONFIGURED_FEATURE, ...);
})
.add(Registries.PLACED_FEATURE, bootstrap -> {
HolderGetter<ConfiguredFeature<?, ?>> otherRegistry = bootstrap.lookup(Registries.CONFIGURED_FEATURE);
bootstrap.register(EXAMPLE_PLACED_FEATURE, new PlacedFeature(
otherRegistry.getOrThrow(EXAMPLE_CONFIGURED_FEATURE), // 获取已配置特性(configured feature)
List.of() // 放置时无操作——请根据你的放置参数替换此处
));
});

最后,我们在实际的数据提供器(data provider)中使用我们的 RegistrySetBuilder,并将该数据提供器注册到事件中:

@SubscribeEvent
public static void onGatherData(GatherDataEvent.Client event) {
// 将生成的注册表对象添加到当前的查找提供器中,便于
// 在其他数据生成(datagen)中使用。
this.createDatapackRegistryObjects(
// 用于生成数据的注册表构建器(registry set builder)。
new RegistrySetBuilder().add(...),
// (可选)一个二元消费者(biconsumer),接收与资源键相关联的加载条件
conditions -> {
conditions.accept(resourceKey, condition);
},
// (可选)我们要为其生成条目的 mod id 集合
// 默认情况下,提供当前 mod 容器的 mod id。
Set.of("yourmodid")
);

// 你可以通过调用某个 `#create*` 方法,或通过 `#getLookupProvider`
// 获取实际查找提供器(lookup provider),来使用你生成的条目
// ...
}