User Tools

Site Tools


tutorial:fluids

Creating a fluid

Overview

Here we'll cover creation of a custom fluid. If you plan to create several fluids, it is recommended to make an abstract basic fluid class where you'll set necessary defaults that will be shared in its subclasses. We'll also make it generate in the world like lakes.

Making an abstract fluid

Vanilla fluids extend net.minecraft.world.level.material.FlowingFluid, and so shall we.

  1. public abstract class TutorialFluid extends FlowingFluid {
  2. /**
  3. * @return whether the given fluid an instance of this fluid
  4. */
  5. @Override
  6. public boolean isSame(Fluid fluid) {
  7. return fluid == getSource() || fluid == getFlowing();
  8. }
  9.  
  10. /**
  11. * @return whether the fluid is infinite (which means can be infinitely created like water). In vanilla, it depends on the game rule.
  12. */
  13. @Override
  14. protected boolean canConvertToSource() {
  15. return false;
  16. }
  17.  
  18. /**
  19. * Perform actions when the fluid flows into a replaceable block. Water drops
  20. * the block's loot table. Lava plays the "block.lava.extinguish" sound.
  21. */
  22. @Override
  23. protected void beforeDestroyingBlock(LevelAccessor level, BlockPos pos, BlockState state) {
  24. final BlockEntity entity = state.hasBlockEntity() ? level.getBlockEntity(pos) : null;
  25. Block.dropResources(state, level, pos, entity);
  26. }
  27.  
  28. /**
  29. * Lava returns true if it's FluidState is above a certain height and the
  30. * Fluid is Water.
  31. *
  32. * @return whether the given Fluid can flow into this FluidState
  33. */
  34. @Override
  35. protected boolean canBeReplacedWith(FluidState state, BlockGetter level, BlockPos pos, Fluid fluid, Direction direction) {
  36. return false;
  37. }
  38.  
  39. /**
  40. * Possibly related to the distance checks for flowing into nearby holes?
  41. * Water returns 4. Lava returns 2 in the Overworld and 4 in the Nether.
  42. */
  43. @Override
  44. protected int getSlopeFindDistance(LevelReader reader) {
  45. return 4;
  46. }
  47.  
  48. /**
  49. * Water returns 1. Lava returns 2 in the overworld and 1 in the nether.
  50. * @return How many levels a fluid loses per block.
  51. */
  52. @Override
  53. protected int getDropOff(LevelReader reader) {
  54. return 1;
  55. }
  56.  
  57. /**
  58. * Water returns 5. Lava returns 30 in the Overworld and 10 in the Nether.
  59. * Seems to return the delay before it updates its state.
  60. */
  61. @Override
  62. public int getTickDelay(LevelReader reader) {
  63. return 5;
  64. }
  65.  
  66. /**
  67. * Water and Lava both return 100.0F.
  68. */
  69. @Override
  70. protected float getExplosionResistance() {
  71. return 100.0F;
  72. }
  73. }

Implementation

Now let's make an actual fluid which will have still and flowing variants. For this tutorial, we will call it Acid. The missing references will be filled in shortly.

  1. public abstract class AcidFluid extends TutorialFluid {
  2. @Override
  3. public Fluid getSource() {
  4. return YOUR_SOURCE_FLUID_HERE;
  5. }
  6.  
  7. @Override
  8. public Fluid getFlowing() {
  9. return YOUR_FLOWING_FLUID_HERE;
  10. }
  11.  
  12. @Override
  13. public Item getBucket() {
  14. return YOUR_BUCKET_ITEM_HERE;
  15. }
  16.  
  17. @Override
  18. protected BlockState createLegacyBlock(FluidState state) {
  19. return YOUR_FLUID_BLOCK_HERE.defaultBlockState().setValue(BlockStateProperties.LEVEL, getLegacyLevel(state));
  20. }
  21.  
  22. public static class Flowing extends AcidFluid {
  23. @Override
  24. protected void createFluidStateDefinition(StateDefinition.Builder<Fluid, FluidState> builder) {
  25. super.createFluidStateDefinition(builder);
  26. builder.add(LEVEL);
  27. }
  28.  
  29. @Override
  30. public boolean isSource(FluidState state) {
  31. return false;
  32. }
  33.  
  34. @Override
  35. public int getAmount(FluidState state) {
  36. return state.getValue(LEVEL);
  37. }
  38. }
  39.  
  40. public static class Still extends AcidFluid {
  41. @Override
  42. public boolean isSource(FluidState state) {
  43. return true;
  44. }
  45.  
  46. @Override
  47. public int getAmount(FluidState state) {
  48. return 8;
  49. }
  50. }
  51. }

