本帖最后由 602723113 于 2019-5-3 15:52 编辑


如何自定义你的实体

目录:
  • 导读
  • 实体的定义
  • 利用EntityZombie来制作你的自定义僵尸
  • 实体的WASD移动
  • PathfinderGoal (实体AI)
  • Navigation (实体的寻路系统)

导读

本教程使用的 Spigot1.10.2-R0.1-SNAPSHOT 和 Spigot1.11.2-R0.1-SNAPSHOT 核心
在阅读之前请确保你具有阅读混淆代码的能力NMS基础Java基础以及反射(reflect)知识
(没有我也会适当的解释的吧)

实体的定义

内容如果你已经知道了什么是 实体,无生命的实体(Insentient Entity),那么你就可以跳过这一小节去看下面的了

实体是Entity包括在Minecraft中所有动态的、移动中的对象
所有的实体都有以下性质:
  • 具有速度、位置和旋转角度。
  • 占有特定的空间大小。此空间是一个固定长宽高的三维长方体(实体不偏斜时,俯视图为正方形)即碰撞箱(Hitbox/AxisAlignedBB/BoundingBox)
  • 当前的生命值。
  • 当着火时,生命值逐渐减少,实体显示出被火焰包围的样子(在携带版中两格高或以上的实体火会显示在臀部)。
  • 状态效果,主要由药水产生
——摘抄自中文MinecraftWiki

在BukkitAPI当中,所有的实体接口都继承了Entity接口,而实体有分为无生命实体有生命实体,例如
有生命实体:
  • Zombie
  • Creeper
  • Player
  • ...
无生命实体:
  • ArmorStand (盔甲架)
  • Arrow (射出的箭)
  • ExpOrb (经验球)
  • ...
所有有生命实体接口都会继承于LivingEntity接口,我们可以在此查看所有继承了LivingEntity接口实体接口
赞美中文BukkitAPI
无生命的实体接口就不会继承于LivingEntity接口

利用EntityZombie来制作你的自定义僵尸

在阅读这一小节之前,我要分为1.11以下1.11以上(包括1.11)两个版本进行讲解,因为在1.11这个版本出现时,Mojang新增了一个叫MinecraftKey的东西,取代了1.11之前使用Integer来表示实体网络ID,当然了它在1.12里面也用于advancement的一个使用

由于在1.8-1.13对实体的实现都是差不多的,所以对于1.11这个分水岭我只讲它们两个注册的不同之处,具体实现请读者自行反编译阅读代码实现!

在BukkitAPI当中,没有什么特殊的操作可以用于自定义一个实体(如果有可以告知我),所以我们就需要进入NMS下对实体进行一个操作

我们新建一个类叫做 MyZombie,之后让它继承于EntityZombie
  1. import net.minecraft.server.v1_10_R1.EntityZombie;

  2. public class MyZombie extends EntityZombie {
  3.    
  4. }
复制代码
然后我们需要实现一个构造方法,这里我使用BukkitAPI里的Location作为它的参数
public MyZombie(Location location) {
    super(((CraftWorld) location.getWorld()).getHandle());
    this.setCustomName("我的僵尸");
    this.setCustomNameVisible(true);
    // 需要设置该实体的Position才会出现在对应的Location
    this.setPosition(location.getX(), location.getY(), location.getZ());
}


之后我们可以复写一些在EntityZombie或其父类里的一些方法,比如我这里复写这个方法
(这里要注意的是不同版本的NMS该方法名会有些不同,所以可以通过该方法的参数名来判断是哪个参数)
@Override
    public boolean a(EntityHuman entityhuman, EnumHand enumhand, @Nullable ItemStack itemstack) {
        // 以下的if判断了这些东西
        // 空值判断
        // 物品类型判断是否为金苹果
        // 判断附加值(耐久, 损坏值)
        // 判断是否为村民
        // 判断是否拥有虚弱Buff
        if ((itemstack != null) && (itemstack.getItem() == Items.GOLDEN_APPLE) && (itemstack.getData() == 0) && (isVillager()) && (hasEffect(MobEffects.WEAKNESS))) {
            if (!entityhuman.abilities.canInstantlyBuild) { // 判断该玩家是否是 可以立即建立的模式 即创造模式
                itemstack.count -= 1; // 减少物品
            }
            if (!this.world.isClientSide) { // 判断是否为客户端
                a(this.random.nextInt(2401) + 3600); // 执行下方截图的代码
            }
            return true;
        }
        return false;
    }
