Bukkit 如何自定义实体/怪物的行为

在Bukkit里,如何自定义实体、怪物的行为呢?

本文用魔改牛牛君为例,介绍了两种如何怪物、生物的行为AI的方法。


一、设想效果


在我设想的效果中,自定义的怪物是多种多样的。同一个怪物模型,不同的怪物等级,拥有不同的AI。举个简单的例子。

普通僵尸:见到玩家会追击和攻击
懒惰的僵尸:见到玩家不会主动追击,除非玩家攻击他。
胆小的僵尸:见到玩家就会逃跑。
变异的僵尸:不惧怕太阳照射。

以上的这几个僵尸例子,就是使用同一个怪物模型(僵尸),不同的AI实现。
而要达成这个想法,使用我翻译的那篇文章显然是不可能的。

在我们自定义怪物的时候,只能自定义已有怪物的AI,而不能修改他们的材质或模型,因为那样的话客户端无法正常显示。

二、研究Minecraft底层中Entity的工作


为此,我专门翻阅了Entity在底层中是如何工作的。
简单来说,Entity包含很多种,而我们的怪物或者是动物的父类是LivingEntity(活性实体)
而LivingEntity之中包含了该实体的AI部分处理。

Bukkit 如何自定义实体/怪物的行为

在上图中,tasks和targetTasks都是实体的AI表,而每个AI表都存在多个AI处理器。

Bukkit 如何自定义实体/怪物的行为

如图是实体僵尸的所有AI,定义了僵尸会游泳, 攻击,监视附近的玩家,以及主动攻击玩家,村民和铁傀儡。首先攻击伤害自己的单位。
而我们的工作就是把这些AI全部修改掉,然后修改成我们自定义的AI。

大约就是 entity.tasks.clear() 和 entity.targetTasks.clear() 这么简单。

每个具有AI的实体都是由World来进行刷新的(每Tick一次 / Tick = 1/20s)

三、魔改牛牛君


众所周知,牛牛菌属于动物,是不会攻击任何生物/怪物 或玩家的。而我们要做的是一个疯狂的牛牛菌,他攻击鸡鸡菌。

为了简化,我简单写了一个插件,然后注册了一个命令叫做mobs,下面我开始onCommand里魔改牛牛菌:

Bukkit 如何自定义实体/怪物的行为

只有简单的几行,我们就可以创建出一个独一无二的,拥有独立AI的牛牛菌。(正常的牛不会受到影响)


第一步、获取Server/World的NMS实例

我们通过CommandSender获得Player(当然Console是不可以使用这个指令的)
关键的第一步我们要获得WorldServer,而对于这个WorldServer,我要进行一下解释:
大家可能都知道Bukkit的World,他代表了一个世界。而如果你深究他是如何实现的,你会发现真正实现它的是CraftBukkit的CraftWorld,而CraftWorld封装了WorldServer。

对于这三者的关系是这样的。
WorldServer是原生的,血脉纯正,MOJANG写的。
CraftWorld 是非官方组织开发的,属于CraftBukkit,封装了WorldServer。
而Bukkit的World只是一个接口,他真正的实现类是CraftWorld。
所以我们通过World强转获得CraftWorld,而通过CraftWorld的getHandle()获得WorldServer的实例


第二步、创建一个实体。

创建实体的步骤很简单,实例化一个实体类。将这个实体类添加到世界中。(如果不添加到世界中则不会显示和刷新AI)
实例化需要提供一个WorldServer作为参数。


第三步、删除实体的AI,让他变白痴。

删除实体的AI,重新实例化一个PathfinderGoalSelector,并覆盖掉实体原来的PathfinderGoalSelector。
新的PathfinderGoalSelector是空的,所以相当于置空了实体的AI列表。
这样做,会让疯狂的牛牛菌变成一个白痴。你打他,他没反应。你把他推到水里,他会沉底。你拿着小麦在他面前晃悠,他无视你。
他不会走动,不会转头。攻击他也不会惊慌失措的逃跑。

PathfinderGoalSelector 是AI列表(具体请看最后解释)


第四步、重新制作牛牛菌的脑子。

下一步就是重新制作牛牛菌的脑子了,我们的目标是: 让牛牛菌主动攻击鸡
你可以看到在上图的代码中,我给他添加了一个名为AIAtk的AI处理器,没错,这个AIAtk是我自己写的一个AI处理器。他会让实体攻击其他实体。

AIAtk完全仿照PathfinderGoalMeleeAttack ,PathfinderGoalMeleeAttack貌似不能为动物所用,无效果,我还没研究透彻。不清楚哪里限制了这个AI处理器不能为动物所用,所以我写了一个AIAtk,完全复制PathfinderGoalMeleeAttack的代码,但是在伤害计算部分是直接进行伤害的

