本篇教程的视频

本篇教程的源代码

GitHub地址:

流体类型基类

Forge中,添加流体的方法与Fabric有些不一样

当然总体上还是差不多的,我们需要完成流体类型流体流体方块三部分内容的编写

简单来说,FluidType决定“是什么液体”,Fluid决定“怎么流”,LiquidBlock决定“如何作为方块存在于世界中”

  • FluidTypeForge特有的抽象层,它决定流体的各种属性,如密度、粘度、温度、雾效等等
  • Fluid:真正的流体对象,决定流体的流动速度、衰减规则等
  • LiquidBlock:负责流体的显示、交互逻辑(能够倒出来的流体,基本都需要依赖LiquidBlock来实现)

首先我们来创建一个BaseFluidType

1
2
3
4
5
public class BaseFluidType extends FluidType {
public BaseFluidType(Properties properties) {
super(properties);
}
}

这个类作为我们流体类型的基类,在这里面我们来定义一些流体的属性

添加字段

当然,这里我们首先要修改构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private final ResourceLocation stillTexture;
private final ResourceLocation flowingTexture;
private final ResourceLocation overlayTexture;
private final int tintColor;
private final Vector3f fogColor;

public BaseFluidType(ResourceLocation stillTexture, ResourceLocation flowingTexture,
ResourceLocation overlayTexture, int tintColor, Vector3f fogColor,
Properties properties) {
super(properties);
this.stillTexture = stillTexture;
this.flowingTexture = flowingTexture;
this.overlayTexture = overlayTexture;
this.tintColor = tintColor;
this.fogColor = fogColor;
}

这里我们加上五个新的字段

  • stillTexture:静止状态流体的纹理
  • flowingTexture:流动状态流体的纹理
  • overlayTexture:覆盖非不透明方块的纹理
  • tintColor:流体本身的颜色
  • fogColor:流体中迷雾的颜色

然后在构造函数中,添加各自的形参,并初始化这些属性

重写 initializeClient 方法

接下来我们还要重写initializeClient方法,让渲染器能正确渲染流体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Override
public void initializeClient(Consumer<IClientFluidTypeExtensions> consumer) {
consumer.accept(new IClientFluidTypeExtensions() {
@Override
public ResourceLocation getStillTexture() {
return stillTexture;
}

@Override
public ResourceLocation getFlowingTexture() {
return flowingTexture;
}

@Override
public ResourceLocation getOverlayTexture() {
return overlayTexture;
}

@Override
public int getTintColor() {
return tintColor;
}

@Override
public @NotNull Vector3f modifyFogColor(Camera camera, float partialTick,
ClientLevel level, int renderDistance,
float darkenWorldAmount, Vector3f fluidFogColor) {
return fogColor;
}

@Override
public void modifyFogRender(Camera camera, FogRenderer.FogMode mode, float renderDistance,
float partialTick, float nearDistance, float farDistance,
FogShape shape) {
RenderSystem.setShaderFogStart(1f);
RenderSystem.setShaderFogEnd(6f);
}
});
}

我们需要实例化IClientFluidTypeExtensions,然后里面还需要重写一些方法,将我们上面的属性传入

前几个方法我就不用解释了,显而易见的,我就说说最后一个modifyFogRender方法

  • setShaderFogStart:设置迷雾的起始距离
  • setShaderFogEnd:设置迷雾的结束距离

这个是决定我们在流体之中显示的迷雾渲染距离的,具体的可结合实际调整

注册 FluidType

接下来我们再创建一个ModFluidTypes类,用来注册我们的流体类型

1
2
3
4
5
6
7
8
public class ModFluidTypes {
public static final DeferredRegister<FluidType> FLUID_TYPES =
DeferredRegister.create(ForgeRegistries.Keys.FLUID_TYPES, TutorialMod.MOD_ID);

public static void register(IEventBus eventBus) {
FLUID_TYPES.register(eventBus);
}
}

同样的,把延迟注册器和初始化注册方法先写上,并在主类调用register方法

1
ModFluidTypes.register(modEventBus);

回过头来,我们注册流体类型