Next, we'll make static instances of still and flowing acid variants, and an acid bucket. In your ModInitializer:

  1. public static FlowingFluid ACID_SOURCE;
  2. public static FlowingFluid FLOWING_ACID;
  3. public static Item ACID_BUCKET;
  4.  
  5. @Override
  6. public void onInitialize() {
  7. ACID_SOURCE = Registry.register(BuiltInRegistries.FLUID, Identifier.fromNamespaceAndPath("tutorial", "acid"), new AcidFluid.Still())
  8. FLOWING_ACID = Registry.register(BuiltInRegistries.FLUID, Identifier.fromNamespaceAndPath("tutorial", "flowing_acid"), new AcidFluid.Flowing());
  9. ACID_BUCKET = Registry.register(BuiltInRegistries.ITEM, Identifier.fromNamespaceAndPath("tutorial", "acid_bucket"),
  10. new BucketItem(ACID_SOURCE, properties), new Item.Properties().craftRemainder(Items.BUCKET).stacksTo(1);
  11.  
  12. // ...
  13. }
  14.  
  15. // ...

To make a custom fluid behave more like water or lava, you must add it to a corresponding fluid tag: For water, make a data/minecraft/tags/fluid/water.json file and write the identifiers of your fluids in there:

  1. {
  2. "replace": false,
  3. "values":
  4. [
  5. "tutorial:acid",
  6. "tutorial:flowing_acid"
  7. ]
  8. }

Making a fluid block

Next we need to create a block which will represent acid in the world. net.minecraft.world.level.block.LiquidBlock is the class we need to use. In your ModInitializer:

  1. public static Block ACID;
  2.  
  3. @Override
  4. public void onInitialize() {
  5. ACID = Registry.register(BuiltInRegistries.BLOCK, Identifier.fromNamespaceAndPath("tutorial", "acid"), new LiquidBlock(ACID_SOURCE, BlockBehaviour.Properties.ofFullCopy(Blocks.WATER)));
  6.  
  7. // ...
  8. }

Now that we have these static objects, we can go back to AcidFluid and fill in the overridden methods:

  1. public abstract class AcidFluid extends TutorialFluid {
  2. @Override
  3. public Fluid getSource() {
  4. return TutorialMod.ACID_SOURCE;
  5. }
  6.  
  7. @Override
  8. public Fluid getFlowing() {
  9. return TutorialMod.FLOWING_ACID;
  10. }
  11.  
  12. @Override
  13. public Item getBucket() {
  14. return TutorialMod.ACID_BUCKET;
  15. }
  16.  
  17. @Override
  18. protected BlockState createLegacyBlock(FluidState state) {
  19. return TutorialMod.ACID.defaultBlockState().setValue(BlockStateProperties.LEVEL, getLegacyLevel(state));
  20. }
  21.  
  22. public static class Flowing extends AcidFluid {
  23. @Override
  24. protected void createFluidStateDefinition(StateDefinition.Builder<Fluid, FluidState> builder) {
  25. super.createFluidStateDefinition(builder);
  26. builder.add(LEVEL);
  27. }
  28.  
  29. @Override
  30. public boolean isSource(FluidState state) {
  31. return false;
  32. }
  33.  
  34. @Override
  35. public int getAmount(FluidState state) {
  36. return state.getValue(LEVEL);
  37. }
  38. }
  39.  
  40. public static class Still extends AcidFluid {
  41. @Override
  42. public boolean isSource(FluidState state) {
  43. return true;
  44. }
  45.  
  46. @Override
  47. public int getAmount(FluidState state) {
  48. return 8;
  49. }
  50. }
  51. }

Rendering setup

For your fluids to have textures or be tinted with a color, you will need to register a FluidRenderHandler for them. Here, we will reuse water's textures and just change the tint color applied to them. To make sure the textures are rendered as translucent, you can use Fabric's BlockRenderLayerMap (see blockappearance).

  1. @Environment(EnvType.CLIENT)
  2. public class TutorialModClient implements ClientModInitializer {
  3.  
  4. @Override
  5. public void onInitializeClient() {
  6. FluidRenderHandlerRegistry.INSTANCE.register(TutorialMod.ACID_SOURCE, TutorialMod.FLOWING_ACID, SimpleFluidRenderHandler.coloredWater(0x4CC248));
  7.  
  8. BlockRenderLayerMap.putFluids(ChunkSectionLayer.TRANSLUCENT, TutorialMod.ACID_SOURCE, TutorialMod.FLOWING_ACID);
  9. }
  10. }

Adding custom textures to your fluid

To create a fluid with custom textures, you will need at least 2 new textures, with an optional 3rd, overlay texture that is shown when you look at the fluid behind glass or leaves. All textures go in the block textures folder.

The first texture should be named YOUR_FLUID_NAME_still.png, and can optionally be animated. The second texture should be named YOUR_FLUID_NAME_flowing.png, and should have double the size in the dimensions. For example, if your still texture has frames of 16×16, the flowing should have frames of 32×32. It can also be animated. The third will be named YOUR_FLUID_NAME_overlay.png, and should not be animated.

Using our acid example, the client initializer will look like this:

  1. @Environment(EnvType.CLIENT)
  2. public class TutorialModClient implements ClientModInitializer {
  3.  
  4. @Override
  5. public void onInitializeClient() {
  6. FluidRenderHandlerRegistry.INSTANCE.register(TutorialMod.ACID_SOURCE, TutorialMod.FLOWING_ACID, new SimpleFluidRenderHandler(
  7. Identifier.fromNamespaceAndPath("tutorial", "block/acid_still"),
  8. Identifier.fromNamespaceAndPath("tutorial", "block/acid_flowing"),
  9. Identifier.fromNamespaceAndPath("tutorial", "block/acid_overlay") /*optional tint can go after the overlay*/);
  10.  
  11. BlockRenderLayerMap.putFluids(ChunkSectionLayer.TRANSLUCENT, TutorialMod.ACID_SOURCE, TutorialMod.FLOWING_ACID);
  12. }
  13. }

You can refer to vanilla's assets 1) as a template for your own textures.

1)
assets/minecraft/textures/block/water_still.png
assets/minecraft/textures/block/water_still.png.mcmeta
assets/minecraft/textures/block/water_flow.png
assets/minecraft/textures/block/water_flow.png.mcmeta
assets/minecraft/textures/block/water_overlay.png
tutorial/fluids.txt · Last modified: 2026/02/23 14:26 by infinitychances