随后我给他的目标选择器targetSelector添加了一个主动攻击近处的目标的AI处理器,而目标设定为EntityChicken鸡鸡菌。

你可以用PathfinderGoalMeleeAttack替代AIAtk,结果是牛追击鸡,但是不会造成伤害。


第五步、开服测试

Bukkit 如何自定义实体/怪物的行为

首先我通过指令mobs生成了一个疯狂的牛牛菌,它现在是半脑残状态,因为我移除了它*移动的AI,所以它只会站着不动。面对我们的镜头,一点也不紧张。

Bukkit 如何自定义实体/怪物的行为

一墙之隔,就是一只正常的鸡鸡菌,它是我用刷怪蛋刷出来的。它拥有健全的大脑,它会监视离它太近的单位(玩家)。有玻璃挡着,它们安然相处。

Bukkit 如何自定义实体/怪物的行为

而当我把玻璃破坏一个口,牛牛菌疯一般的跳过来,然后几脚就把这只可怜的鸡鸡菌踩死了。(如图鸡受到伤害)

很棒,我们实现了牛牛菌的大脑重制。

这对实现多样化的怪物AI有极大的帮助。

设想一下,我们可以让僵尸拥有更高的智慧:
他们可以有仇恨值,可以召唤其他僵尸到来。可以指挥其他僵尸攻击某一个单位。
而动物们,也不再是任人宰杀了,他们可以反击,追击和远远的逃跑。
而BOSS也不只是末影龙和凋零了,有了强大的AI系统,任何一个生物,哪怕是小鸡,也可以成为一个恐怖的,拥有强大技能和AI的终极BOSS。

四、区块卸载重加载时,怪物的自定义行为就会被清空。

原因就是当区块卸载时,怪物的数据就会以NBT的形式保存在Chunk区块文件中,而我们自定义的行为并不在保存的数据之列,也就被理所当然的刷了下去。当区块再加载,怪物从区块文件中读取出来并重新实例化时,我们的自定义行为就消失了。

我不确定这个问题是否和版本有关,我在1.9.4版本下进行的测试。

为了解决这个问题,我们有几种解决方案:
1、修改Entity的NBT序列化过程,把我们的自定义行为写到数据文件中。
2、记录Entity的UUID作为唯一标识,将自定义行为存储在其他地方,当怪物重新被加载时,重新将自定义行为写到怪物身上。

看上去显然第一条比较复杂,修改服务器端需要很大的功夫,也比较麻烦。现在采用第二种方案来修复这个BUG。

通过ChunkLoadEvent来监听区块被加载的事件,然后对这个区块上所有实体进行检测。需要注意的是,ChunkLoadEvent这个事件是异步的,你在监听到事件后马上getEntities()是只会返回空的,所以你需要延迟一段时间再获取,比如:

Bukkit.getScheduler().scheduleSyncDelayedTask(Plugin,Runnable,40L);

我们获取了刚加载的所有怪物的列表,然后依次判断他们的UUID是否在我们的插件数据库中有记录,如果有记录,根据记录对其的AI进行设置。

如果觉得好麻烦,请使用下面这种方法。下面这个就不会遇到这种问题。


五、使用反射更*化的操控AI

“这个方法因为原理不同,所以不会遇到上面第四节所说的BUG”


其实还有一种更彻底的操控方法,就是替换掉Minecraft原生的AI操作类,替换成我们的AI类。
EntityTypes(EntityList)中定义了所有实体的类型、ID和对应的处理类。我们通过反射来篡改Minecraft注册的类,来替换成我们的类。

这个方法虽然复杂一些,但是更*。

Bukkit 如何自定义实体/怪物的行为

如上图,通过直接获取EntityTypes的Field进行修改来达成目的。

我修改了实体列表中的EntityPig类为我自己的Pig类。

Bukkit 如何自定义实体/怪物的行为

我自己写的Pig类继承了EntityPig这个原生的AI类,这样一来我们只需要修改想改变动的部分就可以了。

在我创建的Pig类中,在其生成的时候输出一句话来证明实体猪的创建,最终实例化的是这一个类。
(这里忘记运行截图了)

注解

EntityTypes = EntityList
它们是同一个类,但是MCP和CraftBukkit反混淆时给他们起了不同的名字
由于对Minecraft源码的研究我是通过MCP来完成的,CraftBukkit的反混淆不彻底无法进行研究。故在本文中出现了MCP反混淆时命名的类名
他们和CraftBukkit的命名有明显的插件,故作此解释:
本文中的实体列表类,在MCP的反混淆中叫做EntityAITasks,在CraftBukkit的反混淆中叫做PathfinderGoalSelector。实体AI处理器在MCP中被命名为EntityAIXXXX,而在CraftBukkit中则被命名为 PathfinderGoalXXXX。他们的意思都是一样的。同样的,EntityList(MCP)和EntityTypes(CraftBukkit)也是这样的。