Solved Bat AI/Pathfinding

Discussion in 'Spigot Plugin Development' started by Avenged_, Apr 16, 2017.

  1. Hi, so I'm currently trying to make a custom bat follow me around, but it's not working. The bat gets created, but instead of following the custom navigation I made (which works for other entities), it just flies around randomly. If anyone could tell me why the bat doesn't follow the pathfinding, and how I can fix this that would be great.

    Here's the code.
    Code (Text):
    public BatPet(org.bukkit.World world, Player p) {
            super(((CraftWorld) world).getHandle());

            this.owner = p.getUniqueId();

            // Add pet to pet types.
            PetTypes.types.put(p.getUniqueId(), this);

            // Clear path finding.
            List goalB = (List) Main.getPrivateField("b", PathfinderGoalSelector.class, goalSelector);
            goalB.clear();
            List goalC = (List) Main.getPrivateField("c", PathfinderGoalSelector.class, goalSelector);
            goalC.clear();

            List targetB = (List) Main.getPrivateField("b", PathfinderGoalSelector.class, targetSelector);
            targetB.clear();
            List targetC = (List) Main.getPrivateField("c", PathfinderGoalSelector.class, targetSelector);
            targetC.clear();

            // Set custom path finder goals.
            this.goalSelector.a(0, new PathfinderGoalFloat(this));
            this.goalSelector.a(8, new PathfinderGoalLookAtPlayer(this, EntityHuman.class, 8.0F));

            // Set follow range
            getAttributeInstance(GenericAttributes.FOLLOW_RANGE).setValue(50);

            this.follow();

            this.setLocation(p.getLocation().getX(), p.getLocation().getY(), p.getLocation().getZ(),
                    p.getLocation().getYaw(), p.getLocation().getPitch());
            ((CraftWorld) world).getHandle().addEntity(this);
        }
    The follow method.
    Code (Text):
     public void follow(){
            Player p = Bukkit.getPlayer(owner);
            if(p == null){
                Debug.sendDebugMessage(Level.SEVERE, "Unable to follow " + p.getName() + ", player is no longer online.");
                return;
            }

            new BukkitRunnable(){
                PathEntity path;
                @Override
                public void run() {
                    if(p == null || !isAlive()){
                        this.cancel();
                    }

                    Location loc = p.getLocation().clone();

                    if(loc.distance(getBukkitEntity().getLocation()) > 35){
                        getBukkitEntity().teleport(loc);
                    }else{
                        if(loc.distance(getBukkitEntity().getLocation()) > 6){
                            path = getNavigation().a(loc.getX(), loc.getY(), loc.getZ());
                            getNavigation().a(path, 1.5D);
                        }
                    }
                }
            }.runTaskTimer(Main.plugin, 0, 10);

        }
     
  2. The problem is that the bat doesn't have any pathfinding method
     
  3. Is there any way I can manipulate the bat to follow the player?
     
  4. Take a look into the M() and cM() methods of EntityBat. These seem to have something to do with moving around the world
     
  5. I wasn't able to override the M method, and I also couldn't find it on the GitHub. Do you have a link that has it?
     
  6. Look in your BuildTools directory, it'll have the EntityBat.java file somewhere in there. There might be multiple versions of it, so look at the file dates to find the one you built most recently.
     
  7. Here is the EntityBat file from the actual minecraft server (With JavaDoc), not to be confused with CraftBat

    Code (Text):
    package org.bukkit.craftbukkit.entity;

    import net.minecraft.server.EntityBat;
    import org.bukkit.craftbukkit.CraftServer;
    import org.bukkit.entity.Bat;
    import org.bukkit.entity.EntityType;

    public class CraftBat extends CraftAmbient implements Bat {
        public CraftBat(CraftServer server, EntityBat entity) {
            super(server, entity);
        }

        @Override
        public EntityBat getHandle() {
            return (EntityBat) entity;
        }

        @Override
        public String toString() {
            return "CraftBat";
        }

        public EntityType getType() {
            return EntityType.BAT;
        }

        @Override
        public boolean isAwake() {
            return !getHandle().isAsleep();
        }

        @Override
        public void setAwake(boolean state) {
            getHandle().setAsleep(!state);
        }
    }

    Code (Text):
    package net.minecraft.entity.passive;

    import java.util.Calendar;
    import javax.annotation.Nullable;
    import net.minecraft.block.state.IBlockState;
    import net.minecraft.entity.Entity;
    import net.minecraft.entity.EntityLiving;
    import net.minecraft.entity.SharedMonsterAttributes;
    import net.minecraft.entity.player.EntityPlayer;
    import net.minecraft.init.SoundEvents;
    import net.minecraft.nbt.NBTTagCompound;
    import net.minecraft.network.datasync.DataParameter;
    import net.minecraft.network.datasync.DataSerializers;
    import net.minecraft.network.datasync.EntityDataManager;
    import net.minecraft.util.DamageSource;
    import net.minecraft.util.ResourceLocation;
    import net.minecraft.util.SoundEvent;
    import net.minecraft.util.datafix.DataFixer;
    import net.minecraft.util.math.BlockPos;
    import net.minecraft.util.math.MathHelper;
    import net.minecraft.world.World;
    import net.minecraft.world.storage.loot.LootTableList;

    public class EntityBat extends EntityAmbientCreature
    {
        private static final DataParameter<Byte> HANGING = EntityDataManager.<Byte>createKey(EntityBat.class, DataSerializers.BYTE);

        /** Coordinates of where the bat spawned. */
        private BlockPos spawnPosition;

        public EntityBat(World worldIn)
        {
            super(worldIn);
            this.setSize(0.5F, 0.9F);
            this.setIsBatHanging(true);
        }

        protected void entityInit()
        {
            super.entityInit();
            this.dataManager.register(HANGING, Byte.valueOf((byte)0));
        }

        /**
         * Returns the volume for the sounds this mob makes.
         */
        protected float getSoundVolume()
        {
            return 0.1F;
        }

        /**
         * Gets the pitch of living sounds in living entities.
         */
        protected float getSoundPitch()
        {
            return super.getSoundPitch() * 0.95F;
        }

        @Nullable
        protected SoundEvent getAmbientSound()
        {
            return this.getIsBatHanging() && this.rand.nextInt(4) != 0 ? null : SoundEvents.ENTITY_BAT_AMBIENT;
        }

        protected SoundEvent getHurtSound()
        {
            return SoundEvents.ENTITY_BAT_HURT;
        }

        protected SoundEvent getDeathSound()
        {
            return SoundEvents.ENTITY_BAT_DEATH;
        }

        /**
         * Returns true if this entity should push and be pushed by other entities when colliding.
         */
        public boolean canBePushed()
        {
            return false;
        }

        protected void collideWithEntity(Entity entityIn)
        {
        }

        protected void collideWithNearbyEntities()
        {
        }

        protected void applyEntityAttributes()
        {
            super.applyEntityAttributes();
            this.getEntityAttribute(SharedMonsterAttributes.MAX_HEALTH).setBaseValue(6.0D);
        }

        public boolean getIsBatHanging()
        {
            return (((Byte)this.dataManager.get(HANGING)).byteValue() & 1) != 0;
        }

        public void setIsBatHanging(boolean isHanging)
        {
            byte b0 = ((Byte)this.dataManager.get(HANGING)).byteValue();

            if (isHanging)
            {
                this.dataManager.set(HANGING, Byte.valueOf((byte)(b0 | 1)));
            }
            else
            {
                this.dataManager.set(HANGING, Byte.valueOf((byte)(b0 & -2)));
            }
        }

        /**
         * Called to update the entity's position/logic.
         */
        public void onUpdate()
        {
            super.onUpdate();

            if (this.getIsBatHanging())
            {
                this.motionX = 0.0D;
                this.motionY = 0.0D;
                this.motionZ = 0.0D;
                this.posY = (double)MathHelper.floor(this.posY) + 1.0D - (double)this.height;
            }
            else
            {
                this.motionY *= 0.6000000238418579D;
            }
        }

        protected void updateAITasks()
        {
            super.updateAITasks();
            BlockPos blockpos = new BlockPos(this);
            BlockPos blockpos1 = blockpos.up();

            if (this.getIsBatHanging())
            {
                if (this.world.getBlockState(blockpos1).isNormalCube())
                {
                    if (this.rand.nextInt(200) == 0)
                    {
                        this.rotationYawHead = (float)this.rand.nextInt(360);
                    }

                    if (this.world.getNearestPlayerNotCreative(this, 4.0D) != null)
                    {
                        this.setIsBatHanging(false);
                        this.world.playEvent((EntityPlayer)null, 1025, blockpos, 0);
                    }
                }
                else
                {
                    this.setIsBatHanging(false);
                    this.world.playEvent((EntityPlayer)null, 1025, blockpos, 0);
                }
            }
            else
            {
                if (this.spawnPosition != null && (!this.world.isAirBlock(this.spawnPosition) || this.spawnPosition.getY() < 1))
                {
                    this.spawnPosition = null;
                }

                if (this.spawnPosition == null || this.rand.nextInt(30) == 0 || this.spawnPosition.distanceSq((double)((int)this.posX), (double)((int)this.posY), (double)((int)this.posZ)) < 4.0D)
                {
                    this.spawnPosition = new BlockPos((int)this.posX + this.rand.nextInt(7) - this.rand.nextInt(7), (int)this.posY + this.rand.nextInt(6) - 2, (int)this.posZ + this.rand.nextInt(7) - this.rand.nextInt(7));
                }

                double d0 = (double)this.spawnPosition.getX() + 0.5D - this.posX;
                double d1 = (double)this.spawnPosition.getY() + 0.1D - this.posY;
                double d2 = (double)this.spawnPosition.getZ() + 0.5D - this.posZ;
                this.motionX += (Math.signum(d0) * 0.5D - this.motionX) * 0.10000000149011612D;
                this.motionY += (Math.signum(d1) * 0.699999988079071D - this.motionY) * 0.10000000149011612D;
                this.motionZ += (Math.signum(d2) * 0.5D - this.motionZ) * 0.10000000149011612D;
                float f = (float)(MathHelper.atan2(this.motionZ, this.motionX) * (180D / Math.PI)) - 90.0F;
                float f1 = MathHelper.wrapDegrees(f - this.rotationYaw);
                this.moveForward = 0.5F;
                this.rotationYaw += f1;

                if (this.rand.nextInt(100) == 0 && this.world.getBlockState(blockpos1).isNormalCube())
                {
                    this.setIsBatHanging(true);
                }
            }
        }

        /**
         * returns if this entity triggers Block.onEntityWalking on the blocks they walk on. used for spiders and wolves to
         * prevent them from trampling crops
         */
        protected boolean canTriggerWalking()
        {
            return false;
        }

        public void fall(float distance, float damageMultiplier)
        {
        }

        protected void updateFallState(double y, boolean onGroundIn, IBlockState state, BlockPos pos)
        {
        }

        /**
         * Return whether this entity should NOT trigger a pressure plate or a tripwire.
         */
        public boolean doesEntityNotTriggerPressurePlate()
        {
            return true;
        }

        /**
         * Called when the entity is attacked.
         */
        public boolean attackEntityFrom(DamageSource source, float amount)
        {
            if (this.isEntityInvulnerable(source))
            {
                return false;
            }
            else
            {
                if (!this.world.isRemote && this.getIsBatHanging())
                {
                    this.setIsBatHanging(false);
                }

                return super.attackEntityFrom(source, amount);
            }
        }

        public static void registerFixesBat(DataFixer fixer)
        {
            EntityLiving.registerFixesMob(fixer, EntityBat.class);
        }

        /**
         * (abstract) Protected helper method to read subclass entity data from NBT.
         */
        public void readEntityFromNBT(NBTTagCompound compound)
        {
            super.readEntityFromNBT(compound);
            this.dataManager.set(HANGING, Byte.valueOf(compound.getByte("BatFlags")));
        }

        /**
         * (abstract) Protected helper method to write subclass entity data to NBT.
         */
        public void writeEntityToNBT(NBTTagCompound compound)
        {
            super.writeEntityToNBT(compound);
            compound.setByte("BatFlags", ((Byte)this.dataManager.get(HANGING)).byteValue());
        }

        /**
         * Checks if the entity's current position is a valid location to spawn this entity.
         */
        public boolean getCanSpawnHere()
        {
            BlockPos blockpos = new BlockPos(this.posX, this.getEntityBoundingBox().minY, this.posZ);

            if (blockpos.getY() >= this.world.getSeaLevel())
            {
                return false;
            }
            else
            {
                int i = this.world.getLightFromNeighbors(blockpos);
                int j = 4;

                if (this.isDateAroundHalloween(this.world.getCurrentDate()))
                {
                    j = 7;
                }
                else if (this.rand.nextBoolean())
                {
                    return false;
                }

                return i > this.rand.nextInt(j) ? false : super.getCanSpawnHere();
            }
        }

        private boolean isDateAroundHalloween(Calendar p_175569_1_)
        {
            return p_175569_1_.get(2) + 1 == 10 && p_175569_1_.get(5) >= 20 || p_175569_1_.get(2) + 1 == 11 && p_175569_1_.get(5) <= 3;
        }

        public float getEyeHeight()
        {
            return this.height / 2.0F;
        }

        @Nullable
        protected ResourceLocation getLootTable()
        {
            return LootTableList.ENTITIES_BAT;
        }
    }
     
     
  8. Code (Text):
                 double d0 = (double)this.spawnPosition.getX() + 0.5D - this.posX;
                double d1 = (double)this.spawnPosition.getY() + 0.1D - this.posY;
                double d2 = (double)this.spawnPosition.getZ() + 0.5D - this.posZ;
                this.motionX += (Math.signum(d0) * 0.5D - this.motionX) * 0.10000000149011612D;
                this.motionY += (Math.signum(d1) * 0.699999988079071D - this.motionY) * 0.10000000149011612D;
                this.motionZ += (Math.signum(d2) * 0.5D - this.motionZ) * 0.10000000149011612D;
     
    This is some interesting code
     
  9. Sorry for the late reply, I didn't see Spigots notification. @ramidzk, may I ask how you can find those classes? Also, @DizMizzer, will I be able to somehow change what you said to make the bat follow the player?

    EDIT: Also, when I try to override the updateAI method from the bat class, I get an error saying that the method has private access. How can I get around that?
     
    #9 Avenged_, Apr 18, 2017
    Last edited: Apr 18, 2017
  10. [​IMG]
    Given that this is how Math.signum is defined, we can see from the code that the bat seems to try to go toward it's spawn point.

    Code (Text):

                 double x = (double)this.spawnPosition.getX() + 0.5D - this.posX;
                double y = (double)this.spawnPosition.getY() + 0.1D - this.posY;
                double z = (double)this.spawnPosition.getZ() + 0.5D - this.posZ;
                this.motionX += (Math.signum(d0) * 0.5D - this.motionX) * 0.10000000149011612D;
                this.motionY += (Math.signum(d1) * 0.699999988079071D - this.motionY) * 0.10000000149011612D;
                this.motionZ += (Math.signum(d2) * 0.5D - this.motionZ) * 0.10000000149011612D;
     
    As for the weirdly specific values above, i'm not sure whether it's significant, or just a decompiler thing
     
  11. I think that code is to make the bats movements seem random... Not entirely sure...
     
  12. I got the code with stash for craftbukkit
     
  13. It seems you're somewhat right - Looking at that snippet, it seemed as though it was referencing the spawn position of the bat - It wasn't quite, as a matter of fact. Delving into the source, we have the entire method here (for reference):

    Code (Text):

    protected void M() {
        super.M();
        BlockPosition var1 = new BlockPosition(this);
        BlockPosition var2 = var1.up();
        if(this.isAsleep()) {
            if(this.world.getType(var2).m()) {
                if(this.random.nextInt(200) == 0) {
                    this.aP = (float)this.random.nextInt(360);
                }

                if(this.world.b(this, 4.0D) != null) {
                    this.setAsleep(false);
                    this.world.a((EntityHuman)null, 1025, var1, 0);
                }
            } else {
                this.setAsleep(false);
                this.world.a((EntityHuman)null, 1025, var1, 0);
            }
        } else {
            if(this.b != null && (!this.world.isEmpty(this.b) || this.b.getY() < 1)) {
                this.b = null;
            }

            if(this.b == null || this.random.nextInt(30) == 0 || this.b.distanceSquared((double)((int)this.locX), (double)((int)this.locY), (double)((int)this.locZ)) < 4.0D) {
                this.b = new BlockPosition((int)this.locX + this.random.nextInt(7) - this.random.nextInt(7), (int)this.locY + this.random.nextInt(6) - 2, (int)this.locZ + this.random.nextInt(7) - this.random.nextInt(7));
            }

            double var3 = (double)this.b.getX() + 0.5D - this.locX;
            double var5 = (double)this.b.getY() + 0.1D - this.locY;
            double var7 = (double)this.b.getZ() + 0.5D - this.locZ;
            this.motX += (Math.signum(var3) * 0.5D - this.motX) * 0.10000000149011612D;
            this.motY += (Math.signum(var5) * 0.699999988079071D - this.motY) * 0.10000000149011612D;
            this.motZ += (Math.signum(var7) * 0.5D - this.motZ) * 0.10000000149011612D;
            float var9 = (float)(MathHelper.c(this.motZ, this.motX) * 57.2957763671875D) - 90.0F;
            float var10 = MathHelper.g(var9 - this.yaw);
            this.bf = 0.5F;
            this.yaw += var10;
            if(this.random.nextInt(100) == 0 && this.world.getType(var2).m()) {
                this.setAsleep(true);
            }
        }

    }
     

    Decompiled with mcp:

    Code (Text):

    protected void updateAITasks()
    {
        super.updateAITasks();
        BlockPos blockpos = new BlockPos(this);
        BlockPos blockpos1 = blockpos.up();

        if (this.getIsBatHanging())
        {
            if (this.worldObj.getBlockState(blockpos1).isNormalCube())
            {
                if (this.rand.nextInt(200) == 0)
                {
                    this.rotationYawHead = (float)this.rand.nextInt(360);
                }

                if (this.worldObj.getNearestPlayerNotCreative(this, 4.0D) != null)
                {
                    this.setIsBatHanging(false);
                    this.worldObj.playEvent((EntityPlayer)null, 1025, blockpos, 0);
                }
            }
            else
            {
                this.setIsBatHanging(false);
                this.worldObj.playEvent((EntityPlayer)null, 1025, blockpos, 0);
            }
        }
        else
        {
            if (this.spawnPosition != null && (!this.worldObj.isAirBlock(this.spawnPosition) || this.spawnPosition.getY() < 1))
            {
                this.spawnPosition = null;
            }

            if (this.spawnPosition == null || this.rand.nextInt(30) == 0 || this.spawnPosition.distanceSq((double)((int)this.posX), (double)((int)this.posY), (double)((int)this.posZ)) < 4.0D)
            {
                this.spawnPosition = new BlockPos((int)this.posX + this.rand.nextInt(7) - this.rand.nextInt(7), (int)this.posY + this.rand.nextInt(6) - 2, (int)this.posZ + this.rand.nextInt(7) - this.rand.nextInt(7));
            }

            double d0 = (double)this.spawnPosition.getX() + 0.5D - this.posX;
            double d1 = (double)this.spawnPosition.getY() + 0.1D - this.posY;
            double d2 = (double)this.spawnPosition.getZ() + 0.5D - this.posZ;
            this.motionX += (Math.signum(d0) * 0.5D - this.motionX) * 0.10000000149011612D;
            this.motionY += (Math.signum(d1) * 0.699999988079071D - this.motionY) * 0.10000000149011612D;
            this.motionZ += (Math.signum(d2) * 0.5D - this.motionZ) * 0.10000000149011612D;
            float f = (float)(MathHelper.atan2(this.motionZ, this.motionX) * (180D / Math.PI)) - 90.0F;
            float f1 = MathHelper.wrapDegrees(f - this.rotationYaw);
            this.moveForward = 0.5F;
            this.rotationYaw += f1;

            if (this.rand.nextInt(100) == 0 && this.worldObj.getBlockState(blockpos1).isNormalCube())
            {
                this.setIsBatHanging(true);
            }
        }
    }
     

    It seems a new spawn position is decided for the bat if r.nextInt(30) == 0 (1 in 30 chance)

    Now, with some further edits and comments of mine:

    Code (Text):

    protected void updateAITasks()
    {
        super.updateAITasks();
        BlockPos blockpos = new BlockPos(this);
        BlockPos blockpos1 = blockpos.up();

        if (this.getIsBatHanging())
        {
            if (this.worldObj.getBlockState(blockpos1).isNormalCube())
            {
                if (this.rand.nextInt(200) == 0)
                {
                    this.rotationYawHead = (float)this.rand.nextInt(360);
                }

                if (this.worldObj.getNearestPlayerNotCreative(this, 4.0D) != null)
                {
                    this.setIsBatHanging(false);
                    this.worldObj.playEvent((EntityPlayer)null, 1025, blockpos, 0);
                }
            }
            else
            {
                this.setIsBatHanging(false);
                this.worldObj.playEvent((EntityPlayer)null, 1025, blockpos, 0);
            }
        }
        else
        {
            if (this.spawnPosition != null && (!this.worldObj.isAirBlock(this.spawnPosition) || this.spawnPosition.getY() < 1))
            {
                this.spawnPosition = null;
            }
           
           
            if (this.spawnPosition == null || this.rand.nextInt(30) == 0 || this.spawnPosition.distanceSq((double)((int)this.posX), (double)((int)this.posY), (double)((int)this.posZ)) < 4.0D)
            {
                ThreadLocalRandom r = ThreadLocalRandom.current();
                //-6 and 7 Is actually -6 to 6
                this.spawnPosition = new BlockPos((int)this.posX + r.nextInt(-6, 7),
                        (int)this.posY + r.nextInt(6) - 2, //Bat has a big chance of going upward
                        (int)this.posZ + (int)this.posX + r.nextInt(-6, 7));
            }

           
            //Fly toward specified location.
            double d0 = (double)this.spawnPosition.getX() + 0.5D - this.posX;
            double d1 = (double)this.spawnPosition.getY() + 0.1D - this.posY;
            double d2 = (double)this.spawnPosition.getZ() + 0.5D - this.posZ;
            this.motionX += (Math.signum(d0) * 0.5D - this.motionX) * 0.10000000149011612D;
            this.motionY += (Math.signum(d1) * 0.699999988079071D - this.motionY) * 0.10000000149011612D;
            this.motionZ += (Math.signum(d2) * 0.5D - this.motionZ) * 0.10000000149011612D;
            float f = (float)(MathHelper.atan2(this.motionZ, this.motionX) * (180D / Math.PI)) - 90.0F;
            float f1 = MathHelper.wrapDegrees(f - this.rotationYaw);
            this.moveForward = 0.5F;
            this.rotationYaw += f1;

            if (this.rand.nextInt(100) == 0 && this.worldObj.getBlockState(blockpos1).isNormalCube())
            {
                this.setIsBatHanging(true);
            }
        }
    }
     

    It would seem merely editing the spawnPosition BlockPosition would allow you to specify where the bat should fly in the general direction of. However, how you accomplish that is another matter entirely.

    Or, of course, skip all that bullshit, and just apply some motion vectors in an overrided version of that method, and use some randoms to make it look a bit more like vanilla flight patterns.
    Don't bother worrying about the super.M() at the beginning - the super method is empty anyway.

    Hope this helped!
     
    • Winner Winner x 3
    • Like Like x 1
  14. Thanks for the awesome help :)

    I'll give it a shot.