使用Unity製作遊戲AI

遊資網發表於2019-10-15
本文由獨立遊戲工作室Synnaxium Studio介紹遊戲AI的概念和開發方法。本文中所有內容都是他們在開發《Radiant Blade》遊戲的原型階段所積累的經驗。

下面是《Radiant Blade》的演示畫面。

使用遊戲AI的原因

首先,我們要思考為什麼要給遊戲新增AI?

長期以來,我們都在幻想著為遊戲開發令人驚奇的AI,讓AI給玩家帶來印象深刻的體驗。這種AI可以預料到玩家的每一個操作,幾乎無法被打敗。但說實話,這種AI毫無對抗的樂趣。

值得玩家去玩的遊戲應該是玩家可以獲得樂趣的遊戲。因此,AI必須可以和玩家旗鼓相當。AI可以作為夥伴,讓玩家通過特別的方法進行互動。

顯然,只有樂趣的遊戲不會是一個優秀的遊戲。遊戲也必須有炫酷的機制,深刻的含義以及精美的外觀。但對AI而言,我們希望AI具有娛樂性,因此我們要進一步縮小這個概念的範圍。

遊戲設計

遊戲中的娛樂性是什麼?我們的開發團隊花了一些時間思考這個問題,結論可以總結為一個詞:學習,具備娛樂性的遊戲是玩家可以從中學習和利用知識的遊戲。

娛樂性源於小小的好奇心,在玩家看到新事物時,好奇心會佔據玩家的頭腦,並會不斷增長,直到玩家完全理解這項新事物。也就是說,具有娛樂性的AI必須是可以被玩家學習的。

這個簡單的概念形成了所有遊戲中AI的廣泛解讀,包括:《超級瑪麗》、《毀滅戰士》、《魔獸世界》和《以撒的結合》。如果分析這些遊戲的AI,我們會發現它們都是可以預測的。

由於加入了一些隨機元素,這些遊戲AI不是完全固定不變的,但仍有預測的可能。這樣又出現了另一個問題:如何製作出可預測的遊戲AI?

答案很簡單:使用狀態機

狀態機

狀態機是包含狀態和過渡的數學工具。

使用Unity製作遊戲AI
基本的狀態機

在確定性狀態機中,我們會處於一個特定狀態,在移動時,我們會隨著其中一個可用過渡轉變到新狀態。過渡可能會受到條件限制,例如:只有在擁有特定法術時,AI才可以到達指定狀態。

狀態機的優點是:它們具有表現力和可預測性。假設狀態包括“攻擊”、“受擊”、“奔跑至目標”和“逃跑”,我們可以使用一些過渡,建立出模擬AI基本行為的狀態機。

下面是簡單的AI示例。

使用Unity製作遊戲AI

我們製作開發的AI可以用下面三句話描述:

如果AI的生命值在10%以下時,它就會逃跑。

AI可以受到攻擊。

玩家處在AI範圍內時,AI會向玩家跑去,然後攻擊玩家。

這意味著我們的AI很簡單。如果無法簡單地描述自己設計的AI,那麼我們可能需要對自己的AI做進一步思考。

狀態機與Unity

我們在Unity使用狀態機大致的方法有三種:

  • 自己開發。
  • 使用Animator實現。
  • 從Asset Store資源商店獲取相應資源。


狀態機是遊戲中很常見的工具,所以我們不建議開發者自己編寫程式碼開發狀態機。除非開發者希望學習如何通過編碼實現狀態機,否則我們可以獲取可直接使用的狀態機。

第二種方法是使用Unity的內建Animator功能,它其實是一種可以播放動畫的狀態機。但在Animator中,我們不一定要使用動畫,如果不使用動畫的話,它的工作方式和狀態機一樣。

Animator使用起來快捷而直觀。下圖是《Radiant Blade》中使用Unity Animator實現的弓箭手AI。

使用Unity製作遊戲AI

第三種方法是從Asset Store資源商店獲取相關資源,不少資源有和Animator一樣不錯的效果。


Animator

或許你使用過Animator在Unity中實現標準動畫,但我們會根據需求調整一些方法。

狀態

