Skip to content

注册

字数
3281 字
阅读时间
14 分钟

注册

注册是将 Mod 中的对象(例如物品、方块、实体等)告知游戏的过程。注册非常重要,因为如果没有注册,游戏将无法识别这些对象,从而导致无法解释的行为和崩溃。

简单来说,注册表是一个围绕映射的包装器,它将注册名称(见下文)映射到已注册的对象,通常称为注册条目。注册名称在同一注册表中必须是唯一的,但相同的注册名称可能出现在多个注册表中。最常见的例子是方块(在 BLOCKS 注册表中)和其物品形式(在 ITEMS 注册表中),它们具有相同的注册名称。

每个注册对象都有一个唯一的名称,称为其注册名称。该名称以 ResourceLocation 表示。例如,泥土方块的注册名称是 minecraft:dirt,僵尸的注册名称是 minecraft:zombie。Mod 中的对象当然不会使用 minecraft 命名空间,而是使用其 Mod ID。

原版与 Mod 的对比

为了理解 NeoForge 注册系统中的一些设计决策,我们首先来看 Minecraft 是如何处理注册的。我们将以方块注册表为例,因为大多数其他注册表的工作方式相同。

注册表通常注册单例。这意味着所有注册条目只存在一次。例如,您在游戏中看到的所有石头方块实际上是同一个石头方块的多次显示。如果您需要石头方块,可以通过引用已注册的方块实例来获取它。

Minecraft 在 Blocks 类中注册所有方块。通过 register 方法,调用 Registry#register(),并将 BuiltInRegistries.BLOCK 作为第一个参数。在所有方块注册完成后,Minecraft 会根据方块列表执行各种检查,例如验证所有方块是否已加载模型的自我检查。

这一切能够正常工作的主要原因是 Blocks 类在 Minecraft 启动时足够早地被加载。Mod 不会自动被 Minecraft 加载,因此需要一些变通方法。

注册方法

NeoForge 提供了两种注册对象的方式:DeferredRegister 类和 RegisterEvent。请注意,前者是后者的包装器,建议使用前者以避免错误。

DeferredRegister

我们首先创建 DeferredRegister

java
public static final DeferredRegister<Block> BLOCKS = DeferredRegister.create(
    // 我们要使用的注册表。
    // Minecraft 的注册表可以在 BuiltInRegistries 中找到,NeoForge 的注册表可以在 NeoForgeRegistries 中找到。
    // Mod 也可以添加自己的注册表,请参阅各个 Mod 的文档或源代码以找到它们。
    BuiltInRegistries.BLOCKS,
    // 我们的 Mod ID。
    ExampleMod.MOD_ID
);

然后,我们可以将注册条目添加为静态字段(有关 new Block() 中的参数,请参阅关于方块的文章):

java
public static final DeferredHolder<Block, Block> EXAMPLE_BLOCK = BLOCKS.register(
    "example_block", // 我们的注册名称。
    () -> new Block(...) // 我们要注册的对象的供应商。
);

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 作为字段的类型。这样,上面的代码块变为:

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

请注意,某些地方明确要求 HolderDeferredHolder,而不会接受任何 Supplier。如果您需要这两种类型中的任何一种,最好根据需要将 Supplier 的类型更改回 HolderDeferredHolder

最后,由于整个系统是围绕注册事件的包装器,我们需要告诉 DeferredRegister 根据需要附加到注册事件:

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

信息DeferredRegister 有针对方块和物品的专用变体,分别称为 DeferredRegister.BlocksDeferredRegister.Items,它们提供了辅助方法。

RegisterEvent

RegisterEvent 是注册对象的第二种方式。此事件为每个注册表触发,触发时间在 Mod 构造函数之后(因为 DeferredRegisters 在这些构造函数中注册其内部事件处理程序)和配置加载之前。RegisterEvent 在 Mod 事件总线上触发。

