本篇教程的视频

(待发布)

本篇教程的源代码

GitHub地址:TutorialMod-Fluid-1.20.1

本篇教程目标

  • 理解原版流体及其注册、方块注册和流体桶的注册
  • 学会编写流体和注册相关内容

查看源代码

和前面一样,我们来看看流体的源代码

这里我们先看看流体桶的注册

原版是有3个流体桶,分别是水桶、牛奶桶和岩浆桶,不过牛奶桶是不能倒出来的,而水和岩浆可以

所以这里我们重点来看水和岩浆这两个流体

桶装流体物品

我们到Items类中找到WATER_BUCKET,也就是水桶

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

可以看到,它实例化的是BucketItem,第一个参数是对应的流体,后面是物品设置

物品设置中,recipeRemainder配方剩余物品,也就是空桶会在使用后返回,后面的最大堆叠数量为1

岩浆桶也同样类似,接下去我们看看Fluids类

Fluids类

1
2
public static final FlowableFluid FLOWING_WATER = register("flowing_water", new WaterFluid.Flowing());
public static final FlowableFluid WATER = register("water", new WaterFluid.Still());

这个是流体的注册类,水和岩浆一样都是有静止和流动状态的,所以它们分别有两个注册语句

register是它们的注册方法

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

第一个参数是流体名称,第二个参数是一个泛型,即流体的实例

注册语句与我们之前见到的差不多,它的注册表项调用的是FLUID

另外,在这个类中还有一个静态代码块

1
2
3
4
5
6
7
static {
for (Fluid fluid : Registries.FLUID) {
for (FluidState fluidState : fluid.getStateManager().getStates()) {
Fluid.STATE_IDS.add(fluidState);
}
}
}

这个静态块是将注册到FLUID注册表中的流体状态加入到STATE_IDS集合中,这是给游戏实际运行时使用的

根据上面的注册语句,我们还能看到水和岩浆有自己对应的流体类,现在我们就来看看它们

WaterFluid类

这里我们主要来看看水的,岩浆也是类似的,但是岩浆的一些逻辑是要比水复杂的,感兴趣的同学可以自己研究

1
public abstract class WaterFluid extends FlowableFluid

它是继承了FlowableFluid,这个类里面有流体流动的一些方法,而且这个类也很长,这里我们就不研究这个类了

1
2
3
4
@Override
public Fluid getFlowing() {
return Fluids.FLOWING_WATER;
}

getFlowing方法是获取流动状态的流体,返回注册类中的FLOWING_WATER

1
2
3
4
@Override
public Fluid getStill() {
return Fluids.WATER;
}

getStill方法是获取静止状态的流体,返回注册类中的WATER

1
2
3
4
@Override
public Item getBucketItem() {
return Items.WATER_BUCKET;
}

getBucketItem方法是获取流体桶的物品,返回Items类中的WATER_BUCKET

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
@Override
public void randomDisplayTick(World world, BlockPos pos, FluidState state, Random random) {
if (!state.isStill() && !(Boolean)state.get(FALLING)) {
if (random.nextInt(64) == 0) {
world.playSound(
(double)pos.getX() + 0.5,
(double)pos.getY() + 0.5,
(double)pos.getZ() + 0.5,
SoundEvents.BLOCK_WATER_AMBIENT,
SoundCategory.BLOCKS,
random.nextFloat() * 0.25F + 0.75F,
random.nextFloat() + 0.5F,
false
);
}
} else if (random.nextInt(10) == 0) {
world.addParticle(
ParticleTypes.UNDERWATER,
(double)pos.getX() + random.nextDouble(),
(double)pos.getY() + random.nextDouble(),
(double)pos.getZ() + random.nextDouble(),
0.0,
0.0,
0.0
);
}
}

randomDisplayTick方法是流体随机显示,也就是流体流动时的一些效果,比如粒子和声音

水的话主要是不同状态下发出的声音不同,而岩浆则会随机冒出一些粒子,或者在周围生成火焰亦或是点燃方块

1
2
3
4
5
@Nullable
@Override
public ParticleEffect getParticle() {
return ParticleTypes.DRIPPING_WATER;
}

getParticle方法是获取流体粒子,返回ParticleTypes类中的DRIPPING_WATER

1
2
3
4
@Override
protected boolean isInfinite(World world) {
return world.getGameRules().getBoolean(GameRules.WATER_SOURCE_CONVERSION);
}

isInfinite方法是判断流体是否是无限的,水默认是无限的,但也是由世界规则中的WATER_SOURCE_CONVERSION来决定

岩浆也是一样的,所以岩浆也是可以通过指令将其设置为无限岩浆

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);
}

beforeBreakingBlock方法是流体在破坏方块之前调用的方法

