本篇教程的视频

本篇教程的源代码

Github地址:TutorialMod-Fluid-1.21

介绍

在游戏中,流体也是一个很重要的元素,比如岩浆

在各大工业模组里面,也有各种各样不同的流体,比如石油等等

1.20的教程我没讲流体,所以在1.21的教程中,我们还是来讲讲自定义的流体如何添加

查看源代码

这里我们以游戏中的为例,我们来添加一个仿水的流体

不过在此之前我们先看看水桶怎么写的,毕竟我们的水可以通过水桶放置,也可以被空桶舀起

水桶

1
public static final Item WATER_BUCKET = register("water_bucket", new BucketItem(Fluids.WATER, new Item.Settings().recipeRemainder(BUCKET).maxCount(1)));

这里的水桶实例化的是BucketItem,这个类是定义的(其实你也可以再看一眼牛奶的,它实例化的是MilkBucketItem,因为牛奶不像水或者岩浆这样可以倒出来)

这里的Fluids.WATER是一个Fluid类型的字段,它是原版的流体

后面的就是水桶的设置,这里设置了最大堆叠数配方剩余物(空桶,用于使用后返回)

Fluids类

接下来我们看看Fluids这个类,这个类是定义了原版的流体

1
2
3
4
5
6
7
8
9
10
11
12
public class Fluids {
...
public static final FlowableFluid FLOWING_WATER = register("flowing_water", new WaterFluid.Flowing());
public static final FlowableFluid WATER = register("water", new WaterFluid.Still());
...

private static <T extends Fluid> T register(String id, T value) {
return Registry.register(Registries.FLUID, id, value);
}

...
}

这个类不长,我们看到这里的FLOWING_WATERWATER这两个字段,顾名思义,一个是流动的水,一个是静止的水

他们各自实例化的是WaterFluid.FlowingWaterFluid.Still,这两个类是WaterFluid的内部类

他们的类型都是FlowableFluid,也就是可以流动的流体

这里的register方法是用于注册流体的,看了前面那么多的注册方法,这个方法应该不难理解了吧

WaterFluid类

上面写到了WaterFluid的两个内部类,我们来看看这个两个类

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
public abstract class WaterFluid extends FlowableFluid {
...

public static class Flowing extends WaterFluid {
@Override
protected void appendProperties(StateManager.Builder<Fluid, FluidState> builder) {
super.appendProperties(builder);
builder.add(LEVEL);
}

@Override
public int getLevel(FluidState state) {
return (Integer)state.get(LEVEL);
}

@Override
public boolean isStill(FluidState state) {
return false;
}
}

public static class Still extends WaterFluid {
@Override
public int getLevel(FluidState state) {
return 8;
}

@Override
public boolean isStill(FluidState state) {
return true;
}
}
}

这个类是一个抽象类,继承了FlowableFluid,这个类是用于定义流体的

它的两个内部类FlowingStill分别是流动的水静止的水

这两个类都重写了getLevelisStill方法,getLevel是用于获取流体的高度,isStill是用于判断流体是否静止

Flowing类还重写了appendProperties方法,这个方法是用于添加流体的属性,也就是把LEVEL这个属性加入到流体的状态中(因为流动状态的水会根据流程长度决定流体的高度,而静止的就直接定义即可)

当然这里其他的方法我们可以先不看,待会我们会进行解释

除了渲染和其他数据文件之外,源代码大体上就是这样

注册流体

创建CustomFluid抽象类

这个也是仿照Wiki上的写法,我们创建一个CustomFluid抽象类,用于定义我们的流体

如果说你的流体拥有差不多的属性,完全可以写一个抽象类,把一些共有的属性和方法写在这里,这样就不用每次写一个新的流体还得写一堆辅助的方法

然后你在写新的流体的时候,只需要继承这个抽象类,有必要时再重写一些方法就行了

1
2
3
public abstract class CustomFluid extends FlowableFluid {

}

我们的抽象类继承了FlowableFluid,就和水一样

而后,我们开始在这个类中重写一些方法

matchesType

1
2
3
4
@Override
public boolean matchesType(Fluid fluid) {
return fluid == getFlowing() || fluid == getStill();
}

这个方法是用于判断流体是否匹配,这里我们判断的是流体是否是流动的水或者静止的水

isInfinite

1
2
3
4
@Override
protected boolean isInfinite(World world) {
return false;
}

这个方法是用于判断流体是否是无限的,这里我们返回false,原版的水是无限的,但岩浆不是

实际情况是,你也可以通过gamerule来设置流体是否无限

beforeBreakingBlock

1
2
3
4
5
@Override
protected void beforeBreakingBlock(WorldAccess world, BlockPos pos, BlockState state) {
BlockEntity blockEntity = state.hasBlockEntity() ? world.getBlockEntity(pos) : null;
Block.dropStacks(state, world, pos, blockEntity);
}