1
2
3
4
5
6
7
8
9
public static final ResourceLocation WATER_STILL_RL = ResourceLocation.parse("block/water_still");
public static final ResourceLocation WATER_FLOWING_RL = ResourceLocation.parse("block/water_flow");
public static final ResourceLocation WATER_OVERLAY_RL = ResourceLocation.parse("block/water_overlay");

public static final RegistryObject<FluidType> SEWAGE_FLUID_TYPE = FLUID_TYPES.register("sewage",
() -> new BaseFluidType(WATER_STILL_RL, WATER_FLOWING_RL, WATER_OVERLAY_RL,
0xFF2F4F4F,
new Vector3f(47f / 255f, 79f / 255f, 79f / 255f),
FluidType.Properties.create().density(15).viscosity(5)));

上面我们先写了三条ResourceLocation,它们分别对应静止状态、流动状态和覆盖非透明方块的纹理,当然为了方便起见,我们引用的是原版水的纹理

而后使用FLUID_TYPES.register来注册我们的流体类型,实例化我们自己写的BaseFluidType,把相应的参数传入

这里简单解释一下最后的FluidType.Properties,显然,这是流体的属性

  • density:密度
  • viscosity:粘度

这俩默认是1000,得设置一下,不然实体进去动弹不得(这让我想起了GTNH里的原油)

自定义流体

接下来我们来写自定义流体类

1
2
3
4
5
public abstract class SewageFluid extends ForgeFlowingFluid {
protected SewageFluid() {
super();
}
}

注意这是一个抽象类,并继承ForgeFlowingFluid,这里的构造方法我们先置空

Source

接下来我们新建一个内部类,继承SewageFluid这个类本身

1
2
3
4
5
6
7
8
9
10
11
12
public static class Source extends SewageFluid {

@Override
public boolean isSource(FluidState pState) {
return true;
}

@Override
public int getAmount(FluidState pState) {
return 8;
}
}

还要重写isSourcegetAmount方法

  • isSource:判断是否是流体源(像我们桶倒出来的,都是源)
  • getAmount:获取流体的高度,这个数值会经过计算,水默认的值是8

Flowing

接下来我们再新建一个内部类,同样继承SewageFluid这个类本身

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static class Flowing extends SewageFluid {

@Override
protected void createFluidStateDefinition(StateDefinition.Builder<Fluid, FluidState> pBuilder) {
super.createFluidStateDefinition(pBuilder);
pBuilder.add(LEVEL);
}

@Override
public boolean isSource(FluidState pState) {
return false;
}

@Override
public int getAmount(FluidState pState) {
return pState.getValue(LEVEL);
}
}

另外需要额外重写createFluidStateDefinition方法

这个方法是决定流体方块的方块状态的,这里的LEVEL是流体的液面高度

getAmount方法根据LEVEL来决定,因为这是流动的流体,液面高度会随着流动距离而降低

注册流体

接下来我们新建ModFluids类,在这里面注册流体

1
2
3
4
5
6
7
8
public class ModFluids {
public static final DeferredRegister<Fluid> FLUIDS =
DeferredRegister.create(ForgeRegistries.FLUIDS, TutorialMod.MOD_ID);

public static void register(IEventBus eventBus) {
FLUIDS.register(eventBus);
}
}

同样还是延迟注册器和初始化注册方法,在主类调用register方法

1
ModFluids.register(modEventBus);

然后我们来注册流体

1
2
3
4
5
6
7
8
9
10
11
12
public static final RegistryObject<FlowingFluid> STILL_SEWAGE = FLUIDS.register("sewage",
SewageFluid.Source::new);

public static final RegistryObject<FlowingFluid> FLOWING_SEWAGE = FLUIDS.register("flowing_sewage",
SewageFluid.Flowing::new);

public static final ForgeFlowingFluid.Properties SEWAGE_PROPERTIES = new ForgeFlowingFluid.Properties(
ModFluidTypes.SEWAGE_FLUID_TYPE, STILL_SEWAGE, FLOWING_SEWAGE)
.slopeFindDistance(2)
.levelDecreasePerBlock(1)
.block(ModBlocks.SEWAGE_BLOCK)
.bucket(ModItems.SEWAGE_BUCKET);

一共是三个部分,一个静止流体,一个流动流体,一个流体属性

在流体属性中:

  • slopeFindDistance:这个是流体方块寻找斜坡的间隔
  • levelDecreasePerBlock:这个是流体方块每流过一个方块,高度就会减少多少
  • block:这个是流体方块
  • bucket:这个是流体桶

