Unity 中用有限狀態機來實現一個 AI

貓冬發表於2018-06-26

最近在閱讀《遊戲人工智慧程式設計案例精粹(修訂版)》,本文是書中第二章的一篇筆記。

有限狀態機(英語:Finite-state machine, 縮寫:FSM),是一個被數學家用來解決問題的嚴格形式化的裝置,在遊戲業中也常見有限狀態機的身影。

對於遊戲程式設計師來說,可以用下面這個定義來了解:

一個有限狀態機是一個裝置,或是一個裝置模型。具有有限數量的狀態,它可以在任何給定的時間根據輸入進行操作,是的從一個狀態變換到另一個狀態,或者是促使一個輸出或者一種行為的發生。一個有限狀態機在任何瞬間只能處在一種狀態。 ——《遊戲人工智慧程式設計案例精粹(修訂版)》 Mat Buckland

有限狀態機就是要把一個物件的行為分解成易於處理的“塊”或者狀態。拿某個開關來說,我們可以把它分成兩個狀態:開或關。其中開開關這個操作,就是一次狀態轉移,使開關的狀態從“關”變換到“開”,反之亦然。

拿遊戲來舉例,一個 FPS 遊戲中的敵人 AI 狀態可以分成:巡邏、偵查(聽到了玩家)、追逐(玩家出現在 AI 視野)、攻擊(玩家進入 AI 攻擊範圍)、死亡等,這些狀態都互相獨立有限的,且要滿足某種條件才能從一個狀態轉移到另外一個狀態。

下圖是隻有三種狀態的 AI 的有限狀態機圖示:

優缺點

實現有限狀態機之前,要先了解它的優點:

  1. 程式設計快速簡單:很多有限狀態機的實現都較簡單,本文會列出三種實現方法。
  2. 易於除錯:因為行為被分成單一的狀態塊,因此要除錯的時候,可以只跟蹤某個異常狀態的程式碼。
  3. 很少的計算開銷:幾乎不佔用珍貴的處理器時間,因為除了 if-this-then-that 這種思考處理之外,是不存在真正的“思考”的。
  4. 直覺性:人們總是自然地把事物思考為處在一種或另一種狀態。人類並不是像有限狀態機一樣工作,但我們發現這種方式下考慮行為是很有用的,或者說我們能更好更容易地進行 AI 狀態的分解和建立操作 AI 的規則,容易理解的概念也讓程式設計師之間能更好地交流其設計。
  5. 靈活性:遊戲 AI 的有限狀態機能很容易地由程式設計師進行調整,增添新的狀態和規則也很容易擴充套件一個 AI 的行為。

有限狀態機的缺點是: 1. 當狀態過多時,難以維護程式碼。 2. 《AI Game Development》的作者 Alex J. Champandard 發表過一篇文章《10 Reasons the Age of Finite State Machines is Over》

if-then 實現

這是第一種實現有限狀態機的方法,用一系列 if-then 語句或者 switch 語句來表達狀態。

下面拿那個只有三個狀態的jiangshi AI 舉例:

public enum ZombieState
{
    Chase, Attack, Die
}

public class Zombie : MonoBehaviour
{
    private ZombieState currentState;

    private void Update()
    {
        switch (currentState)
        {
            case ZombieState.Chase:
                if (currentHealth <= 0)
                {
                    ChangeState(ZombieState.Die);
                }
                // 玩家在攻擊範圍內則進入攻擊狀態
                if (PlayerInAttackRange())
                {
                    ChangeState(ZombieState.Attack);
                }
                break;
            case ZombieState.Attack:
                if (currentHealth <= 0)
                {
                    ChangeState(ZombieState.Die);
                }
                if (!PlayerInAttackRange())
                {
                    ChangeState(ZombieState.Chase);
                }
                break;
            case ZombieState.Die:
                Debug.Log("jiangshi死亡");
                break;
        }
    }
}