在这里,如果你有丰富的游戏经验的话,不难看出上方的方法其实就是将僵尸村民吃金苹果转换为普通村民的一个方法,那么在MCP中它又叫什么呢?这里我使用了MCP Query这个工具查到了以下的内容
srg: func_184645_a
name: processInteract
side: Both
notch: zi.a
return: boolean
根据上方的信息我们不难看出,在MCP当中,这个NMS下的a()方法其实叫processInteract,并且是一个双向的方法,可以用于客户端上也可以用与服务器
那么我们重写一下这个方法
  1. public boolean a(EntityHuman entityhuman, EnumHand enumhand, @Nullable ItemStack itemstack) {
  2.         if (itemstack == null) {
  3.             return false;
  4.         }
  5.         if (itemstack.getItem() == Items.APPLE) {
  6.             if (!entityhuman.abilities.canInstantlyBuild) {
  7.                 itemstack.count -= 1;
  8.             }
  9.             entityhuman.getBukkitEntity().sendMessage("真香");
  10.         } else {
  11.             entityhuman.getBukkitEntity().sendMessage("我不喜欢这个...");
  12.         }
  13.         return false;
  14.     }
复制代码

如果你自定义的实体是有生命的实体那么就需要对其注册!

Below 1.11 Version —— 低于1.11版本的注册方式

在NMS当中,有个类叫做EntityTypes,该类主要用于注册实体,(具体的注册方式请读者自己去反编译查看),我们可以通过反射的方式来进行注册,看我下面的操作

static {
    ((Map) getPrivateField("c", EntityTypes.class, null)).put("MyZombie", MyZombie.class);
    ((Map) getPrivateField("d", EntityTypes.class, null)).put(MyZombie.class, "MyZombie");
    // 如果我们想覆盖NMS内的僵尸的话那么就可以加上下面那行注释起来的代码
    //((Map) getPrivateField("e", EntityTypes.class, null)).put(54, MyZombie.class);
    ((Map) getPrivateField("f", EntityTypes.class, null)).put(MyZombie.class, 54);
    (Map) getPrivateField("g", EntityTypes.class, null)).put("MyZombie", 54);
}

private static Object getPrivateField(String fieldName, Class clazz, Object object) {
    Field field;
    Object obj = null;
    try {
        field = clazz.getDeclaredField(fieldName);
        field.setAccessible(true);
        obj = field.get(object);
    } catch (NoSuchFieldException | IllegalAccessException e) {
        e.printStackTrace();
    }
    return obj;
}
(论坛日常吞代码)

我们在MyZombie类下新增了一个静态初始化块,用于注册我们的实体,而下面我们需要利用反射获取到EntityTypes下对于所有实体控制的那几个Map(别看它多,其实就是两个双向的Map就可以达到的目的)

这里有人会问了,那个54是什么意思呢?
1.11版本之前,所有的实体都会有一个实体ID,也就是我们上方代码中字段e的那个Map所表示的那样,实体ID -> 对应实体的class
不同的实体会有不同的实体ID,所有的实体ID都可以在这里找到~
https://wiki.vg/Entity_metadata#Mobs  (那个表格,最左边就是,最右边的是1.11版本当中的minecraftkey)

这样我们就做好了在1.11版本以下的实体的一个注册

After 1.11 Version —— 高于1.11版本的注册方式

在1.11之后,注册就变得简单起来了,因为在EntityTypes当中,之前的那些private Map都改成了public static RegisterMaterial,方便大众(当然我不懂是不是md_5自己改的..)

这里要注意的是在不同版本该变量名有所不同例如1.13中它变成了EntityTypes.REGISTRY
  1. public class MyZombie extends EntityZombie {

  2.     private static MinecraftKey minecraftKey;

  3.     static {
  4.         // 给我们的自定义实体做一个MinecraftKey
  5.         minecraftKey = new MinecraftKey("my_zombie"); // minecraft:my_zombie
  6.         // 实体注册
  7.         EntityTypes.d.add(minecraftKey); // 将此key添加至EntityTypes的列表里
  8.         EntityTypes.b.a(54, minecraftKey, MyZombie.class); // 对其注册


  9.         // 如果想取消对该实体的注册那么可以执行下方的代码
  10.         EntityTypes.d.remove(minecraftKey);
  11.         MinecraftKey oldKey = EntityTypes.b.b(EntityZombie.class); // minecraft:zombie
  12.         EntityTypes.b.a(54, oldKey, EntityZombie.class); // 把54号id所对应的minecraft:key和entity class进行覆盖
  13.     }

  14.     public static MinecraftKey getMinecraftKey() {
  15.         return minecraftKey;
  16.     }
  17. }
