Minecraft Modding: Getting Custom Fluids To Push Entities Like Water

Background


The build in water block is able to push entities according to its flow. Unfortunately, Forge's custom fluid system doesn't have this built in and the vanilla behavior is hard-coded to the Material.WATER material. Now you can set your material to simply be Material.WATER but that means that your fluid will act entirely like water and also have a blue color on the map, which you probably don't want.

So assuming you've created a custom fluid (see Jabelar's Custom Fluid Tutorial), here is how you can replicate the effect of water with your custom fluids.

Note that this technique is a bit advanced and requires replicating a lot of the vanilla code into your own tick handlers. I also assume you know about proxy implementations (if not see Jabelar's Sided Proxy Tutorial) and event handling (if not see Jabelar's Event Handling Tutorial).

The Tick Handling Approach


You can do this in a WorldTickEvent handler fairly easily; however, an even trickier thing is that non-player entities are pushed on the client side but player motion is primarily controlled on the client side. So you have to use some proxy tricks to have equivalent methods but call the client one from the PlayerTickEvent.

So the steps are:

  1. Create a PlayerTickEvent handler that calls a client proxy method for handling material acceleration.
  2. Create a WorldTickEvent handler that calls a common proxy method for handling material acceleration.
  3. Create a common proxy method for handling material acceleration, essentially copying the vanilla code for water.
  4. Create a client proxy method for handling material acceleration, also essentially copying the vanilla code for water.
  5. Create a code in your custom fluid block that is called by the proxy methods.

Step #1 Create A PlayerTickHandler Handler Method

For example, the player tick event handler should be something like this:
@SubscribeEvent(priority=EventPriority.NORMAL, receiveCanceled=true)
    public void onEvent(PlayerTickEvent event)
    {       
// do client side stuff
        if (event.phase == TickEvent.Phase.START && event.player.world.isRemote) // only proceed if START phase otherwise, will execute twice per tick
        {
            EntityPlayer thePlayer = event.player;
       MainMod.proxy.handleMaterialAcceleration(thePlayer, ModBlocks.SLIME_BLOCK.getDefaultState().getMaterial());           
        }
    }

Step #2. Create A WorldTickEvent Handler Method

For example, the world tick event handler should look something like this:
  @SubscribeEvent(priority=EventPriority.NORMAL, receiveCanceled=true)
  public void onEvent(WorldTickEvent event)
  {
      if (event.phase == TickEvent.Phase.END) // only proceed if START phase otherwise, will execute twice per tick
      {
          return;
      }   
      
      List entityList = event.world.loadedEntityList;
      Iterator iterator = entityList.iterator();
      
      while(iterator.hasNext())
      {
       Entity theEntity = iterator.next();
       
       /* 
        * Update all motion of all entities except players that may be inside your fluid
        */
      MainMod.proxy.handleMaterialAcceleration(theEntity, ModBlocks.SLIME_BLOCK.getDefaultState().getMaterial());
      }
  }

Step #3. Create A Method For Handling Material Acceleration In your "Common Proxy"

And your common proxy class, have something like the following (this is fairly complicated and can be copied mostly as is) which essentially copies the code for how water pushes entities:
    /**
     * handles the acceleration of an object whilst in a material. 
     */
public boolean handleMaterialAcceleration(Entity entityIn, Material materialIn)
    {
     World parWorld = entityIn.world;
     AxisAlignedBB bb = entityIn.getEntityBoundingBox().grow(0.0D, -0.4000000059604645D, 0.0D).shrink(0.001D);
    
        int j2 = MathHelper.floor(bb.minX);
        int k2 = MathHelper.ceil(bb.maxX);
        int l2 = MathHelper.floor(bb.minY);
        int i3 = MathHelper.ceil(bb.maxY);
        int j3 = MathHelper.floor(bb.minZ);
        int k3 = MathHelper.ceil(bb.maxZ);

        boolean flag = false;
        Vec3d vec3d = Vec3d.ZERO;
        BlockPos.PooledMutableBlockPos blockpos$pooledmutableblockpos = BlockPos.PooledMutableBlockPos.retain();

        for (int l3 = j2; l3 < k2; ++l3)
        {
            for (int i4 = l2; i4 < i3; ++i4)
            {
                for (int j4 = j3; j4 < k3; ++j4)
                {
                    blockpos$pooledmutableblockpos.setPos(l3, i4, j4);
                    IBlockState iblockstate1 = parWorld.getBlockState(blockpos$pooledmutableblockpos);
                    Block block = iblockstate1.getBlock();

                    Boolean result = block.isEntityInsideMaterial(parWorld, blockpos$pooledmutableblockpos, iblockstate1, entityIn, i3, materialIn, false);
                    if (result != null && result == true)
                    {
                        // Forge: When requested call blocks modifyAcceleration method, and more importantly cause this method to return true, which results in an entity being "inWater"
                        flag = true;
                        vec3d = block.modifyAcceleration(parWorld, blockpos$pooledmutableblockpos, entityIn, vec3d);
                     
//                        // DEBUG
//                     System.out.println("Entity is inside material = "+materialIn+" and motion add vector = "+vec3d);
                     
                        continue;
                    }
                    else if (result != null && result == false) continue;

                    if (iblockstate1.getMaterial() == materialIn)
                    {
//                     // DEBUG
//                     System.out.println("blockstate material matches material in");
                     
                        double d0 = i4 + 1 - BlockLiquid.getLiquidHeightPercent(iblockstate1.getValue(BlockLiquid.LEVEL).intValue());

                        if (i3 >= d0)
                        {
                         flag = true;
                         vec3d = block.modifyAcceleration(parWorld, blockpos$pooledmutableblockpos, entityIn, vec3d);
                         
//                            // DEBUG
//                         System.out.println("deep enough to push entity and motion add = "+vec3d);                 
                         }
                    }
                }
            }
        }

        blockpos$pooledmutableblockpos.release();

        if (vec3d.lengthVector() > 0.0D && entityIn.isPushedByWater())
        {
//         // DEBUG
//         System.out.println("motion vector is non-zero");
         
         /*
          * Although applied to all entities, EntityPlayer doesn't really take
          * affect, so the fluid motion control is handled in the client-side
          * PlayerTickEvent
          */
            vec3d = vec3d.normalize();
            double d1 = 0.014D;
            entityIn.motionX += vec3d.x * d1;
            entityIn.motionY += vec3d.y * d1;
            entityIn.motionZ += vec3d.z * d1;
        }
        else
        {
//             // DEBUG
//             System.out.println("motion vector is zero");
        }
    
        entityIn.fallDistance = 0.0F;

        return flag;
    }
}

