注册表
注册是将 Mod 中的物品、方块、实体等对象告知游戏的过程。注册非常重要:没有注册,游戏就不知道这些对象的存在,可能导致难以解释的行为和崩溃。
简单来说,注册表包装了一个映射,将注册名称映射到已注册对象,后者通常称为注册表条目。同一注册表中的名称必须唯一,但不同注册表可以有同名条目。最常见的例子是 BLOCKS 注册表中的方块,其物品形式在 ITEMS 注册表中使用相同名称。
每个注册对象都有唯一的注册名称,以 ResourceLocation 表示。例如,泥土方块为 minecraft:dirt,僵尸为 minecraft:zombie。Mod 对象使用自己的 Mod ID 作为命名空间,而不应使用 minecraft。
原版与 Mod 的区别
为了理解 NeoForge 注册系统的设计,先看看 Minecraft 的做法。以下以方块注册表为例,多数其他注册表的工作方式相同。
注册表通常注册单例,即每个条目只存在一个实例。例如,游戏中所有石头方块实际都是同一个石头方块对象,被显示了许多次。需要石头方块时,可以直接引用已注册实例。
Minecraft 在 Blocks 类中注册全部方块。通过 register 方法调用 Registry#register(),第一个参数为 BuiltInRegistries.BLOCK 方块注册表。注册完成后,Minecraft 根据方块列表执行检查,例如验证所有方块是否加载了模型。
这一切能够工作,主要因为 Minecraft 足够早地加载了 Blocks 类。Mod 类不会被 Minecraft 自动加载,因此需要额外机制。
注册方法
NeoForge 提供 DeferredRegister 和 RegisterEvent 两种注册方式。前者包装后者,推荐使用前者以减少错误。
DeferredRegister
先创建 DeferredRegister:
public static final DeferredRegister<Block> BLOCKS = DeferredRegister.create(
// The registry we want to use.
// Minecraft's registries can be found in BuiltInRegistries, NeoForge's registries can be found in NeoForgeRegistries.
// Mods may also add their own registries, refer to the individual mod's documentation or source code for where to find them.
BuiltInRegistries.BLOCK,
// Our mod id.
ExampleMod.MOD_ID
);
再将条目添加为静态 final 字段。new Block() 的参数请参阅方块一篇:
public static final DeferredHolder<Block, Block> EXAMPLE_BLOCK = BLOCKS.register(
"example_block", // Our registry name.
() -> new Block(...) // A supplier of the object we want to register.
);
DeferredHolder<R, T extends R> 持有对象。R 是注册表类型,此例为 Block;T 是 supplier 所提供的类型。直接注册 Block 时,第二个参数也为 Block;注册 SlabBlock 等子类时,则填写对应子类。
DeferredHolder<R, T extends R> 是 Supplier<T> 的子类,需要对象时调用 DeferredHolder#get()。也因此,字段可以声明为 Supplier,上例可写成:
public static final Supplier<Block> EXAMPLE_BLOCK = BLOCKS.register(
"example_block", // Our registry name.
() -> new Block(...) // A supplier of the object we want to register.
);
有些位置明确要求 Holder 或 DeferredHolder,不能接收任意 Supplier。遇到这种情况,最好按需将字段类型改回相应类型。
最后,由于整个系统包装了注册事件,必须让 DeferredRegister 在需要时订阅这些事件:
//This is our mod constructor
public ExampleMod(IEventBus modBus) {
//highlight-next-line
ExampleBlocksClass.BLOCKS.register(modBus);
//Other stuff here
}
说明 方块和物品各有提供辅助方法的专用版本,分别为
DeferredRegister.Blocks和DeferredRegister.Items。
RegisterEvent
RegisterEvent 是另一种注册方式。此事件对每个注册表分别触发,发生在 Mod 构造函数之后、配置加载之前,因为 DeferredRegister 正是在 Mod 构造函数中注册内部事件处理器。它在 Mod 事件总线上触发。
@SubscribeEvent // on the mod event bus
public static void register(RegisterEvent event) {
event.register(
// This is the registry key of the registry.
// Get these from BuiltInRegistries for vanilla registries,
// or from NeoForgeRegistries.Keys for NeoForge registries.
BuiltInRegistries.BLOCK,
// Register your objects here.
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(ResourceLocation)到不同对象的可逆映射,因此两种操作都可以:
BuiltInRegistries.BLOCK.get(ResourceLocation.fromNamespaceAndPath("minecraft", "dirt")); // returns the dirt block
BuiltInRegistries.BLOCK.getKey(Blocks.DIRT); // returns the resource location "minecraft:dirt"
// Assume that ExampleBlocksClass.EXAMPLE_BLOCK.get() is a Supplier<Block> with the id "yourmodid:example_block"
BuiltInRegistries.BLOCK.get(ResourceLocation.fromNamespaceAndPath("yourmodid", "example_block")); // returns the example block
BuiltInRegistries.BLOCK.getKey(ExampleBlocksClass.EXAMPLE_BLOCK.get()); // returns the resource location "yourmodid:example_block"
也可以只检查对象是否存在,但只能通过键检查:
BuiltInRegistries.BLOCK.containsKey(ResourceLocation.fromNamespaceAndPath("minecraft", "dirt")); // true
BuiltInRegistries.BLOCK.containsKey(ResourceLocation.fromNamespaceAndPath("create", "brass_ingot")); // true only if Create is installed
如最后一个例子所示,可以使用任意 Mod ID,因此很适合检查其他 Mod 的某个物品是否存在。
还可以遍历注册表的所有键或条目,条目使用 Java Map.Entry 类型:
for (ResourceLocation id : BuiltInRegistries.BLOCK.keySet()) {
// ...
}
for (Map.Entry<ResourceKey<Block>, Block> entry : BuiltInRegistries.BLOCK.entrySet()) {
// ...
}
提示 查询始终使用原版
Registry,而不是DeferredRegister。后者只是注册辅助工具。
重要警告 只有注册完成后,查询才是安全的。不要在注册仍进行时查询注册表!
自定义注册表
自定义注册表允许为其他附属 Mod 提供接入系统。例如,Mod 添加法术时,可以将法术设为注册表,让其他 Mod 无需你额外处理就能添加法术。注册表还支持自动同步条目等功能。
先创建注册表键与注册表本身:
// We use spells as an example for the registry here, without any details about what a spell actually is (as it doesn't matter).
// Of course, all mentions of spells can and should be replaced with whatever your registry actually is.
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)
// If you want to enable integer id syncing, for networking.
// These should only be used in networking contexts, for example in packets or purely networking-related NBT data.
.sync(true)
// The default key. Similar to minecraft:air for blocks. This is optional.
.defaultKey(ResourceLocation.fromNamespaceAndPath("yourmodid", "empty"))
// Effectively limits the max count. Generally discouraged, but may make sense in settings such as networking.
.maxId(256)
// Build the registry.
.create();
然后在 NewRegistryEvent 中将其注册到根注册表,告知游戏该注册表的存在:
@SubscribeEvent // on the mod event bus
public static void registerRegistries(NewRegistryEvent event) {
event.register(SPELL_REGISTRY);
}
现在可以像其他注册表一样,通过 DeferredRegister 或 RegisterEvent 注册内容:
public static final DeferredRegister<Spell> SPELLS = DeferredRegister.create("yourmodid", SPELL_REGISTRY);
public static final Supplier<Spell> EXAMPLE_SPELL = SPELLS.register("example_spell", () -> new Spell(...));
// Alternatively:
@SubscribeEvent // on the mod event bus
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 或其他 Mod 的注册表使用
data/yourmodid/registrynamespace/registrypath,例如data/yourmodid/neoforge/biome_modifier,其中neoforge为注册表命名空间,biome_modifier为路径。
数据包注册表可以从 RegistryAccess 获取。服务器端调用 ServerLevel#registryAccess();客户端调用 Minecraft.getInstance().getConnection()#registryAccess()。客户端必须已连接世界,否则连接为 null。之后可以像其他注册表一样获取元素或遍历内容。
自定义数据包注册表
无需构造 Registry,只需要注册表键和至少一个用于序列化/反序列化的 Codec。继续使用法术示例,将其注册为数据包注册表如下:
public static final ResourceKey<Registry<Spell>> SPELL_REGISTRY_KEY = ResourceKey.createRegistryKey(ResourceLocation.fromNamespaceAndPath("yourmodid", "spells"));
@SubscribeEvent // on the mod event bus
public static void registerDatapackRegistries(DataPackRegistryEvent.NewRegistry event) {
event.dataPackRegistry(
// The registry key.
SPELL_REGISTRY_KEY,
// The codec of the registry contents.
Spell.CODEC,
// The network codec of the registry contents. Often identical to the normal codec.
// May be a reduced variant of the normal codec that omits data that is not needed on the client.
// May be null. If null, registry entries will not be synced to the client at all.
// May be omitted, which is functionally identical to passing null (a method overload
// with two parameters is called that passes null to the normal three parameter method).
Spell.CODEC
);
}
数据包注册表的数据生成
手写全部 JSON 繁琐且容易出错。NeoForge 提供数据提供器来生成文件,适用于内置和自定义数据包注册表。
先创建 RegistrySetBuilder 并添加条目。一个构建器可以保存多个注册表的条目:
new RegistrySetBuilder()
.add(Registries.CONFIGURED_FEATURE, bootstrap -> {
// Register configured features through the bootstrap context (see below)
})
.add(Registries.PLACED_FEATURE, bootstrap -> {
// Register placed features through the bootstrap context (see below)
});
bootstrap lambda 参数实际用于注册对象,类型为 BootstrapContext。调用其 #register 注册对象:
// The resource key of our object.
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(
// The resource key of our configured feature.
EXAMPLE_CONFIGURED_FEATURE,
// The actual 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), // Get the configured feature
List.of() // No-op when placement happens - replace with whatever your placement parameters are
));
});
最后,在实际数据提供器中使用 RegistrySetBuilder,并将数据提供器注册到事件:
@SubscribeEvent // on the mod event bus
public static void onGatherData(GatherDataEvent event) {
event.getGenerator().addProvider(
// Only run datapack generation when server data is being generated
event.includeServer(),
// Create the provider
output -> new DatapackBuiltinEntriesProvider(
output,
event.getLookupProvider(),
// Our registry set builder to generate the data from.
new RegistrySetBuilder().add(...),
// A set of mod ids we are generating. Usually only your own mod id.
Set.of("yourmodid")
)
);
}
来源: NeoForged/Documentation(1.21.1)。中文翻译依据提交
4a680cdd4d15。