这个方法是用于破坏方块之前的操作,这里我们是用于破坏方块时掉落物品,比如草被流水破坏会掉落种子

canBeReplacedWith

1
2
3
4
@Override
protected boolean canBeReplacedWith(FluidState state, BlockView world, BlockPos pos, Fluid fluid, Direction direction) {
return false;
}

这个方法是用于判断流体是否可以被替换,这里我们返回false,原版的岩浆是可以被替换为黑曜石的,具体可见岩浆的这个方法

1
2
3
4
@Override
protected int getFlowSpeed(WorldView world) {
return 4;
}

这个方法是用于获取流体的流动速度,原版的水是4,岩浆在特定的生物群系(极热生物群系,应该只有下界吧)里面是2,默认为4

getLevelDecreasePerBlock

1
2
3
4
@Override
protected int getLevelDecreasePerBlock(WorldView world) {
return 1;
}

这个方法是用于获取流体每个方块的流动速度,原版的水是1,岩浆在特定的生物群系里面是2,默认为1

getTickRate

1
2
3
4
@Override
public int getTickRate(WorldView world) {
return 5;
}

这个方法是用于获取流体的更新速度,原版的水是5,岩浆在特定的生物群系里面是30,默认为10(数值越大更新越慢)

getBlastResistance

1
2
3
4
@Override
protected float getBlastResistance() {
return 100.0F;
}

这个方法是用于获取流体的爆炸抗性,原版的水和岩浆都是100.0F

总体

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
40
41
42
public abstract class CustomFluid extends FlowableFluid {
@Override
public boolean matchesType(Fluid fluid) {
return fluid == getFlowing() || fluid == getStill();
}

@Override
protected boolean isInfinite(World world) {
return false;
}

@Override
protected void beforeBreakingBlock(WorldAccess world, BlockPos pos, BlockState state) {
BlockEntity blockEntity = state.hasBlockEntity() ? world.getBlockEntity(pos) : null;
Block.dropStacks(state, world, pos, blockEntity);
}

@Override
protected boolean canBeReplacedWith(FluidState state, BlockView world, BlockPos pos, Fluid fluid, Direction direction) {
return false;
}

@Override
protected int getFlowSpeed(WorldView world) {
return 4;
}

@Override
protected int getLevelDecreasePerBlock(WorldView world) {
return 1;
}

@Override
public int getTickRate(WorldView world) {
return 5;
}

@Override
protected float getBlastResistance() {
return 100.0F;
}
}

创建OilFluid抽象类

这个类是用于定义我们的流体的,我们继承CustomFluid抽象类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public abstract class OilFluid extends CustomFluid {
@Override
public Fluid getStill() {
return null;
}

@Override
public Fluid getFlowing() {
return null;
}

@Override
public Item getBucketItem() {
return null;
}

@Override
protected BlockState toBlockState(FluidState state) {
return null;
}
}

当然我们这里还得重写四个方法(其实第四个是为内部类服务的),不过目前还没有完成注册,所以都先返回null

接下来我们写里面的两个类,一个是Flowing,一个是Still

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
public static class Flowing extends OilFluid {
@Override
protected void appendProperties(StateManager.Builder<Fluid, FluidState> builder) {
super.appendProperties(builder);
builder.add(LEVEL);
}

@Override
public boolean isStill(FluidState state) {
return false;
}

@Override
public int getLevel(FluidState state) {
return state.get(LEVEL);
}
}

public static class Still extends OilFluid {

@Override
public boolean isStill(FluidState state) {
return true;
}

@Override
public int getLevel(FluidState state) {
return 8;
}
}

这里的内部类其实也和原版是差不多的,只是我们这里的继承的流体是OilFluid

创建ModFluids类

这个类是用于注册我们的流体的,和原版的Fluids类一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ModFluids {

private static <T extends Fluid> T register(String id, T value) {
return Registry.register(Registries.FLUID, Identifier.of(TutorialMod.MOD_ID, id), value);
}

static {
for (Fluid fluid : Registries.FLUID) {
for (FluidState fluidState : fluid.getStateManager().getStates()) {
Fluid.STATE_IDS.add(fluidState);
}
}
}
public static void registerModFluids() {

}
}

这里我们写三块,一个是注册方法,一块静态代码块(用于获取流体的状态),一个初始化注册方法

这个注册方法和代码块是从原版的Fluids类中搬过来的,当然注册方法记得改命名空间

随后我们来注册流体

1
2
public static final FlowableFluid FLOWING_OIL = register("flowing_oil", new OilFluid.Flowing());
public static final FlowableFluid OIL = register("oil", new OilFluid.Still());

