這篇文章將幫助你製作一個簡單但是十分酷的英雄對戰地圖的人工智慧。
這個你將學習的人工智慧系統不是非常完美。我們將創建的是一個可以攻擊其它英雄、可以自己揀物品、學習和使用技能的人工智慧系統,但是還是無法與人類玩家相比。
但是,當你學習了基礎的知識以後你應該可以自己改進它。
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來開啟另外一個執行緒執行這個函數。這是為了防止地圖的初始化時間太長。
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
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我們使用了一個計時器(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系統控制離開的玩家的英雄,但是這也不是必要的,你也可以做別的事情。
當計時器終止的時候我們希望它做了這些事情:
如果英雄死亡,等待他復活。
如果英雄將要死亡,命令他移動到地圖中心的生命泉水。
如果英雄狀態良好,檢測是否有敵人在附近。如果有,則命令英雄攻擊它。否則就檢測是否有物品在附近,如果有的話,發送一個巧妙的命令讓英雄揀起它。然後命令英雄巡邏到地圖的一個隨機座標。
如果英雄是活著的而且有未使用的技能點,學習一個技能。
我們由變數的聲明開始。注意在我函數裡面的實變數"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
如果英雄的狀態良好,檢測他是否處在一個普通命令中(防止它打斷了通魔技能).如果是一個標準命令,我們再檢測在500的半徑內是否有敵人存在.如果存在敵人,簡單的發出一個攻擊命令(不要改變"e"的值,5秒對於這個情況剛剛好).
function AIFilterEnemyConditions takes nothing returns boolean return GetUnitState(GetFilterUnit(), UNIT_STATE_LIFE) > 0 and IsPlayerEnemy(GetOwningPlayer(GetFilterUnit()), GetOwningPlayer(GetAttachedUnit(GetExpiredTimer(), "hero"))) endfunction … else if ((o == "smart") or (o == "attack") or (o == "patrol") or (o == "move") or (o == "stop") or (o == "hold") or (o == null)) then set g = CreateGroup() set b = Condition(function AIFilterEnemyConditions) call GroupEnumUnitsInRange(g, x, y, 500, b) set f = FirstOfGroup(g) if f == null then … else call IssueTargetOrder(h, "attack", f) endif call DestroyGroup(g) call DestroyBoolExpr(b) endif …
如果沒有敵人存在,再檢測物品.如果發現物品,再檢測是否為一個提升狀態的物品.如果不是,檢測英雄物品欄是否有空欄,有的話就命令英雄將它揀起來.
function AISetItem takes nothing returns nothing set bj_lastRemovedItem=GetEnumItem() endfunction function AIItemFilter takes nothing returns boolean return IsItemVisible(GetFilterItem()) and GetWidgetLife(GetFilterItem()) > 0 endfunction function AIHasEmptyInventorySlot takes unit u returns boolean return UnitItemInSlot(u, 0) == null or UnitItemInSlot(u, 1) == null or UnitItemInSlot(u, 2) == null or UnitItemInSlot(u, 3) == null or UnitItemInSlot(u, 4) == null or UnitItemInSlot(u, 5) == null endfunction … if f == null then set i = Rect(x-800, y-800, x+800, y+800) set be = Condition(function AIItemFilter) set bj_lastRemovedItem=null call EnumItemsInRect(i, be, function AISetItem) if bj_lastRemovedItem != null and (GetItemType(bj_lastRemovedItem) == ITEM_TYPE_POWERUP or AIHasEmptyInventorySlot(h)) then call IssueTargetOrder(h, "smart", bj_lastRemovedItem) else … endif call RemoveRect(i) call DestroyBoolExpr(be) …
如果物品欄沒有空位,或者沒有發現物品,則命令英雄到一個隨機地點尋找新的目標.
… else set r = GetRandomLocInRect(bj_mapInitialPlayableArea) call IssuePointOrderLoc(h, "patrol", r) call RemoveLocation(r) …
現在我們需要檢測的是英雄是否有未使用的技能點(將這個函數與進攻/揀取物品/前進到隨機地點等模組分開). 如果英雄有未使用的技能點,調用函數來使英雄學習技能.在我的演示地圖中,我是用一個函數來保存將要讓英雄學習的技能的,使用的是下面這個模式:
function AILearnSkill takes unit h, string a returns nothing local integer i = GetTableInt(a, "LearnSkillOrder")+1 if i == 1 or i == 4 or i == 8 then call SelectHeroSkill(h, GetStoredInteger(udg_GameCache, UnitId2String(GetUnitTypeId(h)), "BaseSkill1")) elseif i == 2 or i == 5 or i == 9 then call SelectHeroSkill(h, GetStoredInteger(udg_GameCache, UnitId2String(GetUnitTypeId(h)), "BaseSkill2")) elseif i == 3 or i == 7 or i == 10 then call SelectHeroSkill(h, GetStoredInteger(udg_GameCache, UnitId2String(GetUnitTypeId(h)), "BaseSkill3")) elseif i == 6 then call SelectHeroSkill(h, GetStoredInteger(udg_GameCache, UnitId2String(GetUnitTypeId(h)), "UltimateSkill")) endif call SetTableInt(a, "LearnSkillOrder", i) endfunction … if GetHeroSkillPoints(h) > 0 and l > 0 then call AILearnSkill(h, a) endif …
現在所需要做的是使計時器在"e"秒之後再次開啟:
… call TimerStart(GetExpiredTimer(), e, true, function AILoop) …
最後我們將區域變數設置為空:
… set h = null set i = null set r = null set g = null set b = null set f = null set be = null …
這些就是英雄AI系統的基礎,它並不完美,但是它可以做為你的起點. 這個系統一點都不複雜,但是我希望你能對照我的演示地圖看,這樣可以讓你更加徹底的明白我的意思. 當你完成了一個屬於你自己的AI系統時,嘗試一下在你的系統中加入一個或者多個以下特徵:
嘗試使它可以尋找周圍最虛弱的敵人.
嘗試在殺死特殊的敵人時讓不同的AI玩家合作.
當大部分戰鬥都以生命泉為中心的時候,讓英雄離開生命泉.
讓AI玩家根據情況的不同說出不同的話(比如在殺死你的時候AI玩家會說"死吧~可憐的孩子")
我希望這篇文章可以使你們有所收穫.
討論區