注册表
注册是将 Mod 的对象(如物品、方块、声音等)告知游戏的过程。注册非常重要:没有注册,游戏就不知道这些对象的存在,可能造成难以解释的行为和崩溃。
原创说明图,依据 Forge 1.21.x 官方文档 绘制。
游戏中大多数需要注册的内容由 Forge 注册表处理。注册表类似映射,将键关联到值。Forge 使用以 ResourceLocation 为键的注册表,因此 ResourceLocation 可以充当对象的“注册表名称”。
每种可注册对象都有自己的注册表。Forge 包装的所有注册表可见 ForgeRegistries 类。同一个注册表中的名称必须唯一,但不同注册表中的名称不会冲突。例如 Block 和 Item 可以各自注册名为 example:thing 的对象;但若两个不同的 Block 或两个不同的 Item 使用完全相同的名称,第二个对象会覆盖第一个。
注册方法
正确注册对象有两种方式:DeferredRegister 类和 RegisterEvent 生命周期事件。
DeferredRegister
DeferredRegister 是推荐方式。它保留静态初始化器的便利,同时避免其相关问题。它维护条目 supplier 列表,在 RegisterEvent 期间从这些 supplier 注册对象。
下面是注册自定义方块的例子:
private static final DeferredRegister<Block> BLOCKS = DeferredRegister.create(ForgeRegistries.BLOCKS, MODID);
public static final RegistryObject<Block> ROCK_BLOCK = BLOCKS.register("rock", () -> new Block(BlockBehaviour.Properties.of().mapColor(MapColor.STONE)));
public ExampleMod(FMLJavaModLoadingContext context) {
BLOCKS.register(context.getModEventBus());
}
RegisterEvent
RegisterEvent 是第二种注册方式。此事件在 Mod 构造函数之后、配置加载之前针对每个注册表触发。向 #register 传入注册表键、注册对象名称和对象本身即可注册对象。另一种重载接收被消费的 helper,以给定名称注册对象;建议使用该方法,避免不必要地创建对象。
例如,以下处理器注册在 Mod 事件总线上:
@SubscribeEvent
public void register(RegisterEvent event) {
event.register(ForgeRegistries.Keys.BLOCKS,
helper -> {
helper.register(ResourceLocation.fromNamespaceAndPath(MODID, "example_block_1"), new Block(...));
helper.register(ResourceLocation.fromNamespaceAndPath(MODID, "example_block_2"), new Block(...));
helper.register(ResourceLocation.fromNamespaceAndPath(MODID, "example_block_3"), new Block(...));
// ...
}
);
}
不属于 Forge 注册表的注册表
并非所有注册表都由 Forge 包装。有些是静态注册表,例如 LootItemConditionType,可以安全使用;也有动态注册表,例如 ConfiguredFeature 和其他一些世界生成注册表,通常以 JSON 表示。DeferredRegister#create 的一个重载允许指定原版注册表的键,进而为其创建 RegistryObject。注册方法及挂接 Mod 事件总线的方式与其他 DeferredRegister 相同。
重要:动态注册表对象只能通过数据文件(例如 JSON)注册,不能在代码中注册。
private static final DeferredRegister<LootItemConditionType> REGISTER = DeferredRegister.create(Registries.LOOT_CONDITION_TYPE, "examplemod");
public static final RegistryObject<LootItemConditionType> EXAMPLE_LOOT_ITEM_CONDITION_TYPE = REGISTER.register("example_loot_item_condition_type", () -> new LootItemConditionType(...));
注意:有些类本身不能注册,应注册对应的
*Type类,并在前者构造函数中使用。例如 BlockEntity 对应BlockEntityType,Entity对应EntityType。这些*Type是按需创建对应类型实例的工厂。
工厂使用对应的 *Type$Builder 类创建。例如,以下 REGISTER 指 DeferredRegister<BlockEntityType>:
public static final RegistryObject<BlockEntityType<ExampleBlockEntity>> EXAMPLE_BLOCK_ENTITY = REGISTER.register(
"example_block_entity", () -> BlockEntityType.Builder.of(ExampleBlockEntity::new, EXAMPLE_BLOCK.get()).build(null)
);
引用已注册对象
创建并注册对象时,不应将对象存入字段。每次该注册表的 RegisterEvent 触发时,都应重新创建并注册对象,以便 Forge 未来版本支持动态加载和卸载 Mod。
必须始终通过 RegistryObject 或带 @ObjectHolder 的字段引用已注册对象。
使用 RegistryObject
RegistryObject 可在注册对象可用后获取其引用。DeferredRegister 使用它返回已注册对象的引用。相应注册表的 RegisterEvent 调用后,这些引用与 @ObjectHolder 注解一起更新。
调用 RegistryObject#create,传入 ResourceLocation 和可注册对象的 IForgeRegistry,即可获取 RegistryObject。自定义注册表也可通过提供注册表名称使用。将 RegistryObject 存在 public static final 字段中,需使用对象时调用 #get。
public static final RegistryObject<Item> BOW = RegistryObject.create(ResourceLocation.withDefaultNamespace("bow"), ForgeRegistries.ITEMS);
// 假设 'neomagicae:mana_type' 是有效注册表,
// 'neomagicae:coffeinum' 是该注册表中的有效对象
public static final RegistryObject<ManaType> COFFEINUM = RegistryObject.create(ResourceLocation.fromNamespaceAndPath("neomagicae", "coffeinum"), ResourceLocation.fromNamespaceAndPath("neomagicae", "mana_type"), "neomagicae");
使用 @ObjectHolder
给类或字段添加 @ObjectHolder 注解并提供足够的信息,以构造标识特定注册表中特定对象的 ResourceLocation,即可将注册对象注入 public static 字段。
规则如下:
- 类带有
@ObjectHolder时,其值是该类所有字段的默认命名空间,除非另有明确指定。 - 类带有
@Mod时,其 Mod ID 是该类所有带注解字段的默认命名空间,除非另有明确指定。 - 字段被考虑注入的条件是:至少具有
public static修饰符;字段本身带有@ObjectHolder,并且明确指定名称值和注册表名称值。 - 字段缺少对应注册表或名称时会抛出编译时异常。
- 生成的
ResourceLocation不完整或无效(路径包含无效字符)时会抛出异常。 - 如果没有其他错误或异常,则注入该字段。
- 如果上述规则均不适用,则不执行操作,可能会记录一条消息。
相关注册表的 RegisterEvent 触发后,@ObjectHolder 字段与 RegistryObject 一起注入值。如果注入时对象不在注册表中,将记录调试消息且不注入任何值。
由于规则较复杂,下面给出示例:
class Holder {
@ObjectHolder(registryName = "minecraft:enchantment", value = "minecraft:flame")
public static final Enchantment flame = null;
// 有注解;必须为 public static,final 可选。
// 明确指定注册表名称:"minecraft:enchantment"
// 明确指定资源位置:"minecraft:flame"
// 将附魔注册表中的 "minecraft:flame" 注入字段。
public static final Biome ice_flat = null;
// 字段没有注解,因此被忽略。
@ObjectHolder("minecraft:creeper")
public static Entity creeper = null;
// 有注解;必须为 public static。
// 字段未指定注册表,因此会产生编译时异常。
@ObjectHolder(registryName = "potion")
public static final Potion levitation = null;
// 有注解;必须为 public static,final 可选。
// 明确指定注册表名称:"minecraft:potion"。
// 字段未指定资源位置,因此会产生编译时异常。
}
创建自定义 Forge 注册表
自定义注册表通常可以只是一个简单的键值映射。这很常见,但会强制依赖该注册表的存在,而且必须手动同步各端之间需要同步的数据。自定义 Forge 注册表提供一种简单替代方案,可创建软依赖,并提供更好的管理以及自动跨端同步(除非另行指定)。对象同样使用 Forge 注册表,因此注册方式也保持一致。
自定义 Forge 注册表借助 RegistryBuilder 创建,可以使用 NewRegistryEvent 或 DeferredRegister。RegistryBuilder 接收注册表名称、ID 范围及注册表不同事件的回调等参数。NewRegistryEvent 触发完成后,新注册表会注册到 RegistryManager。
新建注册表应使用对应的注册方法注册其对象。
使用 NewRegistryEvent
在 NewRegistryEvent 中,向 #create 传入 RegistryBuilder 会返回由 supplier 包装的注册表。事件完成向 Mod 事件总线发布后,才能访问 supplier 提供的注册表;在事件触发完成前获取会得到 null。
新的数据包注册表
通过 Mod 事件总线上的 DataPackRegistryEvent$NewRegistry 添加数据包注册表。向 #dataPackRegistry 传入代表注册表名称的 ResourceKey 和用于在 JSON 中编码、解码数据的 Codec 来创建注册表。还可提供可选的 Codec,以将数据包注册表同步到客户端。
重要:数据包注册表不能使用
DeferredRegister创建,只能通过该事件创建。
使用 DeferredRegister
DeferredRegister 方法也是上述事件的一层包装。使用接收注册表名称和 Mod ID 的 #create 重载在常量字段中创建 DeferredRegister 后,可通过 DeferredRegister#makeRegistry 构造注册表。该方法接收提供 RegistryBuilder 的 supplier,以包含附加配置;默认已经填充 #setName。由于此方法可以随时返回,因此返回的是由 supplier 提供的 IForgeRegistry。在 NewRegistryEvent 触发之前从 supplier 获取自定义注册表会得到 null。
重要:必须在通过
#register将DeferredRegister添加到 Mod 事件总线之前调用DeferredRegister#makeRegistry。#makeRegistry也使用#register方法在NewRegistryEvent期间创建注册表。
处理缺失条目
Mod 更新或更常见的移除情况下,某些注册对象可能不再存在。可以通过注册表事件中的 MissingMappingsEvent 指定缺失映射的处理方式。在该事件中,向 #getMappings 提供注册表键和 Mod ID 可获取缺失映射列表;向 #getAllMappings 提供注册表键可获取所有映射。
重要:
MissingMappingsEvent在 Forge 事件总线上触发。
每个 Mapping 可以选择以下四种操作之一:
| 操作 | 说明 |
|---|---|
IGNORE |
忽略缺失条目并放弃映射。 |
WARN |
在日志中生成警告。 |
FAIL |
阻止世界加载。 |
REMAP |
将条目重新映射到已注册的非空对象。 |
如果未指定操作,默认会通知用户缺失条目,并询问是否仍要加载世界。除重新映射之外,所有操作都会阻止其他注册对象占用原有 ID,以防关联条目将来重新添加到游戏中。
原文:Registries。