Step #4. Create A Method For Handling Material Acceleration In Your "Client Proxy"

And in your "client proxy" class, you should have something like this (very similar to what was in the common proxy but slightly different:
    /**
     * handles the acceleration of an object whilst in a material. 
     */
    @Override
public boolean handleMaterialAcceleration(Entity entityIn, Material materialIn)
    {
     World parWorld = entityIn.world;
     AxisAlignedBB bb = entityIn.getEntityBoundingBox().grow(0.0D, -0.4000000059604645D, 0.0D).shrink(0.001D);
    
        int j2 = MathHelper.floor(bb.minX);
        int k2 = MathHelper.ceil(bb.maxX);
        int l2 = MathHelper.floor(bb.minY);
        int i3 = MathHelper.ceil(bb.maxY);
        int j3 = MathHelper.floor(bb.minZ);
        int k3 = MathHelper.ceil(bb.maxZ);

        boolean flag = false;
        Vec3d vec3d = Vec3d.ZERO;
        BlockPos.PooledMutableBlockPos blockpos$pooledmutableblockpos = BlockPos.PooledMutableBlockPos.retain();

        for (int l3 = j2; l3 < k2; ++l3)
        {
            for (int i4 = l2; i4 < i3; ++i4)
            {
                for (int j4 = j3; j4 < k3; ++j4)
                {
                    blockpos$pooledmutableblockpos.setPos(l3, i4, j4);
                    IBlockState iblockstate1 = parWorld.getBlockState(blockpos$pooledmutableblockpos);
                    Block block = iblockstate1.getBlock();

                    Boolean result = block.isEntityInsideMaterial(parWorld, blockpos$pooledmutableblockpos, iblockstate1, entityIn, i3, materialIn, false);
                    if (result != null && result == true)
                    {
                        // Forge: When requested call blocks modifyAcceleration method, and more importantly cause this method to return true, which results in an entity being "inWater"
                        flag = true;
                        vec3d = block.modifyAcceleration(parWorld, blockpos$pooledmutableblockpos, entityIn, vec3d);
                     
//                        // DEBUG
//                     System.out.println("Entity is inside material = "+materialIn+" and motion add vector = "+vec3d);
                     
                        continue;
                    }
                    else if (result != null && result == false) continue;

                    if (iblockstate1.getMaterial() == materialIn)
                    {
//                     // DEBUG
//                     System.out.println("blockstate material matches material in");
                     
                        double d0 = i4 + 1 - BlockLiquid.getLiquidHeightPercent(iblockstate1.getValue(BlockLiquid.LEVEL).intValue());

                        if (i3 >= d0)
                        {
                         flag = true;
                         vec3d = block.modifyAcceleration(parWorld, blockpos$pooledmutableblockpos, entityIn, vec3d);
                         
//                            // DEBUG
//                         System.out.println("deep enough to push entity and motion add = "+vec3d);                 
                         }
                    }
                }
            }
        }

        blockpos$pooledmutableblockpos.release();

        if (vec3d.lengthVector() > 0.0D && entityIn.isPushedByWater())
        {
//         // DEBUG
//         System.out.println("motion vector is non-zero");
         
         /*
          * Although applied to all entities, EntityPlayer doesn't really take
          * affect, so the fluid motion control is handled in the client-side
          * PlayerTickEvent
          */
            vec3d = vec3d.normalize();
            double d1 = 0.014D;
            entityIn.motionX += vec3d.x * d1;
            entityIn.motionY += vec3d.y * d1;
            entityIn.motionZ += vec3d.z * d1;
        }
        else
        {
//             // DEBUG
//             System.out.println("motion vector is zero");
        }
    
        entityIn.fallDistance = 0.0F;

        return flag;
    }
}

