[Tutorial] Creating custom entities with PathfinderGoals

Discussion in 'Spigot Plugin Development' started by XlordalX, May 11, 2014.

  1. Hello guys,

    Today I will be showing you guys how to create custom entities (and spawn them). So, let's start.

    Creating and spawning custom entities

    Firstly, we will create a custom zombie class:
    Code (Text):
    public class CustomZombie extends EntityZombie
    {
    }
     
    Make sure you extend EntityZombie. Now we have to create a constructor matching the super class:
    Code (Text):
    public class CustomZombie extends EntityZombie
    {
        public CustomZombie(org.bukkit.World world) //You can also directly use the nms world class but this is easier if you are spawning this entity.
        {
            super(((CraftWorld)world).getHandle());
        }
    }
     
    Now we have our custom zombie, it does nothing more than a real zombie. Let's clear the goals of a zombie. To clear the goals of a zombie we will be using this method (I suggest to put this method in a utils class):
    Code (Text):
    public static Object getPrivateField(String fieldName, Class clazz, Object object)
        {
            Field field;
            Object o = null;

            try
            {
                field = clazz.getDeclaredField(fieldName);

                field.setAccessible(true);

                o = field.get(object);
            }
            catch(NoSuchFieldException e)
            {
                e.printStackTrace();
            }
            catch(IllegalAccessException e)
            {
                e.printStackTrace();
            }

            return o;
        }
    You can place this method anywhere you want and then you can use it in your zombie class like this:
    Code (Text):
    public class CustomZombie extends EntityZombie
    {
        public CustomZombie(org.bukkit.World world)
        {
            super(((CraftWorld)world).getHandle());

            List goalB = (List)getPrivateField("b", PathfinderGoalSelector.class, goalSelector); goalB.clear();
            List goalC = (List)getPrivateField("c", PathfinderGoalSelector.class, goalSelector); goalC.clear();
            List targetB = (List)getPrivateField("b", PathfinderGoalSelector.class, targetSelector); targetB.clear();
            List targetC = (List)getPrivateField("c", PathfinderGoalSelector.class, targetSelector); targetC.clear();
        }

        public static Object getPrivateField(String fieldName, Class clazz, Object object)
        {
            Field field;
            Object o = null;

            try
            {
                field = clazz.getDeclaredField(fieldName);

                field.setAccessible(true);

                o = field.get(object);
            }
            catch(NoSuchFieldException e)
            {
                e.printStackTrace();
            }
            catch(IllegalAccessException e)
            {
                e.printStackTrace();
            }

            return o;
        }
    }
     
    This zombie will now do nothing else than standing on its location where it spawned. We will now add its default goals and then customize/remove some.
    Code (Text):
    public class CustomZombie extends EntityZombie
    {
        public CustomZombie(org.bukkit.World world)
        {
            super(((CraftWorld)world).getHandle());
            List goalB = (List)getPrivateField("b", PathfinderGoalSelector.class, goalSelector); goalB.clear();
            List goalC = (List)getPrivateField("c", PathfinderGoalSelector.class, goalSelector); goalC.clear();
            List targetB = (List)getPrivateField("b", PathfinderGoalSelector.class, targetSelector); targetB.clear();
            List targetC = (List)getPrivateField("c", PathfinderGoalSelector.class, targetSelector); targetC.clear();

            this.goalSelector.a(0, new PathfinderGoalFloat(this));
            this.goalSelector.a(2, new PathfinderGoalMeleeAttack(this, EntityHuman.class, 1.0D, false));
            this.goalSelector.a(4, new PathfinderGoalMeleeAttack(this, EntityVillager.class, 1.0D, true));
            this.goalSelector.a(5, new PathfinderGoalMoveTowardsRestriction(this, 1.0D));
            this.goalSelector.a(6, new PathfinderGoalMoveThroughVillage(this, 1.0D, false));
            this.goalSelector.a(7, new PathfinderGoalRandomStroll(this, 1.0D));
            this.goalSelector.a(8, new PathfinderGoalLookAtPlayer(this, EntityHuman.class, 8.0F));
            this.goalSelector.a(8, new PathfinderGoalRandomLookaround(this));
            this.targetSelector.a(1, new PathfinderGoalHurtByTarget(this, true));
            this.targetSelector.a(2, new PathfinderGoalNearestAttackableTarget(this, EntityHuman.class, 0, true));
            this.targetSelector.a(2, new PathfinderGoalNearestAttackableTarget(this, EntityVillager.class, 0, false));
    // This are its default goals.
        }
        public static Object getPrivateField(String fieldName, Class clazz, Object object)
        {
            Field field;
            Object o = null;
            try
            {
                field = clazz.getDeclaredField(fieldName);
                field.setAccessible(true);
                o = field.get(object);
            }
            catch(NoSuchFieldException e)
            {
                e.printStackTrace();
            }
            catch(IllegalAccessException e)
            {
                e.printStackTrace();
            }
            return o;
        }
    }
     
    So now we have its default goals, let's change some.
    Code (Text):
    public class CustomZombie extends EntityZombie
    {
        public CustomZombie(org.bukkit.World world)
        {
            super(((CraftWorld)world).getHandle());
            List goalB = (List)getPrivateField("b", PathfinderGoalSelector.class, goalSelector); goalB.clear();
            List goalC = (List)getPrivateField("c", PathfinderGoalSelector.class, goalSelector); goalC.clear();
            List targetB = (List)getPrivateField("b", PathfinderGoalSelector.class, targetSelector); targetB.clear();
            List targetC = (List)getPrivateField("c", PathfinderGoalSelector.class, targetSelector); targetC.clear();

            this.goalSelector.a(0, new PathfinderGoalFloat(this));
            this.goalSelector.a(2, new PathfinderGoalMeleeAttack(this, EntityIronGolem.class, 1.0D, false));
            this.goalSelector.a(4, new PathfinderGoalMeleeAttack(this, EntitySpider.class, 1.0D, true));
            this.goalSelector.a(5, new PathfinderGoalMoveTowardsRestriction(this, 1.0D));
            this.goalSelector.a(6, new PathfinderGoalMoveThroughVillage(this, 1.0D, false));
            this.goalSelector.a(7, new PathfinderGoalRandomStroll(this, 1.0D));
            this.goalSelector.a(8, new PathfinderGoalLookAtPlayer(this, EntityHuman.class, 8.0F));
            this.goalSelector.a(8, new PathfinderGoalRandomLookaround(this));
            this.targetSelector.a(1, new PathfinderGoalHurtByTarget(this, true));
            this.targetSelector.a(2, new PathfinderGoalNearestAttackableTarget(this, EntitySpider.class, 0, true));
            this.targetSelector.a(2, new PathfinderGoalNearestAttackableTarget(this, EntityIronGolem.class, 0, false));
        }
        public static Object getPrivateField(String fieldName, Class clazz, Object object)
        {
            Field field;
            Object o = null;
            try
            {
                field = clazz.getDeclaredField(fieldName);
                field.setAccessible(true);
                o = field.get(object);
            }
            catch(NoSuchFieldException e)
            {
                e.printStackTrace();
            }
            catch(IllegalAccessException e)
            {
                e.printStackTrace();
            }
            return o;
        }
    }
    What this does is, it does not attack players and villagers anymore. It does now attack Iron Golems and Spiders. You can change this to your liking.

    Now we have our custom zombie, but we need to add it to bukkit first so we can spawn it. To do this, create this enum class:
    Code (Text):
    public enum EntityTypes
    {
        //NAME("Entity name", Entity ID, yourcustomclass.class);
        CUSTOM_ZOMBIE("Zombie", 54, CustomZombie.class); //You can add as many as you want.

        private EntityTypes(String name, int id, Class<? extends Entity> custom)
        {
            addToMaps(custom, name, id);
        }

      public static void spawnEntity(Entity entity, Location loc)
       {
         entity.setLocation(loc.getX(), loc.getY(), loc.getZ(), loc.getYaw(), loc.getPitch());
         ((CraftWorld)loc.getWorld()).getHandle().addEntity(entity);
       }

        private static void addToMaps(Class clazz, String name, int id)
        {
            //getPrivateField is the method from above.
            //Remove the lines with // in front of them if you want to override default entities (You'd have to remove the default entity from the map first though).
            ((Map)getPrivateField("c", net.minecraft.server.v1_7_R4.EntityTypes.class, null)).put(name, clazz);
            ((Map)getPrivateField("d", net.minecraft.server.v1_7_R4.EntityTypes.class, null)).put(clazz, name);
            //((Map)getPrivateField("e", net.minecraft.server.v1_7_R4.EntityTypes.class, null)).put(Integer.valueOf(id), clazz);
            ((Map)getPrivateField("f", net.minecraft.server.v1_7_R4.EntityTypes.class, null)).put(clazz, Integer.valueOf(id));
            //((Map)getPrivateField("g", net.minecraft.server.v1_7_R4.EntityTypes.class, null)).put(name, Integer.valueOf(id));
        }
    }
    To spawn our custom zombie now, we will use the following:
    Code (Text):
    EntityTypes.spawnEntity(new CustomZombie(Bukkit.getWorld("world")), new Location(Bukkit.getWorld("world"), 100, 100, 100));
    Creating your own Pathfindergoals
    Pathfindergoals handle the ai of your entity.

    To create a new Pathfindergoal, create a class like this:
    Code (Text):
    public class PathfinderGoalWalkToLoc extends PathfinderGoal

    {

    }
    Make sure you extend Pathfindergoal.

    Now you will get an error in your IDE (Atleast in IntelliJ which I am using). That's because we have to add this method in our class:
    Code (Text):
    public class PathfinderGoalWalkToLoc extends PathfinderGoal
    {
        public boolean a()
        {
            return true;
        }
    }
    What this method actually means is "shouldStart()".

    Now we are going to add a method called "c()" which will start when "a()" is true (So if you always want this, just make a returning always true like I did above). Since we need an entity to make it walk to our location we need to add a Constructor to initialize our entity and location field:
    Code (Text):
    public class PathfinderGoalWalkToLoc extends PathfinderGoal
    {
       private double speed;

       private EntityInsentient entity;

       private Location loc;

       private Navigation navigation;

       public PathfinderGoalWalkToLoc(EntityInsentient entity, Location loc, double speed)
       {
         this.entity = entity;
         this.loc = loc;
         this.navigation = this.entity.getNavigation();
         this.speed = speed;
       }

       public boolean a()
       {
         return true;
       }
    }
    If you do not know what a constructor is, click here.

    Now we are going to create a method called "c()" which means "onStart()" so once our "a()" method has returned true, it will execute this part of our code. This is how your "c()" method should look like:
    Code (Text):
        public void c()
        {
            PathEntity pathEntity = this.navigation.a(loc.getX(), loc.getY(), loc.getZ());

            this.navigation.a(pathEntity, speed);
        }
    Make sure that the location is not more than 20 blocks (I believe 20, I am not sure about this) away from the entity, otherwise the pathEntity field will be null.

    So now you can add this to your custom entity class the same as we did above. I hope you guys liked this thread.

    I will soon extend this post with a tutorial on how to make the entity walk back to its location as soon as it's pushed away and a tutorial on how to make the entity unable to be pushed at all (The last one can be a bit glitchy some times).

    Thanks for reading!
     
    #1 XlordalX, May 11, 2014
    Last edited: Apr 23, 2016
    • Useful x 110
    • Like x 29
    • Informative x 8
    • Winner x 7
    • Friendly x 3
    • Creative x 3
    • Agree x 2
    • Funny x 1
  2. Nice tutorial. I know some other ways of doing it but this is nice. Good Job.
     
    • Like Like x 2
    • Agree Agree x 1
    • Friendly Friendly x 1
  3. Thank you!
     
    • Winner Winner x 4
  4. Looks sexy. I'll try this out in a little while. Great job.
     
    • Like Like x 1
  5. Awesome! Is there a way to change target radius?
     
  6. Yes I believe there is, I am not entirely sure but try to put the following in your custom entity class (Which extends EntityLiving, so for example EntityZombie):
    Code (Text):
    this.getAttributeInstance(GenericAttributes.b).setValue(/*your value here, default is 32*/);
    So I am not sure if this works but you can just try it :)
     
  7. Okay guys, I added a new tutorial on how to create your own PathfinderGoals.
     
  8. @XlordalX I am not sure if I got this wrong or if I am correct but I think I see a error, In the part where you create the EntityTypes enum the addToMaps method uses getPrivateField, You say that its the one from above, but the one from above uses 3 fields, A string, a class, and a object but when you use it in addToMaps you only put in the field of the string and the class? So I am a little bit confused to as to what to do? Please correct me if I am wrong.
     
    #8 Minezbot, Jun 8, 2014
    Last edited: Jun 8, 2014
  9. Hmmm, strange im not getting any error.
     
  10. Not sure what you are saying here but I changed another thing in the enum addToMaps method.

    Edit: Fixed.
     
  11. I have problems with the EntityTypes class, I can't get it to work... it says "The nested type EntityTypes cannot hide an enclosing type".
    I'm trying to make a custom wither, the customWither class doesn't have errors.

    This tutorial is awesome, but I'm not an expert in java.
    This is the code from my EntityType class:
    Code (Text):

    import org.bukkit.Location;
    import org.bukkit.entity.Entity;

    public class EntityTypes {
     
        public enum EntityTypes{
           //NAME("Entity name", Entity ID, yourcustomclass.class);
           CUSTOM_WITHER("Wither", 64, CustomWither.class); //You can add as many as you want.
         
           private EntityType(String name, int id, Class<? extends Entity> custom){
               addToMaps(custom, name, id);
           }
       
           public static void spawnEntity(Entity entity, Location loc){
              entities.add(entity);
              entity.setLocation(loc.getX(), loc.getY(), loc.getZ(), loc.getYaw(), loc.getPitch());
              ((CraftWorld)loc.getWorld()).getHandle().addEntity(entity);
           }

           private static void addToMaps(Class clazz, String name, int id){
               //getPrivateField is the method from above.
               //Remove the maps with // in front of them if you don't want to override default entities (You'd have to remove the default entity from the map first though).
               //((Map)getPrivateField("c", EntityTypes.class, null)).put(name, clazz);
               ((Map)getPrivateField("d", EntityTypes.class, null)).put(clazz, name);
               //((Map)getPrivateField("e", EntityTypes.class, null)).put(Integer.valueOf(id), clazz);
               ((Map)getPrivateField("f", EntityTypes.class, null)).put(clazz, Integer.valueOf(id));
               //((Map)getPrivateField("g", EntityTypes.class, null)).put(name, Integer.valueOf(id));
           }
        }
    }
     
     
  12. Try removing the enclosing class declaration - OP initially calls the enum a class when outlining the technique, but it is not one.
     
  13. It's ok, I've solved it and it now works with zombies, but when I try to make a custom wither, the mob is still being agressive, I like to spawn a peaceful wither boss... I've edited the default PathFinderGoals but it seems to not take effect.
     
  14. Have you clreared the default goals?
     
  15. I understand it may cause misunderstandings but in this tutorial, the enum is a class. I edited the post though :).
     
  16. Yes, I've cleared all goals. If I spawn a ZombieEntity, the mob is not agressive, if I spawn a wither, it becomes hostile.
     
  17. Add this to your wither class:
    Code (Text):
    protected void bm()
    {
    }
     
  18. I hope someone can help me. I get this to work on many entities, like zombies or skeletons, but this doesn't work with bats. Bats don't have any pathfindergoals, but they move around randomly. Is there another way to stop a bat from moving around, but still flying on the same y level? I can't just override the move(x,y,z) method because i need to move the bat by velocity. Until now I used a small trick. The constructor of EntityBat sets the bat asleep. I override the bn() method that sets bats no longer asleep, so the bat is hanging all the time, but this is really buggy. Any ideas?
     
  19. How could I make it where an entity has the same pathfinder as a pig when it runs away? So like a spider runs away when you hit it rather than attacking back. And btw nice tutorial, really helpful :)
     
  20. Nice tutorial thanks!