水在破坏方块之前会将这个方块的掉落物生成后再破坏,比如用水来除草,可能会生成一堆小麦种子,但岩浆不会

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

getFlowSpeed方法是获取流体流动速度,这里设置为4

岩浆的流动速度还取决于当前维度是否是极暖维度

极暖维度即为原版游戏中的下界,在下界的岩浆会流动得比主世界的要快

1
2
3
4
@Override
public BlockState toBlockState(FluidState state) {
return Blocks.WATER.getDefaultState().with(FluidBlock.LEVEL, Integer.valueOf(getBlockStateLevel(state)));
}

toBlockState方法是获取流体方块状态

流体本身并不是方块,但在世界中是以方块的形式呈现的,所以还是要将它转换成相应的方块状态,让它呈现出来

FluidBlock.LEVEL是流体方块的高度,原版将其定义0至15,一共16个高度

值得注意的是,流体本身还有一个LEVEL属性,它的范围是1至8,流体本身的高度与流体方块的高度是在FlowableFluid的getBlockStateLevel方法中转换的

1
2
3
4
@Override
public boolean matchesType(Fluid fluid) {
return fluid == Fluids.WATER || fluid == Fluids.FLOWING_WATER;
}

matchesType方法是判断流体类型是否匹配,如果传入的流体是水或者流动的水,那么返回true

但如果是两个不一样的流体,它们就不会相融

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

getLevelDecreasePerBlock方法是流体每流过一个方块降低的高度,流体降低到最低之后它就不流动了

水每流过一个方块降低1的高度

在极暖维度中,岩浆每流过一个方块降低1的高度,而在其他维度中,则降低2的高度

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

getTickRate方法是流体的更新速度

水每5 tick更新一次

在极暖维度中,岩浆每10 tick更新一次,而在其他维度中,则每30 tick更新一次

1
2
3
4
@Override
public boolean canBeReplacedWith(FluidState state, BlockView world, BlockPos pos, Fluid fluid, Direction direction) {
return direction == Direction.DOWN && !fluid.isIn(FluidTags.WATER);
}

canBeReplacedWith方法是判断流体是否可以被替换,水可以被其他流体替换

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

getBlastResistance方法是获取流体爆炸抗性,水和岩浆的爆炸抗性都是100

所以原版中的那些爆炸是无法破坏水或者岩浆的

1
2
3
4
@Override
public Optional<SoundEvent> getBucketFillSound() {
return Optional.of(SoundEvents.ITEM_BUCKET_FILL);
}

getBucketFillSound方法是获取流体桶装水的声音,返回SoundEvents类中的ITEM_BUCKET_FILL

另外,下面还有两个嵌套类,它们是在注册的的时候用的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;
}
}

Flowing类是流动的水,它继承了WaterFluid类

appendProperties方法是添加方块状态属性,这里添加了LEVEL属性

getLevel方法是获取流体高度,返回LEVEL属性

isStill方法是判断流体是否是静止的,返回false

1
2
3
4
5
6
7
8
9
10
11
public static class Still extends WaterFluid {
@Override
public int getLevel(FluidState state) {
return 8;
}

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

Still类是静止的水,它继承了WaterFluid类

getLevel方法是获取流体高度,返回8

isStill方法是判断流体是否是静止的,返回true

流体方块

另外,流体方块也是要进行注册的

我们到Blocks中找到WATER

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static final Block WATER = register(
"water",
new FluidBlock(
Fluids.WATER,
AbstractBlock.Settings.create()
.mapColor(MapColor.WATER_BLUE)
.replaceable()
.noCollision()
.strength(100.0F)
.pistonBehavior(PistonBehavior.DESTROY)
.dropsNothing()
.liquid()
.sounds(BlockSoundGroup.INTENTIONALLY_EMPTY)
)
);

它实例化的是FluidBlock,后面的方块设置我们也见得多了

replaceable方法是可以被其他方块替换

noCollision方法是流体方块没有碰撞箱,所以流体方块是可以被穿过

dropsNothing方法是流体方块没有掉落物

liquid方法指定这个方块是流体方块

那么到这里为止,一个可以用桶装的,也可以被倒出来的流体就注册完了

后面还有一些标签和客户端渲染是设置,我们具体在后面再说

接下来就是我们自己来注册流体

注册流体

这里我们先写一个抽象流体类,这个是按照Fabric Wiki上的写法

如果你有一系列属性类似的流体,可以将它们共有的属性抽象出来,然后让它们继承这个抽象类,这样就可以避免重复写代码了

ModFluid

这里我们创建ModFluid这个抽象类,它继承FlowableFluid

1
2
3
public abstract class ModFluid extends FlowableFluid {

}

然后我们来重写一些方法,重写的方法我们前面都解释过了,这里就简单说明一下

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

这里重写matchesType方法,判断流体是否匹配

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

重写isInfinite方法,将我们的流体设置为有限

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);
}