這種寫法能實現有限狀態機,但當遊戲物件複雜到一定程度時,case 就會變得特別多,使程式難以理解、除錯。另外這種寫法也不靈活,難以擴充套件超出它原始設定的範圍。

此外,我們常需要在進入狀態退出狀態時做些什麼,例如jiangshi在開始攻擊時像猩猩一樣錘幾下胸口,玩家跑出攻擊範圍的時候,jiangshi要“搖搖頭”讓自己清醒,好讓自己打起精神繼續追蹤玩家。

狀態變換表

一個用於組織狀態和影響狀態變換的更好的機制是一個狀態變換表

這表格可以被jiangshi AI 不間斷地查詢。使得它能基於從遊戲環境的變化來進行狀態變換。每個狀態可以模型化為一個分離的物件或者存在於 AI 外的函式。提供了一個清楚且靈活的結構。 我們只用告訴jiangshi它有多少個狀態,jiangshi則會根據自己獲得的資訊(例如玩家是否在它的攻擊範圍內)來處理規則(轉移狀態)。

public class Zombie : MonoBehaviour
{
    private ZombieState currentState;

    private void Update()
    {
        // 生命值小於等於0,進入死亡狀態
        if (currentHealth <= 0)
        {
            ChangeState(ZombieState.Die);
            return;
        }
        // 玩家在攻擊範圍內則進入攻擊狀態,反之進入追蹤狀態
        if (PlayerInAttackRange())
        {
            ChangeState(ZombieState.Attack);
        }
        else
        {
            ChangeState(ZombieState.Chase);
        }
    }
}

內建規則

另一種方法就是將狀態轉移規則內建到狀態內部。 在這裡,每一個狀態都是一個小模組,雖然每個模組都可以意識到其他模組的存在,但是每個模組都是一個獨立的單位,而且不依賴任何外部的邏輯來決定自己是否要進行狀態轉移。

public class Zombie : MonoBehaviour
{
    private State currentState;
    public int CurrentHealth { get; private set; }

    private void Update()
    {
        currentState.Execute(this);
    }

    public void ChangeState(State state)
    {
        currentState = state;
    }

    public bool PlayerInAttackRange()
    {
        // ...遊戲邏輯
        return result;
    }
}

public abstract class State
{
    public abstract void Execute(Zombie zombie);
}

public class ChaseState : State
{
    public override void Execute(Zombie zombie)
    {
        if (zombie.CurrentHealth <= 0)
        {
            zombie.ChangeState(new DieState());
        }

        if (zombie.PlayerInAttackRange())
        {
            zombie.ChangeState(new AttackState());
        }
    }
}

public class AttackState : State
{
    public override void Execute(Zombie zombie)
    {
        if (zombie.CurrentHealth <= 0)
        {
            zombie.ChangeState(new DieState());
        }

        if (!zombie.PlayerInAttackRange())
        {
            zombie.ChangeState(new ChaseState());
        }
    }
}

public class DieState : State
{
    public override void Execute(Zombie zombie)
    {
        Debug.Log("jiangshi死亡");
    }
}

Update() 函式只需要根據 currentState 來執行程式碼,當 currentState 改變時,下一次 Update() 的呼叫也會進行狀態轉移。這三個狀態都作為物件封裝,並且都給出了影響狀態轉移的規則(條件)。

這個結構被稱為狀態設計模式(state design pattern),它提供了一種優雅的方式來實現狀態驅動行為。這種實現編碼簡單,容易擴充套件,也可以容易地為狀態增加進入退出的動作。下文會給出更完整的實現。

West World 專案

這專案是關於使用有限狀態機建立一個 AI 的實際例子。遊戲環境是一個古老西部風格的開採金礦的小鎮,稱作 West World。一開始只有一個挖金礦工 Bob,後期會加入他的妻子。任何的狀態改變或者輸出都會出現在控制檯視窗中。West World 中有四個位置:金礦,可以存金塊的銀行,可以解除乾渴的酒吧,還有家。礦工 Bob 會挖礦、睡覺、喝酒等,但這些都由 Bob 的當前狀態決定。

