創建一個魔獸RPG的AI系統
譯文
這篇文章將幫助你製作一個簡單但是十分酷的英雄對戰地圖的人工智慧。
這個你將學習的人工智慧系統不是非常完美。我們將創建的是一個可以攻擊其它英雄、可以自己揀物品、學習和使用技能的人工智慧系統,但是還是無法與人類玩家相比。
但是,當你學習了基礎的知識以後你應該可以自己改進它。
前提需要
JASS基礎
這篇文章使用JASS來製作示例,所以你必須瞭解JASS。在理論上它也可以在T中做出來,但是我不推薦那樣做,因為用T來製作可能導致記憶體洩露、大量不必要的代碼以及在T中是無法使用JASS的返回值BUG和遊戲緩存系統的。如果你不熟悉JASS,請預先補充一下你自己的JASS知識。你同樣必須知道什麼是代碼行,如果你不知道的話,請補充自己的知識。
基於遊戲緩存以及返回值BUG的系統
演示地圖 : 一個演示地圖,很重要,因為我的文章中很多處引用了裡面的代碼。
注意事項
■我們將要製作的AI系統達不到人類的水準,但是比什麼都沒有強。而且我認為當你理解了基礎以後可以自己改進它。
■你不用完全按照我說的做;我按做我的想法做,但是如果你的想法更好或者你覺得自己的做法更舒服,請按照你自己的想法做。我並不完美,這篇文章也不可能完美,但是我希望它可以對你有所幫助。
■你可以使用在我的演示地圖裡面的AI系統而不自己動手(如果你那樣做了,請告訴我一聲),但是我建議你自己動手寫,因為地圖可能很複雜而且你可以自己動手寫一個AI系統中學到更多的知識。
■這個演示地圖可能有BUG,而且它也並不是一個好玩的地圖。請記住它只是一個展示AI系統的演示地圖,如果你想觀賞更好的有AI的地圖,試試下面這個地圖。
初始化部分
首先在WE中創建一個觸發條件為"玩家1-玩家1(紅色)離開遊戲"的觸發器,然後把它轉換為JASS。我們需要這個觸發器來監視玩家離開遊戲,那樣我們才能為這個玩家開啟人工智慧。現在它只監視一號玩家離開遊戲,所以我們在正式地圖中需要使用一個迴圈來監視從0-11號的玩家。
我們希望這個AI系統可以使用技能。聽起來似乎很難,其實很簡單。我們只需要使英雄學習技能,那麼他們就可以自己使用。
注意:電腦控制的英雄釋放自訂技能的情況總是和它釋放這個自訂技能的基礎技能的情況相同(這裡翻譯的有點含糊不清,自訂技能的基礎技能的意思是….基礎技能是遊戲本身帶有的技能,自訂技能都是以某個基礎技能為基礎的…這樣說做過圖的大大應該可以明白吧?).所以如果你的自訂技能是以沉默為基礎技能的,電腦控制的英雄就會在對戰地圖中應該使用沉默的情況使用這個技能。千萬不要將技能以"通魔(Channel)"為基礎,因為電腦從來不會使用它們,即使改變技能的OrderString也沒有什麼用。
為了知道每個英雄都擁有什麼技能,我們創建了一個遊戲緩存(game cache)來保存它。
在演示地圖中我的觸發器在地圖的初始化部分創建了一個遊戲緩存並將它保存在全域變數 udg_GameCache 中。需要注意的是緩存必須在我們使用它之前初始化,所以我在地圖的初始化時間中創建了它。
在我的地圖中我寫了一個函數SetupSkills.在這個AI觸發器的InitTrig函數中我使用了庫函數ExecuteFunc來開啟另外一個執行緒執行這個函數。這是為了防止地圖的初始化時間太長。
我的SetupSkills函數如下
function SetupSkills takes nothing returns nothing
local string h // Create a local string variable
// Paladin // Here we’ll initialise the Paladin’s skills, repeat this for all other heroes
set h = UnitId2String('Hpal') // Store the returned value of UnitId2String(‘Hpal’) in the local
call StoreInteger(udg_GameCache, h, "BaseSkill1", 'AHhb') // One of his base skills is Holy Light, store it as “BaseSkill1”
call StoreInteger(udg_GameCache, h, "BaseSkill2", 'AHds') // Store Divine Shield as “BaseSkill2”
call StoreInteger(udg_GameCache, h, "BaseSkill3", 'AHad') // Store Devotion Aura as “BaseSkill3”
call StoreInteger(udg_GameCache, h, "UltimateSkill", 'AHre') // Store Resurrection as his “UltimateSkill”
… // Repeat for each Hero.
endfunction
接著是我的AI觸發器的InitTrig部分
function InitTrig_AI takes nothing returns nothing
local integer i = 0
set gg_trg_AI = CreateTrigger( )
loop
exitwhen i > 11
call TriggerRegisterPlayerEventLeave( gg_trg_AI, Player(i) )
set i = i + 1
endloop
call TriggerAddAction( gg_trg_AI, function PlayerLeaves )
call ExecuteFunc("SetupSkills")
endfunction
為英雄開啟AI系統
為了控制AI我們使用了一個計時器(timer).我寫了一個函數StartAI來獲取一個單位的類型:英雄(請在演示地圖中查看這個函數)。這個函數只是創建一個計時器,並且"綁定"在這個英雄身上,並且開啟這個計時器。
這是演示地圖中的空的AILoop函數和StartAI函數(這裡給的只是一個框架,等下我們將展示一些動作函數,但是你起碼必須先把function和endfunction寫上去以保證WE不報錯) :
function AILoop takes nothing returns nothing
endfunction
function StartAI takes unit hero returns nothing
local timer m = CreateTimer()
call AttachObject(m, "hero", hero)
call TimerStart(m, 0, false, function AILoop)
set m = null
endfunction
注意:我的這個StartAI函數通過將periodic參數設置為false來達到使計時器只執行一次的目的(以後我們還會來討論它的).
現在,你就可以在你的英雄選擇系統中當由電腦控制的玩家選擇英雄時調用這個函數,並且在玩家離開遊戲的時候執行這個函數。檢測玩家是否擁有一個英雄,如果它擁有,調用這個函數來開啟那個英雄的AI系統。 例如:
function PlayerLeaves takes nothing returns nothing
local player p = GetTriggerPlayer()
call DisplayTextToForce(bj_FORCE_ALL_PLAYERS, GetPlayerName(p)+" has left the game.")
if udg_Hero[GetPlayerId(p)] != null then
call StartAI(udg_Hero[GetPlayerId(p)])
endif
set p = null
endfunction
注意:這個函數將使AI系統控制離開的玩家的英雄,但是這也不是必要的,你也可以做別的事情。
令這個AI做些什麼
當計時器終止的時候我們希望它做了這些事情:
■如果英雄死亡,等待他復活。
■如果英雄將要死亡,命令他移動到地圖中心的生命泉水。
■如果英雄狀態良好,檢測是否有敵人在附近。如果有,則命令英雄攻擊它。否則就檢測是否有物品在附近,如果有的話,發送一個巧妙的命令讓英雄揀起它。然後命令英雄巡邏到地圖的一個隨機座標。
■如果英雄是活著的而且有未使用的技能點,學習一個技能。
我們由變數的聲明開始。注意在我函數裡面的實變數"e",它定義了在計時器再次啟動前所經過的時間,這樣我們就可以在英雄死亡的時候等待短一點的時間,而在他攻擊的時候等待長一點的時間。這個變數初始化值為5。
區域變數的聲明:
function AILoop takes nothing returns nothing
local string a = GetAttachmentTable(GetExpiredTimer())
local unit h = GetTableUnit(a, "hero")
local rect i
local location r
local real x = GetUnitX(h)
local real y = GetUnitY(h)
local group g
local boolexpr b
local boolexpr be
local unit f
local string o = OrderId2String(GetUnitCurrentOrder(h))
local real l = GetUnitState(h, UNIT_STATE_LIFE)
local real e = 5
…
--------------------------------------------------------------------------------
我們由檢測英雄是否死亡開始,如果他死亡了,設置"e"為1.5(因為在復活以後等待5秒的時間太長了,我們並不想這樣).
當英雄的生命值"l"為0時,設置"e"為1.5來使計時器更加頻繁的檢測英雄是否復活.
if l <= 0 then
set e = 1.5
endif
--------------------------------------------------------------------------------
接著我檢測英雄的生命是否低於最大生命值的20%.如果是的,命令英雄移動到生命泉並且設置"e"為3. 當英雄的生命值少於最大生命值的20%時,命令英雄移動到生命泉的位置。
if l < GetUnitState(h, UNIT_STATE_MAX_LIFE)/5 then
call IssuePointOrder(h, "move", GetUnitX(gg_unit_nfoh_0001), GetUnitY(gg_unit_nfoh_0001))
set e = 3
|