当然后面两个东西我们还没写,等会来写

我们先回过头去把SewageFluid的构造函数补充完整

1
2
3
protected SewageFluid() {
super(ModFluids.SEWAGE_PROPERTIES);
}

流体方块类

接下来我们创建一个流体方块类,当然,如果你没有特殊的交互需求可以跳过这部分内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class SewageFluidBlock extends LiquidBlock {
public SewageFluidBlock(RegistryObject<FlowingFluid> fluid, Properties settings) {
super(fluid, settings);
}

@Override
public void entityInside(BlockState pState, Level pLevel, BlockPos pPos, Entity pEntity) {
if (!pLevel.isClientSide() && pEntity instanceof LivingEntity livingEntity) {
if (livingEntity.tickCount % 20 == 0) {
livingEntity.hurt(pLevel.damageSources().magic(), 2.0F);
}
}
}
}

这里我们重写了entityInside方法,这里我们给实体添加了伤害,每1秒执行一次

工业污水能造成伤害也很合理吧,当然这个是终末地过来的案例

注册方块

接下来我们来注册方块

1
2
3
4
public static final RegistryObject<LiquidBlock> SEWAGE_BLOCK = BLOCKS.register("sewage",
() -> new SewageFluidBlock(ModFluids.STILL_SEWAGE, BlockBehaviour.Properties.of()
.replaceable().noCollission().strength(100f).liquid()
.pushReaction(PushReaction.DESTROY).noLootTable()));

如果你省略了上面的自定义流体方块的编写,那么这里实例化可以直接使用LiquidBlock

注册流体桶物品

我们来注册流体桶物品

1
2
public static final RegistryObject<Item> SEWAGE_BUCKET = ITEMS.register("sewage_bucket",
() -> new BucketItem(ModFluids.STILL_SEWAGE, new Item.Properties().stacksTo(1)));

和一般的流体桶一样,我们可以将其最大堆叠数量设置为1

这里我们大体上就和水一样好了

然后ModFluids相关的内容记得添加

不要忘记将流体桶添加到物品组中

1
pOutput.accept(ModItems.SEWAGE_BUCKET.get());

客户端渲染设置

流体属于半透明类型的,所以它的渲染还需要在客户端类中进行注册的

在模组主类的ClientModEvents类的onClientSetup方法中加上以下语句

1
2
3
4
event.enqueueWork(() -> {
ItemBlockRenderTypes.setRenderLayer(ModFluids.FLOWING_SEWAGE.get(), RenderType.translucent());
ItemBlockRenderTypes.setRenderLayer(ModFluids.STILL_SEWAGE.get(), RenderType.translucent());
});

数据文件

语言文件

我们添加一下桶装流体的语言文件,方块也可以加一下

1
2
add(ModItems.SEWAGE_BUCKET.get(), "Sewage Bucket");
add(ModBlocks.SEWAGE_BLOCK.get(), "Sewage");

模型文件

流体桶物品的模型文件

1
basicItem(ModItems.SEWAGE_BUCKET.get());

方块的模型文件

1
simpleBlockWithoutBlockModel(ModBlocks.SEWAGE_BLOCK);

当然我们只需要它的方块状态文件即可

流体标签

另外,对于可以倒出来的流体而言,我们还得写一个流体的标签,不然它会失去流体应有的属性,如让玩家浮起来等

这里我们就不使用数据生成来写了,直接手动创建标签

我们在resource/data/minecraft/tags/fluids文件夹下,创建一个water.json文件

1
2
3
4
5
6
7
{
"replace": false,
"values": [
"tutorial_mod:sewage",
"tutorial_mod:flowing_sewage"
]
}

我们将replace设置为false,不然原版的水就被我们顶掉了

values中添加我们的流体和流动的流体

材质文件

我们还要将流体桶的材质文件放到对应的位置

方块模型

然后我们还要写一个流体方块的模型文件

1
2
3
4
5
{
"textures": {
"particle": "block/water_still"
}
}

这里就指定一下它的粒子即可

那么到这里为止,我们的流体就完成了,接下来我们就可以进行测试了

测试

跑完数据生成之后,我们就可以启动游戏进行测试了