复制代码
从上方的代码可以看出,我们不再需要反射来对实体进行注册了

这样你就学会了怎么在1.11版本之后对实体进行注册了



具体生成代码:
通过反编译查找到World#spawnEntity可以看出在CraftWorld下会有一个方法叫做addEntity(net.minecraft.server.XXX.Entity entity, SpawnReason reason)
于是我们可以得到下方的代码
Location location = player.getLocation();
// 对自定义进行实例化
MyZombie myZombie = new MyZombie(location);
// 生成至世界内
CraftWorld craftWorld = (CraftWorld) location.getWorld();
craftWorld.addEntity(myZombie, CreatureSpawnEvent.SpawnReason.CUSTOM);

具体效果:



实体的WASD移动

实体的WASD移动,在EntityLiving中,有个叫做g(float, float)的方法,以下是它的MCP信息
srg: func_70612_e
name: moveEntityWithHeading
side: Both
notch: su.g

从上方的信息不难看出,该方法用于控制实体的头向转动和移动
那么我们借用上方的MyZombie来做我们的"载具",首先我们需要重写该方法
  1. public class MyZombie extends EntityZombie {

  2.     /**
  3.      * Move entity with Heading
  4.      *
  5.      * @param sideMotion  向左右的偏移值
  6.      * @param frontMotion 向前或向后的偏移值
  7.      */
  8.     @Override
  9.     public void g(float sideMotion, float frontMotion) {
  10.         super.g(sideMotion, frontMotion);
  11.     }
  12.         
  13. }
复制代码
之后我们就可以开始写具体的WASD移动了
以下代码适用于1.11.2
  1. public class MyZombie extends EntityZombie {

  2.     private static Field isJump = null; // 跳跃字段

  3.     static {
  4.             // 反射一下EntityLiving里的跳跃字段
  5.         try {
  6.             isJump = EntityLiving.class.getDeclaredField("bd"); // 在1.11版本当中, 该字段称为bd, 而在1.10.2中该字段称为be
  7.             isJump.setAccessible(true);
  8.         } catch (NoSuchFieldException | SecurityException e1) {
  9.             e1.printStackTrace();
  10.         }

  11.     }

  12.     /**
  13.      * Move entity with Heading
  14.      *
  15.      * @param sideMotion  向左右的偏移值
  16.      * @param frontMotion 向前或向后的偏移值
  17.      */
  18.     @Override
  19.     public void g(float sideMotion, float forwardMotion) {
  20.         if (this.passengers != null && !this.passengers.isEmpty()) { // 判断有无乘客
  21.             EntityLiving passenger = (EntityLiving) this.passengers.get(0); // 获取该乘客

  22.             // 设置该实体的俯仰角(pitch)、航向角(yaw)跟乘客的俯仰角、航向角一致
  23.             this.yaw = passenger.yaw;
  24.             this.lastYaw = this.yaw;
  25.             this.pitch = (passenger.pitch * 0.5F);
  26.             setYawPitch(this.yaw, this.pitch);
  27.             this.aN = this.yaw;
  28.             this.aP = this.aN;

  29.             float speedAmplifier = 2F; // 这里可以设置你的速度放大器
  30.             // 在1.10.2当中下方的 passenger.be <=> passenger.bf, passenger.bf <=> passenger.bg
  31.             sideMotion = passenger.be * speedAmplifier;
  32.             forwardMotion= passenger.bf * speedAmplifier;
  33.             if (forwardMotion<= 0.0F) { // 如果向前方的偏移值小于0则说明是后退
  34.                 forwardMotion*= 0.25F; // 然后将其设置得慢一点
  35.             }

  36.             if (isJump != null && this.onGround) { // 判断实体是否在地上, 不在地上就不能跳
  37.                 try {
  38.                     if (isJump.getBoolean(passenger)) { // 判断乘客是否跳跃了
  39.                         float jumpHeight = 0.5F; // 跳跃高度
  40.                         this.motY = jumpHeight; // 将该实体在Y轴上进行偏移
  41.                     }
  42.                 } catch (IllegalAccessException e) {
  43.                     e.printStackTrace();
  44.                 }
  45.             }

  46.             super.g(sideMotion, forwardMotion); // 作用回EntityLiving
  47.         }
  48.     }
  49. }
