流体

流体

原版 Minecraft 的两种流体——水和熔岩——是特殊的方块,能够向一定距离内的相邻方块扩散。它们通常不是固体,实体可以进入并在其中“游泳”。

在模组 Minecraft 中,尤其是许多科技 Mod 中,流体还会充当配方原料。这是因为流体存在于独立的注册表中,只是借助流体方块放入世界;因此,流体实际上可以完全独立于方块理解。

本文介绍如何添加自己的流体。世界中的流体行为请参阅世界中的流体;在配方中使用流体请参阅配方中的流体。

警告 原版只有两种流体,并且对它们进行了大量特殊处理。因此,本分类中的部分系统实现方式比较迂回,又存在难以通过测试全面覆盖的边界情况,不一定总能正确工作。如果发现流体相关错误,请通过 Discord 联系我们,或在 GitHub 提交问题。

Fluid 与 FluidType

注册流体前,需要先了解 Minecraft 和 NeoForge 的一些设计选择。

Minecraft 中,水和熔岩各有流动流体和源流体两个变体。它们主要通过硬编码运作,同时与 FluidState 有一定关联,详见下文。这种硬编码不易使用,有时甚至难以复用。因此,NeoForge 引入 FluidType 类,并修改了大量位置来使用它。FluidType 主要保存流体共用的逻辑,例如声音、是否支持划船等,只将实际流动逻辑留在流体自身中。FluidType 位于 NeoForge 新增的独立注册表中,所以除了注册 Fluid,还必须注册 FluidType。

了解这些概念后,就可以开始创建流体。以下以熔融铁流体为例。首先需要两个注册表:

public static final DeferredRegister<Fluid> FLUIDS =
        DeferredRegister.create(Registries.FLUID, ExampleMod.MOD_ID);
public static final DeferredRegister<FluidType> FLUID_TYPES =
        DeferredRegister.create(NeoForgeRegistries.FLUID_TYPES, ExampleMod.MOD_ID);

创建 Fluid 需要 FluidType,因此先创建后者。FluidType 的选项通过 Properties 对象定义,与方块属性类似。

public static final DeferredHolder<FluidType, FluidType> MOLTEN_IRON_TYPE = FLUID_TYPES.register(
        // The registry name of the fluid type. Usually it makes sense to name it the same as the `Fluid`.
        "molten_iron",
        // The factory for the fluid type, accepting a `FluidType.Properties` object.
        id -> new FluidType(FluidType.Properties.create()
                // The translation key of the fluid. While this will not be visible in vanilla Minecraft,
                // it will be visible if the fluid is stored in e.g. a modded tank, or when looked at
                // in-world with WAILA (What Am I Looking At?) or similar mods installed.
                // `id` is the lambda parameter we got passed in.
                .descriptionId(Util.makeDescriptionId("fluid", id))
                // Set lava-like sounds for our fluid. This is only relevant if you have a bucket item,
                // which we will look at later.
                .sound(SoundActions.BUCKET_FILL, SoundEvents.BUCKET_FILL_LAVA)
                .sound(SoundActions.BUCKET_EMPTY, SoundEvents.BUCKET_EMPTY_LAVA)
                // We cannot swim or drown in molten iron.
                .canDrown(false)
                .canSwim(false)
                // We want molten iron to slightly glow.
                .lightLevel(5)
        ));

建议 FluidType 中还有许多其他方法。例如,创建类似水的流体时,可以关注 supportsBoating() 和 isWaterLike()。完整方法列表请查看 FluidType.Properties 的源代码。

并非所有方法都由原版系统使用。其中一些方法,例如 temperature() 和 density(),是在 FluidType 系统最初设计阶段为 Mod 兼容需求提出的,具体 Mod 系统可能会使用,也可能不会。

创建 FluidType 后,接着创建 Fluid。NeoForge 提供了基础类 BaseFlowingFluid,它有 Source、Flowing 和 Properties 三个内部类。Source 与 Flowing 都继承 BaseFlowingFluid,结构沿用原版 WaterFluid 和 LavaFluid 的设计。Properties 同样类似方块属性对象,用于关联流体类型、源流体、流动流体,以及后面会介绍的桶和流体方块等。

源流体、流动流体依赖流体属性,而流体属性又依赖这两种流体,因此需要注意静态初始化顺序,并在部分位置加上类名限定。假设流体统一放在 ModFluids 类中,代码如下:

// The source fluid. This is usually named without specifying "source" in the name. 
public static final DeferredHolder<Fluid, BaseFlowingFluid.Source> MOLTEN_IRON = FLUIDS.register(
        // The registry name.
        "molten_iron",
        // The source fluid supplier. Qualify the properties with the class name here.
        () -> new BaseFlowingFluid.Source(ModFluids.MOLTEN_IRON_PROPERTIES));

// The flowing fluid. The name is commonly prefixed with "flowing_".
public static final DeferredHolder<Fluid, BaseFlowingFluid.Flowing> FLOWING_MOLTEN_IRON = FLUIDS.register(
        // The registry name.
        "flowing_molten_iron",
        // The flowing fluid supplier. Again, qualify the properties with the class name.
        () -> new BaseFlowingFluid.Flowing(ModFluids.MOLTEN_IRON_PROPERTIES));