專案在這裡:programming-game-ai-by-example-in-unity/WestWorld/

West World

當你看到礦工改變了位置時,就代表礦工改變了狀態,其他的事情都是狀態中發生的事情。

Base Game Entity 類

public abstract class BaseGameEntity
{
    /// <summary>
    /// 每個實體具有一個唯一的識別數字
    /// </summary>
    private int m_ID;

    /// <summary>
    /// 這是下一個有效的ID,每次 BaseGameEntity 被例項化這個值就被更新
    /// 這專案居民較少,採用預定義 id 的方式,可以忽視
    /// </summary>
    public static int m_iNextValidID { get; private set; }

    protected BaseGameEntity(int id)
    {
        m_ID = id;
    }

    public int ID
    {
        get { return m_ID; }
        set
        {
            m_ID = value;
            m_iNextValidID = m_ID + 1;
        }
    }
    // 在 GameManager 的 Update() 函式中呼叫,相當於實體自己的 Update 函式
    public abstract void EntityUpdate();
}

Miner 類

MIner 類是從 BaseGameEntity 類中繼承的,包含很多成員變數,程式碼如下:

public class Miner : BaseGameEntity
{
    /// <summary>
    /// 指向一個狀態例項的指標
    /// </summary>
    private State m_pCurrentState;

    /// <summary>
    /// 曠工當前所處的位置
    /// </summary>
    private LocationType m_Location;

    /// <summary>
    /// 曠工的包中裝了多少金塊
    /// </summary>
    private int m_iGoldCarried;

    /// <summary>
    /// 曠工在銀行存了多少金塊
    /// </summary>
    private int m_iMoneyInBank;

    /// <summary>
    /// 口渴程度,值越高,曠工越口渴
    /// </summary>
    private int m_iThirst;

    /// <summary>
    /// 疲倦程度,值越高,曠工越疲倦
    /// </summary>
    private int m_iFatigue;

    public Miner(int id) : base(id)
    {
        m_Location = LocationType.Shack;
        m_iGoldCarried = 0;
        m_iMoneyInBank = 0;
        m_iThirst = 0;
        m_iFatigue = 0;
        m_pCurrentState = GoHomeAndSleepTilRested.Instance;
    }

    /// <summary>
    /// 等於 Update 函式,在 GameManager 內被呼叫,每呼叫一次就變得越口渴
    /// </summary>
    public override void EntityUpdate()
    {
        m_iThirst += 1;
        m_pCurrentState.Execute(this);
    }
    // ...其他的程式碼看 Github 專案
}

Miner 狀態

金礦工人有四種狀態:

  • EnterMineAndDigForNugget:如果礦工沒在金礦,則改變位置。在金礦裡了,就挖掘金塊。
  • VisitBankAndDepositGold:礦工會走到銀行並且儲存他攜帶的所有天然金礦。
  • GoHomeAndSleepTilRested:礦工會回到他的小木屋睡覺知道他的疲勞值下降到可接受的程度。醒來繼續去挖礦。
  • QuenchThirst:去酒吧買一杯威士忌,不口渴了繼續挖礦。

再談狀態設計模式

之前提到要為狀態實現進入退出這兩個一個狀態只執行一次的邏輯,這樣可以增加有限狀態機的靈活性。下面是威力加強版:

public abstract class State
{
    /// <summary>
    /// 當狀態被進入時執行這個函式
    /// </summary>
    public abstract void Enter(Miner miner);

    /// <summary>
    /// 曠工更新狀態函式
    /// </summary>
    public abstract void Execute(Miner miner);

    /// <summary>
    /// 當狀態退出時執行這個函式
    /// </summary>
    public abstract void Exit(Miner miner);
}