复制代码
那么上面的那些 aN, aQ还有be,bf的字段我是怎么知道的呢?我们反编译EntityHorseAbstract#g(float, float)就可以发现,不同版本这些字段也是不同的,而这在上放的一些字段里,大多都是关于实体的偏移值和一些欧拉角参数的字段,为了能够找到对应版本所对应的字段,请读者自己反编译该方法就可以找到对应的字段了

例如: 1.10.2下的代码
以下代码适用于1.10.2
  1. public class MyZombie extends EntityZombie {

  2.     private static Field isJump = null; // 跳跃字段

  3.     static {
  4.         // 反射一下EntityLiving里的跳跃字段
  5.         try {
  6.             isJump = EntityLiving.class.getDeclaredField("be");
  7.             isJump.setAccessible(true);
  8.         } catch (NoSuchFieldException | SecurityException e) {
  9.             e.printStackTrace();
  10.         }

  11.     }

  12.     /**
  13.      * Move entity with Heading
  14.      *
  15.      * @param sideMotion  向左右的偏移值
  16.      * @param forwardMotion向前或向后的偏移值
  17.      */
  18.     @Override
  19.     public void g(float sideMotion, float forwardMotion) {
  20.         if (this.passengers != null && !this.passengers.isEmpty()) { // 判断有无乘客
  21.             EntityLiving passenger = (EntityLiving) this.passengers.get(0); // 获取该乘客

  22.             // 设置该实体的俯仰角(pitch)、航向角(yaw)跟乘客的俯仰角、航向角一致
  23.             this.yaw = passenger.yaw;
  24.             this.lastYaw = this.yaw;
  25.             this.pitch = (passenger.pitch * 0.5F);
  26.             setYawPitch(this.yaw, this.pitch);
  27.             this.aO = this.yaw;
  28.             this.aQ = this.aO;

  29.             float speedAmplifier = 2F; // 这里可以设置你的速度放大器
  30.             // bf bg
  31.             sideMotion = passenger.bf * speedAmplifier;
  32.             forwardMotion= passenger.bg * speedAmplifier;
  33.             if (forwardMotion<= 0.0F) { // 如果向前方的偏移值小于0则说明是后退
  34.                 forwardMotion*= 0.25F; // 然后将其设置得慢一点
  35.             }

  36.             if (isJump != null && this.onGround) { // 判断实体是否在地上, 不在地上就不能跳
  37.                 try {
  38.                     if (isJump.getBoolean(passenger)) { // 判断乘客是否跳跃了
  39.                         float jumpHeight = 0.5F; // 跳跃高度
  40.                         this.motY = jumpHeight; // 将该实体在Y轴上进行偏移
  41.                     }
  42.                 } catch (IllegalAccessException e) {
  43.                     e.printStackTrace();
  44.                 }
  45.             }

  46.             super.g(sideMotion, forwardMotion); // 作用回EntityLiving
  47.         }
  48.     }
  49. }
复制代码
具体效果:


PathfinderGoal (实体AI)

其实这个内容在论坛里面已经有了相关的帖子,这里我就懒得再说了。。
比如:
所以这里就随便讲讲了

以下的讲解所使用的Spigot版本为1.11.2
在NMS当中,所有的EntityInsentient都会有自己的一套AI系统,比如说EntityZombie,我们反编译该类可以发现一个Zombie有以下的AI
  • 0 -> Float (在水里浮着 感谢12楼的补充)
  • 1 -> HurtByTarget<EntityPigZombie> (欲与猪人五五开)
  • 2 -> ZombieAttack (僵尸式攻击)
  • 2 -> NearestAttackableTarget<EntityHuman> (攻击玩家)
  • 3 -> NearestAttackableTarget<EntityVillager, EntityIronGolem> (攻击村民,铁傀儡)
  • 5 -> MoveTowardsRestriction (走向限制)
  • 6 -> MoveThroughVillage (穿过村庄)
  • 7 -> RandomStrollLand (随机行走)
  • 8 -> LookAtPlayer (看向玩家)
  • 8 -> RandomLookaround (四处看)
我在每一个AI的前面都标上了他们的优先级,级别越低越最先执行

如果我们想制作一个善良的僵尸的话我们就可以把NearestAttackableTarget<EntityHuman, EntityVillager, EntityIronGolem>这几个AI给删去