通常,Animator的狀態包含動畫。我們沒有這樣使用,而是把狀態關聯到描述行為的程式碼。

為了展示這個方法,我們現在檢視定義弓箭手的遊戲物件。Behaviours物件的子物件是AI行為,它們其實是小型控制器,在對應狀態啟用時,它們會控制弓箭手。

在Shoot狀態啟用時,會在弓箭手上使用Shoot Behaviour指令碼。

使用Unity製作遊戲AI

這是基於狀態的物件。在完成行為後,Shoot Behaviour會通知Animator。Animator內建的藍色進度條可能會讓人迷惑,但它只在外觀上起到作用。

變數

我們的AI設計是響應式系統,它會隨條件而變化,條件是玩家和環境。

Animator的變數用於描述遊戲的狀態,以及作出已知決策。下圖是弓箭手使用的變數,它們描述了形成AI的所有要素。

使用Unity製作遊戲AI

在以傳統方法使用Animator時,大多數狀態過渡會隨著關聯動畫結束而結束。對於AI來說,狀態就是行為,它會在未定義的時間內儲存遊戲邏輯。

我們使用了兩個變數behaviour_ended和behaviour_error,作用是通知狀態的結束。它們是狀態的輸出結果,表示狀態成功結束,或是出現錯誤。

過渡

過渡定義了AI行為的改變過程。例如,過渡可以表示:當AI完成向目標行走的過程後,它應該要做什麼。

下圖是示例過渡:如果目標在近戰範圍內,AI會進行攻擊。

使用Unity製作遊戲AI

對Unity的Animator,有些開發者可能不知道的是:過渡是有先後順序的。特定過渡會被首先評估,僅在它的相關條件為假時,第二個過渡才會進行評估。

如下圖所示,選中Neutral狀態時,我們可以檢視過渡的優先順序。

使用Unity製作遊戲AI

這項功能很不錯,它允許我們把AI設計為中心大腦,根據優先順序來作出合適的選擇。

在我們的弓箭手AI中,需要注意AI的順序和中心部分。Neutral節點是決策中心,它的主要工作過程如下:

  • 如果沒有玩家的話,AI停止戰鬥;
  • 如果玩家距離較遠,AI向玩家移動,進入射擊範圍;
  • 如果玩家不在AI的視線方向,AI向玩家移動,從而能夠進行射擊;
  • 如果處於近戰範圍,則進行近戰攻擊;
  • 如果玩家過於接近AI,AI可能會向後退;
  • AI有可能隨機改變和玩家的方向;
  • AI會向玩家射擊。


該功能的好處是每個單獨的過渡都非常簡單:過渡會歸結為一次測試,或甚至不進行測試。使用後續過渡的前提是之前的過渡條件必須為假。

實現方法

現在,我們開始瞭解具體操作。你應該會注意到,我們還未提供過任何相關程式碼。

這意味著我們的框架有足夠高的抽象級,不必處理任何技術細節,就可以很好進行解釋。在程式碼部分完成後,設計AI的過程非常直觀。

我們需要什麼

下面是實現AI的三個任務:

編寫AI行為。

將Animator和可用行為關聯。

為Animator更新遊戲相關變數的列表。

行為

開始處理前,回顧行為的功能:

行為會和遊戲的角色控制器配合使用。

行為可以被識別。

行為可以被啟用。

行為可以成功完成。

行為也可以出現錯誤。

行為可以被中斷。

現在我們知道了想要的功能,我們要把它編寫為API。

  1. public abstract class AbstractAIBehaviour : MonoBehaviour {

  2.     // 角色由行為控制
  3.     [SerializeField]
  4.     protected CharController charController;

  5.     // 必須返回對應行為的Animator狀態的短雜湊值。
  6.     abstract public int GetBehaviourHash();

  7.     // 在行為成功結束時呼叫的事件。
  8.     public event Action OnBehaviourEnded;

  9.     // 在行為失敗時,要呼叫的事件
  10.     public event Action OnBehaviourError;

  11.     // OnDisable()
  12.     // enable = true/false;
  13. }
複製程式碼