這兩個增加的方法只有在礦工改變狀態時才會被呼叫。我們也需要修改 ChangeState 方法的程式碼如下:

public void ChangeState(State state)
{
// 執行上一個狀態的退出方法
    m_pCurrentState.Exit(this);
    // 更新狀態
    m_pCurrentState = state;
    // 執行當前狀態的進入方法
    m_pCurrentState.Enter(this);
}

另外,每個具體的狀態都新增了單例模式,這樣可以節省記憶體資源,不必重複分配和釋放記憶體給改變的狀態。以其中一個狀態為例子:

public class EnterMineAndDigForNugget : State
{
    public static EnterMineAndDigForNugget Instance { get; private set; }

    static EnterMineAndDigForNugget()
    {
        Instance = new EnterMineAndDigForNugget();
    }

    public override void Enter(Miner miner)
    {
        if (miner.Location() != LocationType.Goldmine)
        {
            Debug.Log("礦工:走去金礦");
            miner.ChangeLocation(LocationType.Goldmine);
        }
    }

    public override void Execute(Miner miner)
    {
        miner.AddToGoldCarried(1);
        miner.IncreaseFatigue();
        Debug.Log("礦工:採到一個金塊 | 身上有 " + miner.GoldCarried() + " 個金塊");
        // 口袋裡金塊滿了就去銀行存
        if (miner.PocketsFull())
        {
            miner.ChangeState(VisitBankAndDepositGold.Instance);
        }

        // 口渴了就去酒吧喝威士忌
        if (miner.Thirsty())
        {
            miner.ChangeState(QuenchThirst.Instance);
        }
    }

    public override void Exit(Miner miner)
    {
        Debug.Log("礦工:離開金礦");
    }
}

看到這裡,大家應該都會很熟悉。這不就是 Unity 中動畫控制器 Animator 的功能嗎!

沒錯,Animator 也是一個狀態機,有和我們之前實現十分相似的功能,例如:新增狀態轉移的條件,每個狀態都有進入、執行、退出三個回撥方法供使用。

我們可以建立 Behaviour 指令碼,對 Animator 中每一個狀態的進入、執行、退出等方法進行自定義,所以有些人直接拿 Animator 當狀態機來使用,不過我們在下文還會為我們的狀態機實現擴充套件更多的功能。

public class NewState : StateMachineBehaviour {
    // OnStateEnter is called when a transition starts and the state machine starts to evaluate this state
    //override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
    //
    //}

    // OnStateUpdate is called on each Update frame between OnStateEnter and OnStateExit callbacks
    //override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
    //
    //}

    // OnStateExit is called when a transition ends and the state machine finishes evaluating this state
    //override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
    //
    //}
    // ...
}

使 State 基類可重用

由於上面四個狀態是礦工獨有的狀態,如果要新建不同功能的角色,就有必要建立一個分離的 State 基類,這裡用泛型實現。

public abstract class State<T>
{
    /// <summary>
    /// 當狀態被進入時執行這個函式
    /// </summary>
    public abstract void Enter(T entity);

    /// <summary>
    /// 曠工更新狀態函式
    /// </summary>
    public abstract void Execute(T entity);

    /// <summary>
    /// 當狀態退出時執行這個函式
    /// </summary>
    public abstract void Exit(T entity);
}

狀態翻轉(State Blip)

這個專案其實有點像模擬人生這個遊戲,其中有一點有意思的是,當模擬人生的主角做某件事時忽然要上廁所,去完之後會繼續做之前停止的事情。這種返回前一個狀態的行為就是狀態翻轉(State Blip)

private State<T> m_pCurrentState;
private State<T> m_pPreviousState;
private State<T> m_pGlobalState;

m_pGlobalState 是一個全域性狀態,也會在 Update() 函式中和 m_pCurrentState 一起呼叫。如果有緊急的行為中斷狀態,就把這行為(例如上廁所)放到全域性狀態中,等到全域性狀態為空再進入當前狀態。

