Bukkit 如何自定義實體/怪物的行爲

在Bukkit裏,如何自定義實體、怪物的行爲呢?

本文用魔改牛牛君爲例,介紹了兩種如何怪物、生物的行爲AI的方法。


一、設想效果


在我設想的效果中,自定義的怪物是多種多樣的。同一個怪物模型,不同的怪物等級,擁有不同的AI。舉個簡單的例子。

普通殭屍:見到玩家會追擊和攻擊
懶惰的殭屍:見到玩家不會主動追擊,除非玩家攻擊他。
膽小的殭屍:見到玩家就會逃跑。
變異的殭屍:不懼怕太陽照射。

以上的這幾個殭屍例子,就是使用同一個怪物模型(殭屍),不同的AI實現。
而要達成這個想法,使用我翻譯的那篇文章顯然是不可能的。

在我們自定義怪物的時候,只能自定義已有怪物的AI,而不能修改他們的材質或模型,因爲那樣的話客戶端無法正常顯示。

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


爲此,我專門翻閱了Entity在底層中是如何工作的。
簡單來說,Entity包含很多種,而我們的怪物或者是動物的父類是LivingEntity(活性實體)
而LivingEntity之中包含了該實體的AI部分處理。

Entity Living AI Task

在上圖中,tasks和targetTasks都是實體的AI表,而每個AI表都存在多個AI處理器。

Zombie AI

如圖是實體殭屍的所有AI,定義了殭屍會游泳, 攻擊,監視附近的玩家,以及主動攻擊玩家,村民和鐵傀儡。首先攻擊傷害自己的單位。
而我們的工作就是把這些AI全部修改掉,然後修改成我們自定義的AI。

大約就是 entity.tasks.clear() 和 entity.targetTasks.clear() 這麼簡單。

每個具有AI的實體都是由World來進行刷新的(每Tick一次 / Tick = 1/20s)

三、魔改牛牛君


衆所周知,牛牛菌屬於動物,是不會攻擊任何生物/怪物 或玩家的。而我們要做的是一個瘋狂的牛牛菌,他攻擊雞雞菌。

爲了簡化,我簡單寫了一個插件,然後註冊了一個命令叫做mobs,下面我開始onCommand裏魔改牛牛菌:

Cow

只有簡單的幾行,我們就可以創建出一個獨一無二的,擁有獨立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,結果是牛追擊雞,但是不會造成傷害。


第五步、開服測試

Test1

首先我通過指令mobs生成了一個瘋狂的牛牛菌,它現在是半腦殘狀態,因爲我移除了它自由移動的AI,所以它只會站着不動。面對我們的鏡頭,一點也不緊張。

Test2

一牆之隔,就是一隻正常的雞雞菌,它是我用刷怪蛋刷出來的。它擁有健全的大腦,它會監視離它太近的單位(玩家)。有玻璃擋着,它們安然相處。

Test3

而當我把玻璃破壞一個口,牛牛菌瘋一般的跳過來,然後幾腳就把這隻可憐的雞雞菌踩死了。(如圖雞受到傷害)

很棒,我們實現了牛牛菌的大腦重製。

這對實現多樣化的怪物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註冊的類,來替換成我們的類。

這個方法雖然複雜一些,但是更自由。

1

如上圖,通過直接獲取EntityTypes的Field進行修改來達成目的。

我修改了實體列表中的EntityPig類爲我自己的Pig類。

2

我自己寫的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)也是這樣的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章