// The fluid properties. We will use this later to connect additional stuff
// to the fluid, for example the bucket.
public static final BaseFlowingFluid.Properties MOLTEN_IRON_PROPERTIES =
        // Parameters are the fluid type, the source fluid and the flowing fluid.
        new BaseFlowingFluid.Properties(MOLTEN_IRON_TYPE, MOLTEN_IRON, FLOWING_MOLTEN_IRON);

完成后,流体应已加载到游戏中,并能被配方使用,但渲染还不正常。需要在仅客户端的 Mod 总线事件处理器中注册流体模型:

@SubscribeEvent // on the mod event bus only on the physical client
private static void registerFluidModels(RegisterFluidModelsEvent event) {
    event.register(new FluidModel.Unbaked(
            // The still, flowing and overlay texture materials. The overlay material is nullable;
            // if it is null, no overlay will be displayed. Overlays are only used for in-world fluids,
            // so if you don't have an in-world fluid, it should always be null.
            new Material(Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "block/molten_iron_still")),
            new Material(Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "block/molten_iron_flowing")),
            new Material(Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "block/molten_iron_overlay")),
            // The fluid tint source. We leave it at null, which means no tint. See below for more info.
            null,
            // The fluid renderer. Can be supplied for entirely custom fluid rendering.
            // This parameter is optional and will default to null if omitted.
            null),
            // Suppliers for the still and flowing fluids.
            ModFluids.MOLTEN_IRON,
            ModFluids.FLOWING_MOLTEN_IRON
    );
}

资源

流体虽然已经存在,但还需要添加资源文件,包括纹理、模型和翻译。

首先添加纹理。制作资源时,建议以原版水或熔岩纹理为基础。流动流体尤其需要注意:它实际上使用 2×2 的纹理,由流体渲染器采样。文件命名和位置如下,其中 examplemod 为你的 Mod ID:

  • 静止纹理:assets/examplemod/textures/block/molten_iron_still.png。
  • 流动纹理:assets/examplemod/textures/block/molten_iron_flowing.png。
  • 覆盖纹理(需要时):assets/examplemod/textures/block/molten_iron_overlay.png。

警告 这些路径与先前传入 RegisterFluidModelsEvent#register() 的路径对应。也可以放在其他位置,但必须同步调整渲染器中的路径。

流体纹理位于方块图集中,因此应放在 textures/block 文件夹中。

大多数流体具有动画,因此还需要配套的 .png.mcmeta 文件。同样可以参考原版文件。更多信息请参阅纹理文档。

最后是翻译。流体的翻译键由 FluidType#descriptionId() 定义,可以通过 FluidType#getDescriptionId() 获取:

// In your LanguageProvider
@Override
protected void addTranslations() {
    add(ModFluids.MOLTEN_IRON_TYPE.getDescriptionId(), "Molten Iron");
}

更多信息请参阅国际化、本地化与数据生成。

流体着色源

另见:着色

流体和方块一样可以着色。原版水使用着色,熔岩则不使用。NeoForge 修改了该系统以支持 Mod。相关逻辑均通过 FluidTintSource 接口实现;简单实现只需重写 #color():

// If possible, we want to use a singleton.
public final class MoltenIronTintSource implements FluidTintSource {
    public static final MoltenIronTintSource INSTANCE = new MoltenIronTintSource();
    
    private MoltenIronTintSource() {}

    @Override
    public int color(FluidState state) {
        // Return whatever color you want here. The value is in ARGB; make sure that you include
        // a proper alpha value, otherwise the rendering will be invisible.
        return 0xff000000;
    }
}

创建着色源后,在 RegisterFluidModelsEvent 中使用:

@SubscribeEvent // on the mod event bus only on the physical client
private static void registerFluidModels(RegisterFluidModelsEvent event) {
    event.register(new FluidModel.Unbaked(
            new Material(Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "block/molten_iron_still")),
            new Material(Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "block/molten_iron_flowing")),
            new Material(Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "block/molten_iron_overlay")),
            // Use our tint source instance here.
            MoltenIronTintSource.INSTANCE),
            ModFluids.MOLTEN_IRON,
            ModFluids.FLOWING_MOLTEN_IRON
    );
}

建议 如果实现只重写 #color(FluidState),也可以用函数式接口 lambda 代替单例类。

需要更复杂的行为时,还可以使用以下方法。它们默认均委托给 #color(FluidState):

  • colorInWorld(FluidState fluidState, BlockState blockState, BlockAndTintGetter level, BlockPos pos):在世界中显示流体时使用,可以根据位置决定颜色。水通过它实现随生物群系变化的颜色。
  • colorAsStack(FluidStack stack):根据 FluidStack 决定颜色,例如根据数据组件着色。原版不使用它,因为 FluidStack 是 NeoForge 提供的系统。

此外,FluidTintSource 继承 BlockTintSource,因此根据 BlockState 着色的全部方法也都可用。


来源: NeoForged/Documentation(26.1)。中文翻译依据提交 4a680cdd4d15。