java
@SubscribeEvent
public void register(RegisterEvent event) {
    event.register(
        // 这是注册表的注册键。
        // 从 BuiltInRegistries 获取原版注册表,
        // 或从 NeoForgeRegistries.Keys 获取 NeoForge 注册表。
        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(ResourceLocation)到唯一对象的映射,即一个可逆的映射,因此这两种操作都有效:

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

// 假设 ExampleBlocksClass.EXAMPLE_BLOCK.get() 是一个 ID 为 "yourmodid:example_block" 的 Supplier<Block>
BuiltInRegistries.BLOCKS.get(ResourceLocation.fromNamespaceAndPath("yourmodid", "example_block")); // 返回示例方块
BuiltInRegistries.BLOCKS.getKey(ExampleBlocksClass.EXAMPLE_BLOCK.get()); // 返回资源位置 "yourmodid:example_block"

如果您只想检查对象是否存在,这也是可能的,但只能通过键进行检查:

java
BuiltInRegistries.BLOCKS.containsKey(ResourceLocation.fromNamespaceAndPath("minecraft", "dirt")); // true
BuiltInRegistries.BLOCKS.containsKey(ResourceLocation.fromNamespaceAndPath("create", "brass_ingot")); // 仅在安装了 Create 时为 true

如最后一个示例所示,这适用于任何 Mod ID,因此是检查另一个 Mod 中的某个物品是否存在的完美方式。

最后,我们还可以遍历注册表中的所有条目,无论是键还是条目(条目使用 Java 的 Map.Entry 类型):

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

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

警告:查询操作仅在注册完成后才是安全的。在注册仍在进行时不要查询注册表!

自定义注册表

自定义注册表允许您指定其他 Mod 可能希望插入的系统。例如,如果您的 Mod 添加了法术,您可以将法术设为注册表,从而允许其他 Mod 向您的 Mod 添加法术,而您无需做任何其他事情。它还允许您自动执行某些操作,例如同步条目。

让我们首先创建注册表键和注册表本身:

java
// 我们以法术为例,没有详细说明法术是什么(因为它不重要)。
// 当然,所有关于法术的提及都可以并且应该替换为您注册表的实际内容。
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 中将注册表注册到根注册表来告诉游戏注册表的存在:

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

您现在可以像任何其他注册表一样注册新的注册表内容,通过 DeferredRegisterRegisterEvent

java
public static final DeferredRegister<Spell> SPELLS = DeferredRegister.create("yourmodid", SPELL_REGISTRY);
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 或 Mod 的)使用格式 data/yourmodid/registrynamespace/registrypath(例如 data/yourmodid/neoforge/biome_modifier,其中 neoforge 是注册表命名空间,biome_modifier 是注册表路径)。

数据包注册表可以从 RegistryAccess 中获取。可以通过调用 ServerLevel#registryAccess()(如果在服务器上)或 Minecraft.getInstance().getConnection()#registryAccess()(如果在客户端)来获取此 RegistryAccess(后者仅在您实际连接到世界时有效,否则连接将为 null)。然后,这些调用的结果可以像任何其他注册表一样使用,以获取特定元素或遍历内容。

自定义数据包注册表

自定义数据包注册表不需要构建 Registry。相反,它们只需要一个注册表键和至少一个 Codec 来(反)序列化其内容。以前面的法术为例,将我们的法术注册表注册为数据包注册表如下所示:

java
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,
        // 注册表内容的网络 Codec。通常与普通 Codec 相同。
        // 可能是普通 Codec 的简化版本,省略了客户端不需要的数据。
        // 可能为 null。如果为 null,注册表条目将不会同步到客户端。
        // 可以省略,这在功能上等同于传递 null(调用具有两个参数的方法重载,将 null 传递给普通的三参数方法)。
        Spell.CODEC
    );
}

数据包注册表的数据生成

由于手动编写所有 JSON 文件既繁琐又容易出错,NeoForge 提供了一个数据生成器来为您生成 JSON 文件。这适用于内置的和您自己的数据包注册表。

首先,我们创建一个 RegistrySetBuilder 并向其中添加我们的条目(一个 RegistrySetBuilder 可以包含多个注册表的条目):

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

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

java
// 我们对象的资源键。
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 还可以用于从另一个注册表中查找条目:

java
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,并将该数据生成器注册到事件中:

java
@SubscribeEvent
static void onGatherData(GatherDataEvent event) {
    event.getGenerator().addProvider(
        // 仅在生成服务器数据时运行数据包生成
        event.includeServer(),
        // 创建生成器
        output -> new DatapackBuiltinEntriesProvider(
            output,
            event.getLookupProvider(),
            // 我们用于生成数据的 RegistrySetBuilder。
            new RegistrySetBuilder().add(...),
            // 我们正在生成的 Mod ID 集合。通常只有您自己的 Mod ID。
            Set.of("yourmodid")
        )
    );
}

贡献者

The avatar of contributor named as 小飘 小飘

文件历史