public void StateUpdate()
{
    // 如果有一個全域性狀態存在,呼叫它的執行方法
    if (m_pGlobalState != null)
    {
        m_pGlobalState.Execute(m_pOwner);
    }
    if (m_pCurrentState != null)
    {
        m_pCurrentState.Execute(m_pOwner);
    }
}

StateMachine 類

通過把所有與狀態相關的資料和方法封裝到一個 StateMachine 類中,可以使得設計更為簡潔。

public class StateMachine<T>
{
    private T m_pOwner;
    private State<T> m_pCurrentState;
    private State<T> m_pPreviousState;
    private State<T> m_pGlobalState;

    public StateMachine(T owner)
    {
        m_pOwner = owner;
    }

    public void SetCurrentState(State<T> state)
    {
        m_pCurrentState = state;
    }

    public void SetPreviousState(State<T> state)
    {
        m_pPreviousState = state;
    }

    public void SetGlobalState(State<T> state)
    {
        m_pGlobalState = state;
    }

    public void StateMachineUpdate()
    {
        // 如果有一個全域性狀態存在,呼叫它的執行方法
        if (m_pGlobalState != null)
        {
            m_pGlobalState.Execute(m_pOwner);
        }

        if (m_pCurrentState != null)
        {
            m_pCurrentState.Execute(m_pOwner);
        }
    }

    public void ChangeState(State<T> newState)
    {
        m_pPreviousState = m_pCurrentState;
        m_pCurrentState.Exit(m_pOwner);
        m_pCurrentState = newState;
        m_pCurrentState.Enter(m_pOwner);
    }

    /// <summary>
    /// 返回之前的狀態
    /// </summary>
    public void RevertToPreviousState()
    {
        ChangeState(m_pPreviousState);
    }

    public State<T> CurrentState()
    {
        return m_pCurrentState;
    }

    public State<T> PreviousState()
    {
        return m_pPreviousState;
    }

    public State<T> GlobalState()
    {
        return m_pGlobalState;
    }

    public bool IsInState(State<T> state)
    {
        return m_pCurrentState == state;
    }
}

新人物 Elsa

第二個專案會演示之前的改進。Elsa 是礦工 Bob 的妻子,她會清理小木屋和上廁所(老喝咖啡)。其中 VisitBathroom 狀態是用狀態翻轉實現的,即上完廁所要回到之前的狀態。

專案地址:programming-game-ai-by-example-in-unity/WestWorldWithWoman/
West World With Woman

訊息功能

好的遊戲實現趨向於事件驅動。即當一件事情發生了(發射了武器,主角發出了聲音等等),事件會被廣播給遊戲中相關的物件。

整合事件(觀察者模式)的狀態機可以實現更靈活的需求,例如:一個足球運動員從隊友旁邊通過時,傳球者可以傳送一個(延時)訊息,通知隊友應該什麼時候到相應位置來接球;一個士兵正在開槍攻擊敵人,忽然一個隊友中了流彈,這時候隊友可以傳送一個(即時)訊息,通知士兵立刻救援隊友。

Telegram 結構

public struct Telegram
{
    public BaseGameEntity Sender { get; private set; }
    public BaseGameEntity Receiver { get; private set; }
    public MessageType Message { get; private set; }
    public float DispatchTime { get; private set; }
    public Dictionary<string, string> ExtraInfo { get; private set; }

    public Telegram(float time, BaseGameEntity sender, BaseGameEntity receiver, MessageType message,
        Dictionary<string, string> extraInfo = null) : this()
    {
        Sender = sender;
        Receiver = receiver;
        DispatchTime = time;
        Message = message;
        ExtraInfo = extraInfo;
    }
}

這裡用結構體來實現訊息。要傳送的訊息可以作為列舉加在 MessageType 中,DispatchTime 是決定立刻傳送還是延時傳送的時間戳,ExtraInfo 能攜帶額外的資訊。這裡只用兩種訊息做例子。