對於啟用和禁用部分,我們會利用Unity的內建方法,這裡不必自己編寫方法。我們會使用簡潔的API。

對於識別符,我們建立了帶有特殊名稱的方法:GetBehaviourHash。因為Animator狀態的識別方式是:使用狀態的識別符號,也就是其名稱的雜湊值。

因此對於Shoot狀態,對應的識別符號是Animator.StringToHash(“Shoot”)。

為了弄清楚物件,避免再次計算相同的雜湊值,我們可以把它們儲存為靜態變數:

  1. /**
  2. *該類是預計算雜湊值的佔位符。
  3. * 目的是建立Animator狀態名稱和AI行為之間的關聯。
  4. * 下面定義的整數應該用於GetBehaviourHash中繼承自AbstractAIBehaviour的類。
  5. */
  6. public class BehaviourHashes {

  7.     //該行為會讓角色向目標移動。
  8.     static public readonly int OBJ_MOVETO_STATE = Animator.StringToHash("Obj MoveTo");

  9.     // 該行為會讓角色什麼都不做。
  10.     static public readonly int IDLE_STATE = Animator.StringToHash("Idle");

  11.     // 此時角色會漫無目的地四處移動。
  12.     static public readonly int ROAM_STATE = Animator.StringToHash("Roam");
  13.    
  14.     // ...
  15. }
複製程式碼

考慮到這點,AbstractAIBehaviour的實現程式碼如下。

  1.   // 必須返回對應行為的Animator狀態的短雜湊值。
  2.     public override int GetBehaviourHash()
  3.     {
  4.         // State name in the Animator is “Idle”
  5.         // Animator中的狀態名稱為Idle。
  6.         return BehaviourHashes.IDLE_STATE;
  7.     }
複製程式碼

我們將每個雜湊值存到對應的指令碼中,因此ROAM_STATE可以儲存在RoamBehaviour類中。

唯一的問題是,由於我們暗中把每個行為關聯到名稱,因此開啟每個行為類收集Animator狀態的授權名稱可能很麻煩。

現在,我們的工作是為真實行為編寫實際的程式碼,我們需要做的是實現AbstractAIBehaviour的子類。

關聯行為和Animator

我們的AI的行為可以被識別、監聽、啟用和禁用,現在我們要利用行為。

我們從控制器開始。由於我們有多個彼此獨立的實體,因此我們需要同步它們,從而實現流暢的工作效果。該控制器的目的是確保每次只啟用一個行為,並提供修改當前行為的切入點。

一些開發者可能不知道應該何時給遊戲新增新控制器的類。好的習慣是把控制器看作用來同步多個較小功能的程式碼。

  1. /**
  2. * AIBehaviourController應該關聯AI的Animator和相應行為。
  3. */
  4. public class AIBehaviourController

  5.     /**
  6.      * 這部分包含可用行為
  7.      *
  8.      * 行為的關鍵是GetBehaviourHash方法返回的數值
  9.      */
  10.     protected Dictionary<int, AbstractAIBehaviour> behaviours = new Dictionary<int, AbstractAIBehaviour>();

  11.     // AI的Animator
  12.     private Animator stateMachine;

  13.     // 該變數表示正在執行的行為
  14.     private AbstractAIBehaviour currentBehaviour;   
  15.    
  16.     // 下面是必須存在AI  Animator中的觸發器
  17.     public static readonly int BEHAVIOUR_ENDED = Animator.StringToHash("behaviour_ended");
  18.     public static readonly int BEHAVIOUR_ERROR = Animator.StringToHash("behaviour_error");

  19.     /**
  20.      * 強制某個行為中斷正在執行的行為
  21.      */
  22.     public void SetBehaviour(int behaviorHash)
  23.     {
  24.         // 安全地禁用當前行為
  25.         if (currentBehaviour)
  26.             currentBehaviour.enabled = false;

  27.         try
  28.         {
  29.             // 開始新的行為
  30.             currentBehaviour = behaviours[behaviorHash];
  31.             currentBehaviour.enabled = true;
  32.         }
  33.         catch (KeyNotFoundException)
  34.         {
  35.             currentBehaviour = null;
  36.         }
  37.     }

  38.     void Awake()
  39.     {
  40.         stateMachine = GetComponent<Animator>();

  41.        // 對於每個子物件
  42.         foreach (AbstractAIBehaviour behaviour in GetComponentsInChildren<AbstractAIBehaviour>())
  43.         {
  44.             // 註冊行為
  45.             behaviours.Add(behaviour.GetBehaviourHash(), behaviour);

  46.             // 監聽行為
  47.             behaviour.OnBehaviourEnded += OnBehaviourEnded;
  48.             behaviour.OnBehaviourError += OnBehaviourError;
  49.         }
  50.     }

  51.     /**
  52.      * 在行為結束時,通知AI的Animator
  53.      */
  54.     private void OnBehaviourEnded()
  55.     {
  56.         stateMachine.SetTrigger(BEHAVIOUR_ENDED);
  57.     }

  58.     /**
  59.      * 在行為失敗時,通知AI的Animator
  60.      */
  61.     private void OnBehaviourError()
  62.     {
  63.         stateMachine.SetTrigger(BEHAVIOUR_ERROR);
  64.     }
  65. }