重写beforeBreakingBlock方法,仿照水的写法,在流体方块被破坏之前,将流体方块中的物品掉落出来

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

重写canBeReplacedWith方法,将流体方块设置为不可被替换

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

重写getFlowSpeed方法,设置流体方块流动速度为4

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

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

重写getLevelDecreasePerBlock方法,设置流体方块每流过一个方块降低1的高度

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

重写getTickRate方法,设置流体方块更新速度为5

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

重写getBlastResistance方法,设置流体方块爆炸抗性为100

OilFluid

抽象类写好了,接下来就来写具体的实例

这里我们创建OilFluid这个类,它继承ModFluid,不过同样的,它也是一个抽象类

1
2
3
public abstract class OilFluid extends ModFluid {

}

这里我们先写两个嵌套类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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);
}
}

Flowing类继承OilFluid类

然后我们就重写其中的方法

appendProperties方法中添加LEVEL属性

isStill方法返回false

getLevel方法返回LEVEL属性

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

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

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

Still类继承OilFluid类,然后重写其中的方法

isStill方法返回true

getLevel方法返回8

ModFluids

接下来就是来注册流体了,这里我们创建ModFluids

1
2
3
public class ModFluids {

}

然后我们将原版的register方法搬过来改一下

1
2
3
private static <T extends Fluid> T register(String name, T fluid) {
return (T) Registry.register(Registries.FLUID, new Identifier(TutorialMod.MOD_ID, name), fluid);
}

我们将模组的命名空间加上

另外还有一个静态代码块,不过这个东西写不写似乎没什么关系

1
2
3
4
5
6
7
static {
for (Fluid fluid : Registries.FLUID) {
for (FluidState fluidState : fluid.getStateManager().getStates()) {
Fluid.STATE_IDS.add(fluidState);
}
}
}

然后我们来注册流体,写法与原版的类似

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

一个是流动的,一个是静止的,它们实例化相对应的类

另外还有一个用来初始化的方法

1
2
3
public static void registerModFluids() {

}

我们要在模组主类中调用这个方法

1
ModFluids.registerModFluids();

物品、方块注册

物品注册

接下来我们就可以进行桶装流体的注册

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

这里我们创建一个OIL_BUCKET,实例化BucketItem,然后我们传入我们的流体和物品设置

加入物品栏

然后我们就可以将桶装流体加入物品栏

1
entries.add(ModItems.OIL_BUCKET);

注册流体方块

随后我们就来注册流体方块

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

这里我们要使用Registry.register来注册,因为它没有对应的方块物品

实例化FluidBlock,传入我们的流体,方块设置可以直接用原版水的

OilFluid另外的重写方法

接下来我们回到OilFluid类,重写另外四个方法

1
2
3
4
@Override
public Fluid getFlowing() {
return ModFluids.OIL_FLOWING;
}

重写getFlowing方法,返回注册的流动的流体

1
2
3
4
@Override
public Fluid getStill() {
return ModFluids.OIL;
}

重写getStill方法,返回注册的静止的流体

1
2
3
4
@Override
public Item getBucketItem() {
return ModItems.OIL_BUCKET;
}

重写getBucketItem方法,返回注册的桶装流体

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

重写toBlockState方法,将流体方块设置为我们的流体方块

客户端渲染设置

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

同样我们还要为流体指定颜色

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

这里我们使用FluidRenderHandlerRegistry来注册,这个是Fabric的API

传入我们的流体和流动的流体,然后实例化SimpleFluidRenderHandler

传入静止的流体的纹理,流动的流体的纹理,当然这两个纹理都是原版水的纹理

最后一个是16进制颜色,这里我们使用0x42413b,你可以按照你的想法进行设置

还有一个是渲染层的设置

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

我们将其设置为半透明,然后传入我们的流体和流动的流体

数据文件

语言文件

我们添加一下桶装流体的语言文件

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

模型文件

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

物品模型还是和一般的物品一样

流体标签

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

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

我们在resource文件夹下创建一个data文件夹,然后创建一个minecraft文件夹

注意是minecraft,不是我们模组的命名空间

再创建一个tags文件夹,然后创建一个fluids文件夹,最后创建一个water.json文件

1
2
3
4
5
6
7
{
"replace": false,
"values": [
"tutorial-mod:oil",
"tutorial-mod:oil_flowing"
]
}

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

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

材质文件

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

方块状态

这里我们还要写一个方块状态文件,是流体方块的

1
2
3
4
5
6
7
{
"variants": {
"": {
"model": "tutorial-mod:block/oil"
}
}
}

这个是那种最简单的方块状态文件

方块模型

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

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

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

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

测试

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