注册是将模组的对象(例如物品、方块、实体等)提供给游戏的过程。注册事物很重要,因为没有注册,游戏将根本不知道这些对象,这将导致无法解释的行为和崩溃。
简而言之,注册表是围绕映射的包装器,该映射将注册名称(稍后介绍)映射到已注册的对象,通常称为注册条目。注册名称在同一注册表中必须是唯一的,但相同的注册名称可能存在于多个注册表中。最常见的例子是方块(在 BLOCKS 注册表中),它们具有具有相同注册名称(在 ITEMS 注册表中)的物品形式。
每个注册的对象都有一个唯一的名称,称为其注册名称。该名称表示为 ResourceLocation。例如,泥土方块的注册名称是 minecraft:dirt
,僵尸的注册名称是 minecraft:zombie
。模组对象当然不会使用 minecraft
命名空间;而是使用它们的模组 ID。
原版 vs 模组
为了理解 NeoForge 的注册系统中做出的一些设计决策,我们将首先看看 Minecraft 是如何做到这一点的。我们将以方块注册表为例,因为大多数其他注册表的工作方式相同。
注册表通常注册单例。这意味着所有注册条目都只存在一次。例如,你在整个游戏中看到的所有石头方块实际上都是同一个石头方块,只是显示了很多次。如果你需要石头方块,你可以通过引用已注册的方块实例来获取它。
Minecraft 在 Blocks
类中注册所有方块。通过 register
方法,调用 Registry#register()
,其中 BuiltInRegistries.BLOCK
处的方块注册表是第一个参数。在注册完所有方块后,Minecraft 会根据方块列表执行各种检查,例如自检,以验证所有方块是否都加载了模型。
这一切之所以有效的主要原因是 Blocks
由 Minecraft 足够早地进行类加载。模组不会由 Minecraft 自动进行类加载,因此需要一些变通方法。
注册的途径
NeoForge 提供了两种注册对象的方式:DeferredRegister
类和 RegisterEvent
。请注意,前者是后者的包装器,为了防止出错,建议使用前者。
DeferredRegister
我们从创建我们自己的DeferredRegister
开始:
public static final DeferredRegister<Block> BLOCKS = DeferredRegister.create(
// 我们想要使用的注册表.
// Minecraft的注册表可以在BuiltInRegistries中找到,NeoForge的注册表可以在NeoForgeRegistries中找到.
// 模组也可以添加它们自己的注册表,请参阅各个模组的文档或源代码以查找它们的位置。
BuiltInRegistries.BLOCKS,
// 我们的模组ID
ExampleMod.MOD_ID
);
之后我们可以使用下面的方法添加自己的注册表入口作为final字段(查阅有关方块的文章以获取new Block()
中可以添加什么参数的信息):
public static final DeferredHolder<Block, Block> EXAMPLE_BLOCK_1 = BLOCKS.register(
// 我们的注册名称.
"example_block"
// 提供我们要注册的对象的供应商。
() -> 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
是我们注册到的注册表的类型(在我们的例子中是 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(...)
);
ℹ️请注意,一些地方明确要求
Holder
或DeferredHolder
,而不仅仅接受任何Supplier
。如果你需要这两个中的任何一个,最好根据需要将Supplier
的类型改回Holder
或DeferredHolder
。
最后,由于整个系统是围绕注册事件的包装器,我们需要告诉 DeferredRegister
根据需要将其自身附加到注册事件:
//这是模组的构造函数
public ExampleMod(IEventBus modBus) {
ExampleBlocksClass.BLOCKS.register(modBus);
//在这里写其他的代码
}
ℹ️有针对方块、物品和数据组件的
DeferredRegister
的专门变体,它们提供了辅助方法:分别是DeferredRegister.Blocks
、DeferredRegister.Items
和DeferredRegister.DataComponents
。
RegisterEvent
RegisterEvent
是注册对象的第二种方式。此事件在模组构造函数之后(因为这些是 DeferredRegister
注册其内部事件处理程序的地方)和配置加载之前,针对每个注册表触发。RegisterEvent
在模组事件总线上触发。
@SubscribeEvent
public void register(RegisterEvent event) {
event.register(
// 这是注册表的注册键。
// 对于原版注册表,请从 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(...));
}
);
}
查询注册表
有时,你会发现自己处于想要通过给定的 ID 获取已注册对象的情况。或者,你想要获取某个已注册对象的 ID。由于注册表基本上是将 ID(ResourceLocations)映射到不同对象的映射,即一个可逆映射,因此这两种操作都有效:
BuiltInRegistries.BLOCKS.getValue(ResourceLocation.fromNamespaceAndPath("minecraft", "dirt")); // 返回泥土方块
BuiltInRegistries.BLOCKS.getKey(Blocks.DIRT); // 返回id"minecraft:dirt"
// 假设ExampleBlocksClass.EXAMPLE_BLOCK.get()是一个id是 "yourmodid:example_block"的Supplier<Block>
BuiltInRegistries.BLOCKS.getValue(ResourceLocation.fromNamespaceAndPath("yourmodid", "example_block")); // 返回的是示例方块
BuiltInRegistries.BLOCKS.getKey(ExampleBlocksClass.EXAMPLE_BLOCK.get()); // 返回id"yourmodid:example_block"
如果你只是想确认一个物体是否存在,这也能通过key做到:
BuiltInRegistries.BLOCKS.containsKey(ResourceLocation.fromNamespaceAndPath("minecraft", "dirt")); // true
BuiltInRegistries.BLOCKS.containsKey(ResourceLocation.fromNamespaceAndPath("create", "brass_ingot")); // 只有在机械动力已经安装的情况下为true
如上一个例子展示的,使用任意的模组id都是可行的,因此这是一个完美的检查另一个模组中的特定物品是否存在的方法.
最终,我们也可以在注册表中遍历所有的实体,使用key或者 无论是通过键还是通过条目(条目使用 Java 的 Map.Entry
类型):
for (ResourceLocation id : BuiltInRegistries.BLOCKS.keySet()) {
// ...
}
for (Map.Entry<ResourceKey<Block>, Block> entry : BuiltInRegistries.BLOCKS.entrySet()) {
// ...
}
⚠️注意
查询操作总是使用原版
Registry
,而不是DeferredRegister
。这是因为DeferredRegister
只是注册实用程序。
❗危险
查询操作仅在注册完成后才能安全使用。请勿在注册仍在进行时查询注册表!
自定义注册表
自定义注册表允许你指定你的模组的附加模组可能想要插入的其他系统。例如,如果你的模组要添加法术,你可以将法术设为一个注册表,从而允许其他模组向你的模组添加法术,而你无需执行任何其他操作。它还允许你执行一些操作,例如自动同步条目。
让我们从创建注册键和注册表本身开始:
// 在这里我们使用法术作为注册表的示例,没有任何关于法术实际是什么的细节(因为它无关紧要)。
// 当然,所有提及法术的地方都可以并且应该替换为你注册表的实际内容。
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)
// 默认key.与用于方块的minecraft:air类似.这是可选的.
.defaultKey(ResourceLocation.fromNamespaceAndPath("yourmodid", "empty"))
// 有效限制了最大计数。通常不建议这样做,但在网络等设置中可能有意义。
.maxId(256)
// 构建注册表
.create();
然后,通过在 NewRegistryEvent
中将它们注册到根注册表来告诉游戏该注册表存在:
@SubscribeEvent
static void registerRegistries(NewRegistryEvent event) {
event.register(SPELL_REGISTRY);
}
现在,你可以像使用任何其他注册表一样,通过 DeferredRegister
和 RegisterEvent
注册新的注册表内容:
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(...));
});
}
数据包注册表
数据包注册表(也称为动态注册表,或者根据其主要用例称为世界生成注册表)是一种特殊的注册表,它在世界加载时从数据包 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
获取数据包注册表。如果在服务器上,可以通过调用 ServerLevel#registryAccess()
来检索此 RegistryAccess
,如果在客户端,则可以通过 Minecraft.getInstance().getConnection()#registryAccess()
来检索(后者仅在你实际连接到世界时才有效,否则连接将为空)。然后可以将这些调用的结果像任何其他注册表一样使用,以获取特定元素或迭代内容。
自定义数据包注册表
自定义数据包注册表不需要构建 Registry
。相反,它们只需要一个注册键和至少一个 Codec
来(反)序列化其内容。重申之前的法术示例,将我们的法术注册表注册为数据包注册表看起来像这样:
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(
// 注册键
SPELL_REGISTRY_KEY,
// 注册内容的codec
Spell.CODEC,
// 注册表内容的网络编解码器。通常与普通编解码器相同。
// 可能是省略了客户端不需要的数据的普通编解码器的简化变体。
// 可以为 null。如果为 null,则注册表条目将根本不会同步到客户端。
// 可以省略,这在功能上与传递 null 相同(调用具有两个参数的方法重载,该方法将 null 传递给普通的三个参数方法)。
Spell.CODEC
);
}
为数据包注册表生成数据
由于手动编写所有 JSON 文件既乏味又容易出错,NeoForge 提供了一个数据提供程序来为你生成 JSON 文件。这适用于内置的和自定义的数据包注册表。
首先,我们创建一个 RegistrySetBuilder
并将我们的条目添加到其中(一个 RegistrySetBuilder
可以保存多个注册表的条目):
new RegistrySetBuilder()
.add(Registries.CONFIGURED_FEATURE, bootstrap -> {
// 通过引导上下文注册配置的功能(见下文)
})
.add(Registries.PLACED_FEATURE, bootstrap -> {
// 通过引导上下文注册放置的功能(见下文)
});
引导 lambda 参数是我们实际用来注册对象的参数。它的类型是 BootstrapContext
。要注册一个对象,我们像这样在其上调用 #register
:
// 我们物体的资源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 -> {
// ...
});
引导上下文还可以用于在需要时查找另一个注册表中的条目:
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), // 获取配置的功能
List.of() // 放置时无操作 - 替换为你自己的放置参数。
));
});
最后,我们在实际的数据提供程序中使用我们的 RegistrySetBuilder
,并将该数据提供程序注册到事件:
@SubscribeEvent
public static void onGatherData(GatherDataEvent event) {
CompletableFuture<HolderLookup.Provider> lookupProvider = event.getGenerator().addProvider(
// 只有在服务器数据生成的时候运行数据包生成
event.includeServer(),
// 创建提供者
output -> new DatapackBuiltinEntriesProvider(
output,
event.getLookupProvider(),
// 我们的注册表设置构造器来生成数据表单
new RegistrySetBuilder().add(...),
// 我们生成的一系列模组id。通常只有你自己模组的id.
Set.of("yourmodid")
)
).getRegistryProvider();
// 使用从你的数据包条目生成的查找提供程序作为所有其他数据提供程序的输入
// ...
}