Step #5. Create A Method In Your Custom Fluid Block To Support Material Acceleration

Add a modifyAcceleration() and related methods to your custom fluid block class, as follows (again this is fairly complicated and can be mostly just copied as is):
public static boolean getPushesEntity()
{
return pushesEntity;
}

public static void setPushesEntity(boolean parPushesEntity)
{
pushesEntity = parPushesEntity;
}

    @Override
public Vec3d modifyAcceleration(World worldIn, BlockPos pos, Entity entityIn, Vec3d motion)
    {
//     // DEBUG
//     System.out.println("modifyAcceleration for "+entityIn+" with isPushedByWater() = "+entityIn.isPushedByWater());
    
     if (getPushesEntity())
     {
     Vec3d flowAdder = getFlow(worldIn, pos, worldIn.getBlockState(pos));

//     // DEBUG
//     System.out.println("may push entity with motion adder = "+flowAdder);
    
return motion.add(flowAdder);
     }
     else
     {
//     // DEBUG
//     System.out.println("may not push entity");
    
     return motion;
     }
    }

    protected Vec3d getFlow(IBlockAccess worldIn, BlockPos pos, IBlockState state)
    {
        double d0 = 0.0D;
        double d1 = 0.0D;
        double d2 = 0.0D;
        int i = this.getRenderedDepth(state);
        BlockPos.PooledMutableBlockPos blockpos$pooledmutableblockpos = BlockPos.PooledMutableBlockPos.retain();

        for (EnumFacing enumfacing : EnumFacing.Plane.HORIZONTAL)
        {
            blockpos$pooledmutableblockpos.setPos(pos).move(enumfacing);
            int j = this.getRenderedDepth(worldIn.getBlockState(blockpos$pooledmutableblockpos));

            if (j < 0)
            {
                if (!worldIn.getBlockState(blockpos$pooledmutableblockpos).getMaterial().blocksMovement())
                {
                    j = this.getRenderedDepth(worldIn.getBlockState(blockpos$pooledmutableblockpos.down()));

                    if (j >= 0)
                    {
                        int k = j - (i - 8);
                        d0 += enumfacing.getFrontOffsetX() * k;
                        d1 += enumfacing.getFrontOffsetY() * k;
                        d2 += enumfacing.getFrontOffsetZ() * k;
                    }
                }
            }
            else if (j >= 0)
            {
                int l = j - i;
                d0 += enumfacing.getFrontOffsetX() * l;
                d1 += enumfacing.getFrontOffsetY() * l;
                d2 += enumfacing.getFrontOffsetZ() * l;
            }
        }

        Vec3d vec3d = new Vec3d(d0, d1, d2);

        if (state.getValue(LEVEL).intValue() >= 8)
        {
//         // DEBUG
//         System.out.println("fluid level greater than zero");
        
            for (EnumFacing enumfacing1 : EnumFacing.Plane.HORIZONTAL)
            {
                blockpos$pooledmutableblockpos.setPos(pos).move(enumfacing1);

                if (this.causesDownwardCurrent(worldIn, blockpos$pooledmutableblockpos, enumfacing1) || this.causesDownwardCurrent(worldIn, blockpos$pooledmutableblockpos.up(), enumfacing1))
                {
//                 // DEBUG
//                 System.out.println("Causes downward current");
                
                    vec3d = vec3d.normalize().addVector(0.0D, -6.0D, 0.0D);
                    break;
                }
            }
        }

        blockpos$pooledmutableblockpos.release();
        return vec3d.normalize();
    }

    protected int getDepth(IBlockState state)
    {
        return state.getMaterial() == this.blockMaterial ? state.getValue(LEVEL).intValue() : -1;
    }

    protected int getRenderedDepth(IBlockState state)
    {
        int i = this.getDepth(state);
        return i >= 8 ? 0 : i;
    }

    /**
     * Checks if an additional {@code -6} vertical drag should be applied to the entity. See {#link
     * net.minecraft.block.BlockLiquid#getFlow()}
     */
    private boolean causesDownwardCurrent(IBlockAccess worldIn, BlockPos pos, EnumFacing side)
    {
        IBlockState iblockstate = worldIn.getBlockState(pos);
        Block block = iblockstate.getBlock();
        Material material = iblockstate.getMaterial();

        if (material == this.blockMaterial)
        {
            return false;
        }
        else if (side == EnumFacing.UP)
        {
            return true;
        }
        else if (material == Material.ICE)
        {
            return false;
        }
        else
        {
            boolean flag = isExceptBlockForAttachWithPiston(block) || block instanceof BlockStairs;
            return !flag && iblockstate.getBlockFaceShape(worldIn, pos, side) == BlockFaceShape.SOLID;
        }
    }
}

Conclusion


So basically the technique is copy a whole bunch of the water pushing code over to the tick handlers so you can get same effect with your own fluids. Mostly you can just copy the code too, but as always I recommend you at least read through it to understand what it does.

No comments:

Post a Comment