我们来看看下面的操作
  1. public class MyZombie extends EntityZombie {

  2.     @Override
  3.     protected void r() {
  4.         this.goalSelector.a(0, new PathfinderGoalFloat(this));
  5.         this.goalSelector.a(2, new PathfinderGoalZombieAttack(this, 1.0D, false));
  6.         this.goalSelector.a(5, new PathfinderGoalMoveTowardsRestriction(this, 1.0D));
  7.         this.goalSelector.a(7, new PathfinderGoalRandomStrollLand(this, 1.0D));
  8.         this.goalSelector.a(8, new PathfinderGoalLookAtPlayer(this, EntityHuman.class, 8.0F));
  9.         this.goalSelector.a(8, new PathfinderGoalRandomLookaround(this));
  10.         this.dk();
  11.     }

  12.     @Override
  13.     protected void dk() {
  14.         this.goalSelector.a(6, new PathfinderGoalMoveThroughVillage(this, 1.0D, false));
  15.         this.targetSelector.a(1, new PathfinderGoalHurtByTarget(this, true, EntityPigZombie.class));
  16.         // 将其攻击生物的AI删除
  17. //        this.targetSelector.a(2, new PathfinderGoalNearestAttackableTarget(this, EntityHuman.class, true));
  18. //        if (this.world.spigotConfig.zombieAggressiveTowardsVillager) {
  19. //            this.targetSelector.a(3, new PathfinderGoalNearestAttackableTarget(this, EntityVillager.class, false));
  20. //        }

  21. //        this.targetSelector.a(3, new PathfinderGoalNearestAttackableTarget(this, EntityIronGolem.class, true));
  22.     }
  23. }
复制代码
进游戏后你就会发现,这个僵尸不会再攻击你了

自定义AI:
基本上所有的AI都是以PathfinderGoal为基础的,如果我们想自己写一套AI的话,那么我们就需要将这个类继承与PathfinderGoal,在PathfinderGoal里面有几个需要开发者实现的方法
一个PathfinderGoal基类里面有以下的方法
  • abstarct boolean a()
  • MCP称为: shouldExecute
  • (该方法用于判断该AI任务应该什么时候开始执行)

  • boolean b()
  • MCP称为: continueExecuting
  • (返回正在进行的AI任务是否应继续执行)

  • void c()
  • // MCP称为: startExecuting
  • (执行一次性任务或开始执行连续任务)

  • void d()
  • // MCP称为: resetTask
  • (重置任务的内部状态。 当此AI任务被另一个AI任务中断时调用)

  • void e()
  • // MCP称为: updateTask
  • (继续执行已经启动的连续AI任务)

在一般情况下,我们一般重写 a(),b(),c(),这几个方法足以,具体的自定义AI,我将在下方的Navigation里介绍

Navigation (实体的寻路系统)

在NMS当中,所有的EntityInsentient都会有一个寻路系统,即Navigation,那么我们要怎么用它呢?我们来看下面的几个例子

首先,该系统通常跟实体AI相结合,我们来看下面的一个例子,我们就知道了(顺便把自定义AI也带上了)
  1. import net.minecraft.server.v1_11_R1.EntityInsentient;
  2. import net.minecraft.server.v1_11_R1.Navigation;
  3. import net.minecraft.server.v1_11_R1.PathfinderGoal;
  4. import org.bukkit.Location;

  5. /**
  6. * 使一个实体走动至一个坐标的AI
  7. *
  8. * @author Zoyn
  9. * @since 2018/8/13
  10. */
  11. public class PathfinderGoalWalkToLoc extends PathfinderGoal {

  12.     /**
  13.      * 实体移动速度
  14.      */
  15.     private double speed;
  16.     /**
  17.      * 实体对象
  18.      */
  19.     private EntityInsentient entity;
  20.     /**
  21.      * 坐标
  22.      */
  23.     private Location loc;
  24.     /**
  25.      * 实体导航系统
  26.      */
  27.     private Navigation navigation;

  28.     public PathfinderGoalWalkToLoc(EntityInsentient entity, double speed, Location loc) {
  29.         this.entity = entity;
  30.         this.navigation = (Navigation) this.entity.getNavigation();
  31.         this.speed = speed;
  32.         this.loc = loc;
  33.     }

  34.     @Override
  35.     public boolean a() {
  36.         return true;
  37.     }

  38.     @Override
  39.     public boolean b() {
  40.         return false;
  41.     }

  42.     @Override
  43.     public void c() {
  44.         if (loc != null) {
  45.             this.navigation.a(loc.getX(), loc.getY(), loc.getZ(), speed);
  46.         }
  47.     }
  48. }