public enum MessageType
{
    /// <summary>
    /// 礦工讓妻子知道他已經回到小屋了
    /// </summary>
    HiHoneyImHome,    
    /// <summary>
    /// 妻子通知礦工自己什麼時候要將晚飯從烤箱中拿出來
    /// 以及通知礦工食物已經放在桌子上了
    /// </summary>
    StewReady,
}

傳送訊息

下面是 MessageDispatcher 類,用來管理訊息的傳送。

/// <summary>
/// 管理訊息傳送的類
/// 處理立刻被髮送的訊息,和打上時間戳的訊息
/// </summary>
public class MessageDispatcher
{
    public static MessageDispatcher Instance { get; private set; }

    static MessageDispatcher()
    {
        Instance = new MessageDispatcher();
    }

    private MessageDispatcher()
    {
        priorityQueue = new HashSet<Telegram>();
    }

    /// <summary>
    /// 根據時間排序的優先順序佇列
    /// </summary>
    private HashSet<Telegram> priorityQueue;

    /// <summary>
    /// 該方法被 DispatchMessage 或者 DispatchDelayedMessages 利用。
    /// 該方法用最新建立的 telegram 呼叫接受實體的訊息處理成員函式 receiver
    /// </summary>
    public void Discharge(BaseGameEntity receiver, Telegram telegram)
    {
        if (!receiver.HandleMessage(telegram))
        {
            Debug.LogWarning("訊息未處理");
        }
    }

    /// <summary>
    /// 建立和管理訊息
    /// </summary>
    /// <param name="delay">時間的延遲(要立刻傳送就用零或負值)</param>
    /// <param name="senderId">傳送者 ID</param>
    /// <param name="receiverId">接受者 ID</param>
    /// <param name="message">訊息本身</param>
    /// <param name="extraInfo">附加訊息</param>
    public void DispatchMessage(
        float delay,
        int senderId,
        int receiverId,
        MessageType message,
        Dictionary<string, string> extraInfo)
    {
        // 獲得訊息傳送者
        BaseGameEntity sender = EntityManager.Instance.GetEntityFromId(senderId);
        // 獲得訊息接受者
        BaseGameEntity receiver = EntityManager.Instance.GetEntityFromId(receiverId);
        if (receiver == null)
        {
            Debug.LogWarning("[MessageDispatcher] 找不到訊息接收者");
            return;
        }

        float currentTime = Time.time;
        if (delay <= 0)
        {
            Telegram telegram = new Telegram(0, sender, receiver, message, extraInfo);

            Debug.Log(string.Format(
                "訊息傳送時間: {0} ,傳送者是:{1},接收者是:{2}。訊息是 {3}",
                currentTime,
                sender.Name,
                receiver.Name,
                message.ToString()));
            Discharge(receiver, telegram);
        }
        else
        {
            Telegram delayedTelegram = new Telegram(currentTime + delay, sender, receiver, message, extraInfo);
            priorityQueue.Add(delayedTelegram);

            Debug.Log(string.Format(
                "延時訊息傳送時間: {0} ,傳送者是:{1},接收者是:{2}。訊息是 {3}",
                currentTime,
                sender.Name,
                receiver.Name,
                message.ToString()));
        }
    }

    /// <summary>
    /// 傳送延時訊息
    /// 這個方法會放在遊戲的主迴圈中,以正確地和及時地傳送任何定時的訊息
    /// </summary>
    public void DisplayDelayedMessages()
    {
        float currentTime = Time.time;
        while (priorityQueue.Count > 0 &&
               priorityQueue.First().DispatchTime < currentTime &&
               priorityQueue.First().DispatchTime > 0)
        {
            Telegram telegram = priorityQueue.First();
            BaseGameEntity receiver = telegram.Receiver;

            Debug.Log(string.Format("延時訊息開始準備分發,接收者是 {0},訊息是 {1}",
                receiver.Name,
                telegram.Message.ToString()));
            // 開始分發訊息
            Discharge(receiver, telegram);
            priorityQueue.Remove(telegram);
        }
    }
}

