Creating Your First Fluid
PREREQUISITES
You must first understand how to create a block and how to create an item.
This example will cover the creation of an acid fluid that hurts, weakens, and blinds entities that stand inside of it. To do this, we'll need two fluid instances for the source and fluid states, a liquid block, a bucket item, and a fluid tag.
Creating the Fluid Class
We'll start by creating an abstract class, in this case called AcidFluid, that extends the baseline FlowingFluid class. Then, we'll override any the methods that should be the same for both the source and the flowing fluid.
Pay special attention to the following methods:
animateTickis used for displaying particles and sound. The behaviour shown below is based on water, which plays sound when it flows and has underwater bubbling particles.entityInsideis used to handle what should happen when an entity touches the fluid. We'll base it off water and extinguish any fire on entities, but also make it hurt, weaken, and blind entities inside - it is acid after all.canBeReplacedWithhandles some flowing logic - note thatModFluidTags.ACIDis not yet defined, we'll handle that at the end.
Putting this all together, we end up with the following class:
java
public abstract class AcidFluid extends FlowingFluid {
@Override
public void animateTick(Level world, BlockPos pos, FluidState state, RandomSource random) {
if (!state.isSource() && !(Boolean) state.getValue(FALLING)) {
if (random.nextInt(64) == 0) {
world.playLocalSound(
pos.getX() + 0.5,
pos.getY() + 0.5,
pos.getZ() + 0.5,
SoundEvents.BUBBLE_COLUMN_WHIRLPOOL_AMBIENT, // Bubbling poison/swamp sound
SoundSource.AMBIENT,
random.nextFloat() * 0.25F + 0.75F,
random.nextFloat() + 0.5F,
false);
}
} else if (random.nextInt(10) == 0) {
world.addParticle(
ParticleTypes.UNDERWATER, pos.getX() + random.nextDouble(), pos.getY() + random.nextDouble(),
pos.getZ() + random.nextDouble(), 0.0, 0.0, 0.0);
}
}
@Nullable
@Override
public ParticleOptions getDripParticle() {
return ParticleTypes.DRIPPING_WATER;
}
@Override
protected boolean canConvertToSource(ServerLevel world) {
return world.getGameRules().get(GameRules.WATER_SOURCE_CONVERSION);
}
@Override
protected void beforeDestroyingBlock(LevelAccessor world, BlockPos pos, BlockState state) {
BlockEntity blockEntity = state.hasBlockEntity() ? world.getBlockEntity(pos) : null;
Block.dropResources(state, world, pos, blockEntity);
}
@Override
protected void entityInside(Level world, BlockPos pos, Entity entity, InsideBlockEffectApplier handler) {
handler.apply(InsideBlockEffectType.EXTINGUISH);
if (!(world instanceof ServerLevel serverLevel) || !(entity instanceof LivingEntity livingEntity)) return;
if (world.getGameTime() % 20 == 0) {
// Hurt and weaken entities that step inside.
livingEntity.hurtServer(serverLevel, world.damageSources().magic(), 2.0F); // 1 heart/sec
livingEntity.addEffect(new MobEffectInstance(MobEffects.WEAKNESS, 300, -3));
livingEntity.addEffect(new MobEffectInstance(MobEffects.BLINDNESS, 300, -3));
}
}
@Override
protected int getSlopeFindDistance(LevelReader world) {
return 4;
}
@Override
public int getDropOff(LevelReader world) {
return 1;
}
@Override
public int getTickDelay(LevelReader world) {
return 5;
}
@Override
public boolean canBeReplacedWith(FluidState state, BlockGetter world, BlockPos pos, Fluid fluid,
Direction direction) {
return direction == Direction.DOWN && !fluid.is(ModFluidTags.ACID);
}
@Override
protected float getExplosionResistance() {
return 100.0F;
}
@Override
public Optional<SoundEvent> getPickupSound() {
return Optional.of(SoundEvents.BUCKET_FILL);
}
}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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
Inside AcidFluid, we'll create two subclasses for the Source and Flowing fluids.
java
public static class Flowing extends AcidFluid {
@Override
protected void createFluidStateDefinition(StateDefinition.Builder<Fluid, FluidState> builder) {
super.createFluidStateDefinition(builder);
builder.add(LEVEL);
}
@Override
public int getAmount(FluidState state) {
return state.getValue(LEVEL);
}
@Override
public boolean isSource(FluidState state) {
return false;
}
}
public static class Source extends AcidFluid {
@Override
public int getAmount(FluidState state) {
return 8;
}
@Override
public boolean isSource(FluidState state) {
return true;
}
}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
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
Registering Fluids
Next, we'll create a class to register all the fluid instances. We'll call it ModFluids.
java
public class ModFluids {
public static final FlowingFluid ACID_FLOWING = register("flowing_acid", new AcidFluid.Flowing());
public static final FlowingFluid ACID_STILL = register("acid", new AcidFluid.Source());
private static FlowingFluid register(String name, FlowingFluid fluid) {
return Registry.register(BuiltInRegistries.FLUID, Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, name), fluid);
}
public static void initialize() {
}
}1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
Just like with blocks, you need to ensure that the class is loaded so that all static fields containing your fluid instances are initialized. You can do this by creating a dummy initialize method, which can be called in your mod's initializer to trigger the static initialization.
Now, go back to the AcidFluid class, and add these methods to associate the registered fluid instances with this fluid:
java
@Override
public Fluid getFlowing() {
return ModFluids.ACID_FLOWING;
}
@Override
public Fluid getSource() {
return ModFluids.ACID_STILL;
}
@Override
public boolean isSame(Fluid fluid) {
return fluid == ModFluids.ACID_STILL || fluid == ModFluids.ACID_FLOWING;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
So far, we've registered the fluid's source state and its flowing state. Next, we'll need to register a bucket and a LiquidBlock for it.
Registering Fluid Blocks
Let's now add a liquid block for our fluid. This is needed by some commands like setblock, so your fluid can exist in the world. If you haven't done so yet, you should take a look at how to create your first block.
Open your ModBlocks class and register this following LiquidBlock:
java
public static final Block ACID = register(
"acid",
(props) -> new LiquidBlock(ModFluids.ACID_STILL, props),
BlockBehaviour.Properties.ofFullCopy(Blocks.WATER),
false
);1
2
3
4
5
6
2
3
4
5
6
Then, override this method in AcidFluid to associate your block with the fluid:
java
@Override
protected BlockState createLegacyBlock(FluidState state) {
return ModBlocks.ACID.defaultBlockState().setValue(LiquidBlock.LEVEL, getLegacyLevel(state));
}1
2
3
4
2
3
4
Registering Buckets
Fluids in Minecraft usually go in buckets, so let's see how we can add an item for a Bucket of Acid. If you haven't done so yet, you should take a look at how to create your first item.
Open your ModItems class and register this following BucketItem:
java
public static final Item ACID_BUCKET = register(
"acid_bucket",
props -> new BucketItem(ModFluids.ACID_STILL, props),
new Item.Properties()
.craftRemainder(Items.BUCKET)
.stacksTo(1)
);1
2
3
4
5
6
7
2
3
4
5
6
7
Then, override this method in AcidFluid to associate your bucket with the fluid:
java
@Override
public Item getBucket() {
return ModItems.ACID_BUCKET;
}1
2
3
4
5
2
3
4
5
Don't forget that items require a translation, texture, model, and client item with the name acid_bucket in order to render correctly. An example texture is provided below.
It's also recommended to add your mod's bucket to the ConventionalItemTags.BUCKET item tag so that other mods can handle it appropriately, either manually or through data generation.
Tagging Your Fluids
INFO
Users of data generation may wish to register tags via FabricTagProvider.FluidTagProvider, rather than writing them by hand.
Because a fluid is considered two separate blocks in its flowing and still states, a tag is often used to check for both states together. We'll create a fluid tag in data/example-mod/tags/fluid/acid.json:
json
{
"values": [
"example-mod:acid",
"example-mod:flowing_acid"
]
}1
2
3
4
5
6
2
3
4
5
6
TIP
Minecraft also provider other tags to control the behavior of fluids:
- If you need your mod's fluid to behave like water (water fog, absorbed by sponges, swimmable, slows entities...), considering adding it to the
minecraft:waterfluid tag. - If you need it to behave like lava (lava fog, swimmable by Striders/Ghasts, slows entities...), consider adding it to the
minecraft:lavafluid tag. - If you only need some of those things, you may wish to use mixins to finely control the behavior.
For this demo, we'll also add the acid fluid tag to the water fluid tag, data/minecraft/tags/fluid/water.json.
json
{
"values": [
"#example-mod:acid"
]
}1
2
3
4
5
2
3
4
5
Transparency and Textures
To texture your fluid, you should use Fabric API's FluidRenderHandlerRegistry.
TIP
For simplicity, this demo uses the water texture Identifiers provided by Fabric API, but you can replace those with an Identifier that points to your own texture in the format minecraft:block/water_still.
Add the following lines to your ClientModInitializer to create a SimpleFluidRenderHandler, that takes in two Identifiers for the textures—one for the still source and one for the flowing fluid—and an integer for the color to tint it with.
java
FluidRenderHandlerRegistry.INSTANCE.register(
ModFluids.ACID_STILL,
ModFluids.ACID_FLOWING,
new SimpleFluidRenderHandler(
// Source texture
SimpleFluidRenderHandler.WATER_STILL,
// Flowing texture
SimpleFluidRenderHandler.WATER_FLOWING,
0x075800
)
);1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
We'll also use the BlockRenderLayerMap to set the ChunkSectionLayer to transparent, so you can see through the fluid. For more information, see the docs on Transparency and Tinting.
java
BlockRenderLayerMap.putBlock(ModBlocks.ACID, ChunkSectionLayer.TRANSLUCENT);1
At this point, we have all we need to see the Acid in-game! You can use setblock or the Acid Bucket item to place acid in the world.




