这里我们注册了了OIL分别是静止的和流动的,它们各自实例化的是OilFluid中对应的内部类

注册流体方块

在我们的OilFluid类中,还有一个toBlockState方法,这个方法是用于设置不同LEVEL的流体方块的方块状态

但在此之前,我们得有个流体方块

1
2
public static final Block OIL = Registry.register(Registries.BLOCK, Identifier.of(TutorialMod.MOD_ID, "oil"),
new FluidBlock(ModFluids.OIL, AbstractBlock.Settings.copy(Blocks.WATER)){});

这里我们注册了一个OIL,它实例化的是FluidBlock,这个类是用于定义流体方块的

第一个参数是我们的流体,第二个参数是方块的设置,这里我们用的是WATER的设置,你也可以自定义

注册流体桶

那么我们的流体已经完成了,接下来我们来创建对应的桶物品

1
2
public static final Item OIL_BUCKET = registerItems("oil_bucket",
new BucketItem(ModFluids.OIL, new Item.Settings().recipeRemainder(Items.BUCKET).maxCount(1)));

这里我们注册了一个OIL_BUCKET,它实例化的是BucketItem,后面的设置和原版的水桶一样,也给它设置了最大堆叠数配方剩余物

加入物品栏

不要忘了把这个桶加入到物品栏中

1
entries.add(ModItems.OIL_BUCKET);

完成方法的重写

那么现在,我们上面返回null的四个方法就可以填写了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override
public Fluid getStill() {
return ModFluids.OIL;
}

@Override
public Fluid getFlowing() {
return ModFluids.FLOWING_OIL;
}

@Override
public Item getBucketItem() {
return ModItems.OIL_BUCKET;
}

@Override
protected BlockState toBlockState(FluidState state) {
return ModBlocks.OIL.getDefaultState().with(FluidBlock.LEVEL, WaterFluid.getBlockStateLevel(state));
}

仿照着原版的WaterFluid,我们填写了这四个方法,这样我们的流体才算彻底写好了

注册流体渲染和流体渲染层

那么我们的水是半透明的对吧?岩浆在某种意义上其实也是半透明的(也仅限于玩家在里面的时候,至少你还能看得见边缘,接近方块的时候)

不过我们这里不管岩浆,我们就用水的渲染

1
BlockRenderLayerMap.INSTANCE.putFluids(RenderLayer.getTranslucent(), ModFluids.OIL, ModFluids.FLOWING_OIL);

我们在TutorialModClientonInitializeClient方法中注册我们的流体渲染层

这里用的还是FabricAPI,我们把OILFLOWING_OIL这两个流体放到RenderLayer.getTranslucent()这个渲染层中,这个也就是半透明的

当然你也可以通过Mixin去添加

1
2
3
4
5
6
FluidRenderHandlerRegistry.INSTANCE.register(ModFluids.OIL, ModFluids.FLOWING_OIL,
new SimpleFluidRenderHandler(
Identifier.of("minecraft:block/water_still"),
Identifier.of("minecraft:block/water_flow"),
0x42413b
));

另外,借助FabricAPI,我们还得注册我们的流体渲染,因为流体和一般的方块不一样,它们的渲染是独立开来的

这里我们注册了OILFLOWING_OIL这两个流体的渲染,我们用的是SimpleFluidRenderHandler,这个是一个简单的流体渲染处理器

面的两个Identifier是用于指定流体的静止和流动的贴图,这里我们就借用原版贴图即可

最后一个参数是用于指定流体的颜色,这个颜色是16进制的,可以根据你的流体的颜色来设置

数据文件

语言文件

1
translationBuilder.add(ModItems.OIL_BUCKET, "Oil Bucket");

这里就写桶物品就好了

模型文件

1
itemModelGenerator.register(ModItems.OIL_BUCKET, Models.GENERATED);

模型文件用的也还是GENERATED,也就是一般的物品模型

water.json

这个文件得写,而且不能写错

路径是resources/data/minecraft/tags/fluids/water.json

没写的话你的流体就不能正常显示了,而写错了的话就会导致原版的水也不正常了(因为我们写的是仿水的流体,除非你完完全全从头到尾重新定义了一个流体)

1
2
3
4
5
6
7
{
"replace": false,
"values": [
"tutorialmod:oil",
"tutorialmod:flowing_oil"
]
}

replace得写falsevalues里面写我们的流体(命名空间+注册名)

贴图文件

最后也别忘了贴图文件,这个就不用我多说了吧

在此之后就可以进入游戏进行测试了,你可以用对应的桶来放置流体,也可以用空桶来舀起流体

它可以提供浮力,也会冲着你走(流动状态下),也可以破坏草这类的方块