DispatchMessage 函式會管理訊息的傳送,即時訊息會直接由 Discharge 函式傳送到接收者,延時訊息會進入佇列,通過 GameManager 遊戲主迴圈,每一幀呼叫 DisplayDelayedMessages() 函式來輪詢要傳送的訊息,當發現當前時間超過了訊息的傳送時間,就把訊息傳送給接收者。

處理訊息

處理訊息的話修改 BaseGameEntity 來增加處理訊息的功能。

public abstract class BaseGameEntity
{
    // ... 省略無關程式碼
    public abstract bool HandleMessage(Telegram message);
}

public class Miner : BaseGameEntity
{
    public override bool HandleMessage(Telegram message)
    {
        return m_stateMachine.HandleMessage(message);
    } 
}

StateMachine 程式碼也要改:

public class StateMachine<T>
{
    public bool HandleMessage(Telegram message)
    {
        if (m_pCurrentState != null && m_pCurrentState.OnMessage(m_pOwner, message))
        {
            return true;
        }

        // 如果當前狀態沒有程式碼適當的處理訊息
        // 它會傳送到實體的全域性狀態的訊息處理者
        if (m_pCurrentState != null && m_pGlobalState.OnMessage(m_pOwner, message))
        {
            return true;
        } 
        return false;
    }
}

State 基類也要修改:

public abstract class State<T>
{
    /// <summary>
    /// 處理訊息
    /// </summary>
    /// <param name="entity">接受者</param>
    /// <param name="message">要處理的訊息</param>
    /// <returns>訊息是否成功被處理</returns>
    public abstract bool OnMessage(T entity, Telegram message);
}

Discharge 函式傳送訊息給接收者,接收者將訊息給他 StateMachine 的 HandleMessage 函式處理,訊息最後通過 StateMachine 到達各種狀態的 OnMessage 函式,開始根據訊息的型別來做出處理(例如進行狀態轉移)。

具體實現請看專案程式碼:programming-game-ai-by-example-in-unity/WestWorldWithMessaging/ West World With Messaging

這裡實現的場景是:

  1. 礦工 Bob 回家後傳送 HiHoneyImHome 即時訊息給他的妻子 Elsa,提醒她做飯。
  2. Elsa 收到訊息後,停止手上的活兒,開始進入 CookStew 狀態做飯。
  3. Elsa 進入 CookStew 狀態後,把肉放到烤爐裡面,並且傳送 StewReady 延時訊息提醒自己在一段時間後拿出烤爐中的肉。
  4. Elsa 收到 StewReady 訊息後,傳送一個 StewReady 即時訊息給 Bob 提醒他飯已經做好了。如果 Bob 這時不在家,命令列將顯示 Discharge 函式中的 Warning “訊息未處理”。Bob 在家,就會開心地去吃飯。
  5. Bob 收到 StewReady 的訊息,狀態轉移到 EatStew,開始吃飯。

總結

有時候我們可能會用到多個狀態機來並行工作,例如一個 AI 有多個狀態,其中包括攻擊狀態,而攻擊狀態又有不同攻擊型別(瞄準和射擊),像一個狀態機包含另一個狀態機這種層次化的狀態機。當然也有其他不同的使用場景,我們不能受限於自己的想象力。

本文根據《遊戲人工智慧程式設計案例精粹(修訂版)》進行了 Unity 版本的實現,我對有限狀態機也有了更清晰的認識。閱讀這本書的同時也會把 Unity 實現放到下面的倉庫地址中,下篇文章可能會總結行為樹的知識,如果沒看到請督促我~

專案地址:programming-game-ai-by-example-in-unity

引用

相關文章