複製程式碼

這個類比較長,但是程式碼其實很簡單:

  • 字典包含我們已知的行為。
  • 方法可以啟用特定行為。
  • 兩個事件用於在行為結束時通知Animator。


有了切入點,我們可以把它和Animator連線起來。我們會使用一個不常用的功能:StateMachineBehaviour。

如下圖所示,選中Animator時,如果在空白處單擊左鍵,我們會聚焦Animator本身,並顯示Animator的隱藏檢視視窗。

使用Unity製作遊戲AI

StateMachineBehaviour允許我們向Animator插入自定義程式碼。我們會在Animator的狀態變化時,呼叫我們的AIBehaviourController。

  1. /**
  2. * 該類會插入AI的Animator。
  3. *
  4. * 它的唯一作用是監視Animator中的狀態轉換。
  5. */
  6. public class AIStateController : StateMachineBehaviour {
  7.    
  8.     /**
  9.      * 在Animator進入新狀態時,通知AI控制器。
  10.      */
  11.     override public void OnStateEnter(Animator animator, AnimatorStateInfo info, int layerIndex)
  12.     {
  13.         if (!animator.GetComponent<AIBehaviourController>().SetBehaviour(animatorStateInfo.shortNameHash))
  14.         {
  15.             // 如果狀態不存在,那麼把它設為決策中心。
  16.             // 強制Animator直接評估該狀態。
  17.             animator.Update(0f);
  18.         }
  19.     }
  20. }
複製程式碼

這些程式碼非常直觀,它會處理Unity的一個特別之處:Animator無法在每幀處理多個狀態,因此在我們遍歷決策中心時,會造成短暫的延遲。

幸運的是,解決方法很簡單,我們可以強行執行Update方法,強制Animator處理狀態。

通過使用我們的新類,我們可以把功能結合起來,只要把該指令碼新增到AI的Animator即可。現在進入新狀態時,我們的AI Animator會呼叫AIBehaviourController。

使用Unity製作遊戲AI

最後,我們在框架中包含三個類,子類以及一個角色控制器,它們包含著實際的遊戲邏輯。

下圖是一個組合成AI框架的小型類圖示。

使用Unity製作遊戲AI

處理遊戲邏輯

總而言之,技術解決方案可以總結為三個類,每個類都非常簡潔。

我們還需要什麼呢?當然是遊戲本身了。但這個部分必須由開發者自己製作,實現自己的AI需要的內容如下:

一個角色控制器,負責角色和其渲染的實際邏輯。

變數以及讓變數與Animator保持同步的程式碼。

自定義行為,例如:攻擊,移動。

此時我們要處理的都是常見的Unity標準程式碼。

使用Unity製作遊戲AI

小結

如何在Unity中製作遊戲AI的方法為大家介紹到這裡,行動起來,在你的遊戲中,實現自定義行為的AI吧。

作者:Synnaxium Studio  
來源:Unity官方平臺
原地址:https://mp.weixin.qq.com/s/hkBmBAn7YMOnfjLygz-4Jw

相關文章