复制代码
在上方的代码中,我们看到c()方法,可以很明显的看到这一行
  • this.navigation.a(loc.getX(), loc.getY(), loc.getZ(), speed);
那么这一行就是使实体进行移动的一个例子,而在最后面的那个参数是实体移动的速度

那么我们在实体的AI里添加上它
  1. public class MyZombie extends EntityZombie {

  2.     @Override
  3.     protected void r() {
  4.             // 其他的AI我这里予以省略
  5.         this.goalSelector.a(6, new PathfinderGoalWalkToLoc(this, 1.0D, 你的坐标));
  6.                 // 为了防止实体在移动过程中随意走动, 我们把随意走动的AI删除
  7.                 // this.goalSelector.a(7, new PathfinderGoalRandomStrollLand(this, 1.0D));
  8.     }
  9. }
复制代码
那么同理,我们可以利用Navigation做出让实体巡逻的一个AI,看下面的代码
  1. import com.google.common.collect.Lists;
  2. import net.minecraft.server.v1_11_R1.EntityInsentient;
  3. import net.minecraft.server.v1_11_R1.Navigation;
  4. import net.minecraft.server.v1_11_R1.PathfinderGoal;
  5. import org.bukkit.Location;

  6. import java.util.List;

  7. /**
  8. * 使一个实体进行循环式的巡逻AI
  9. *
  10. * @author Zoyn
  11. * @since 2018/8/13
  12. */
  13. public class PathfinderGoalPatrol extends PathfinderGoal {

  14.     /**
  15.      * 实体移动速度
  16.      */
  17.     private double speed;
  18.     /**
  19.      * 实体对象
  20.      */
  21.     private EntityInsentient entity;
  22.     /**
  23.      * 所有的坐标
  24.      */
  25.     private List<Location> loc;
  26.     /**
  27.      * 实体导航系统
  28.      */
  29.     private Navigation navigation;
  30.     /**
  31.      * 当前应走的坐标的下标
  32.      */
  33.     private int currentLocationIndex;

  34.     public PathfinderGoalPatrol(EntityInsentient entity, double speed, Location... locations) {
  35.         this.entity = entity;
  36.         this.navigation = (Navigation) this.entity.getNavigation();
  37.         this.speed = speed;
  38.         loc = Lists.newArrayList(locations.clone());
  39.         currentLocationIndex = 0;
  40.     }

  41.     public PathfinderGoalPatrol(EntityInsentient entity, double speed, List<Location> locations) {
  42.         this.entity = entity;
  43.         this.navigation = (Navigation) this.entity.getNavigation();
  44.         this.speed = speed;
  45.         loc = locations;
  46.         currentLocationIndex = 0;
  47.     }

  48.     @Override
  49.     public boolean a() {
  50.         // 循环
  51.         Location entityLocation = entity.getBukkitEntity().getLocation();
  52.         if (entityLocation.distance(this.loc.get(currentLocationIndex)) < 1) { // 判断实体的坐标与当前需要走到的坐标之间的距离
  53.             if (currentLocationIndex + 1 >= loc.size()) {
  54.                 currentLocationIndex = 0; // 自动返回至第一项
  55.                 return true;
  56.             }
  57.             currentLocationIndex++;
  58.         }
  59.         return true;
  60.     }

  61.     @Override
  62.     public boolean b() {
  63.         return !this.navigation.n(); // 判断实体是否已经到达
  64.     }

  65.     @Override
  66.     public void c() {
  67.         // 执行移动
  68.         this.navigation.a(loc.get(currentLocationIndex).getX(), loc.get(currentLocationIndex).getY(), loc.get(currentLocationIndex).getZ(), speed);
  69.     }
  70. }
复制代码
具体效果:


那么有人就会想问,如果我想用A*算法来做寻路算法要怎么做呢?
你可以选择重写NavigationAbstract里的MC寻路算法,然后自己实现即可
(当然我觉得没有几个人想写...)


结语
又是一篇没有什么质量的教程,看来我终究写不出来有质量的教程....了吧,希望读者在阅读了本篇教程后可以有自主意识,而不是只顾着CV代码,能写出自己的东西才是最值得的

实体这一块其实主要要配合MCP来阅读源码,这样才会那些混淆过的字段名方法名什么的有个基本概念,这样就会有更多的实体黑科技了~

—— 一个快要高三的普高文科生

2018.8.13


[groupid=1306]Bone Studio[/groupid]