【細說遊戲 AI】這個 AI 好厲害,給我也整一個之狀態機 AI

ITryagain發表於2021-05-07

前言

最近也看了不少關於遊戲 AI 的相關文章,對這方面挺感興趣,就打算嘗試實現各種 AI,然後總結,寫成一個系列文章,這篇文章將介紹狀態機 AI,並給出在 Unity 中的相關實現。
去年 5 月份學校的動畫與遊戲程式設計這門課程佈置了個大作業,用 Unity 做坦克大戰(雖說就是照著教程做罷了),當時就試著用狀態機寫了 AI。這次跟幾個小夥伴參加 CUSGA(第一屆中國大學生遊戲開發創作大賽),做了個簡化版的 RTS 遊戲,其中的 AI 也是通過狀態機實現的。將在這邊文章中做個總結。

注:遊戲目前實現的功能還並不是很多,如果看了演示視訊,希望能給些相關的改進建議,等這段時間畢設搞完之後就開始繼續改進。

有限狀態機(FSM)

這段時間看了不少大佬的分析和文章,瞭解到狀態機大致有有限狀態機和分層狀態機兩種,這裡將先介紹有限狀態機。

定義

狀態機這個概念對大家來說應該並不陌生,比如編譯原理中的有窮自動機(FA),字串演算法中的自動機(AC自動機、字尾自動機、迴文自動機等)等。

這裡將給出定義:

  1. 包含有限個狀態
  2. 可以表示為一張圖,節點為狀態,邊為狀態的改變

其形式大致與 Unity 的 Animation Controller 類似

Unity Animation Controller

實現

關於有限狀態機 AI 的實現,最簡單的當然就是 if... else 或是 switch 判斷當前狀態,然後執行相應的 action,但是這樣的實現會導致當狀態和動作空間變大時,程式碼難以維護。因此,我在實現的時候都是採用的狀態模式。

關於目前在專案中使用的狀態機架構,主要是五個類,分別為 Action、State、Decision、Transition、StateController,程式碼如下:

Action

public abstract class Action : ScriptableObject
{
    public abstract void Act(StateController controller);
}

Decision

public abstract class Decision : ScriptableObject
{
    public abstract bool Decide(StateController controller);
}

Transition

[System.Serializable]
public class Transifition
{
    public Decision decision;
    public State trueState;
    public State falseState;
}

State

[CreateAssetMenu (menuName = "AI/State")]
public class State : ScriptableObject
{
    public Action[] actions;
    public Transifition[] transitions;

    public void UpdateState(StateController controller)
    {
        DoActions(controller);
        CheckTransitions(controller);
    }

    private void DoActions(StateController controller)
    {
        for(int i = 0; i < actions.Length; i++)
        {
            actions[i].Act(controller);
        }
    }

   private void CheckTransitions(StateController controller)
    {
        for(int i = 0; i < transitions.Length; i++)
        {
            bool decisionSucceeded = transitions[i].decision.Decide(controller);

            State transitionState = decisionSucceeded ? transitions[i].trueState : transitions[i].falseState;

            controller.TransitionToState(transitionState);

        }
    }
}

StateController

public class StateController : MonoBehaviour
{
    public State currentState;
    public State remainState;
    /*
    	其他成員變數
    */

    private bool aiActive = true;

    private void Awake()
    {
        // 自身成員變數設定
    }

    // 用於 Manager 類來設定 AI
    public void SetupAI(/* 引數 */)
    {
        // 需要設定的東西
    }

    // 用於狀態的改變
    public void TransitionToState(State nextState)
    {
        if(nextState != remainState)
        {
            //Debug.Log(transform.gameObject.name + " start" + nextState);
            currentState = nextState;
            OnExitState();
        }
    }

    // 用於判斷進入該狀態是否經過 duration 時間
    public bool CheckifCountDownElapsed(float duration)
    {
        stateTimeElapsed += Time.deltaTime;
        return (stateTimeElapsed >= duration);
    }
    
    public void OnExitState()
    {
        stateTimeElapsed = 0;
    }

    void Update()
    {
        if (!aiActive)
            return;
        currentState.UpdateState(this);
    }
}

使用

以上便是這幾個類的相關程式碼,下面來分別講一講如何使用

  1. Action 作為抽象類,表示在某一 State 下,AI 將會執行的動作,通過繼承的方式實現需要執行的動作,比如要實現 AI 的移動的話

    [CreateAssetMenu(menuName = "AI/Actions/Player/Move")]
    public class MoveAction : Action
    {
        public State standby;
    
        public override void Act(StateController controller)
        {
            Move(controller);
        }
    
        private void Move(StateController controller)
        {
            controller.navMeshAgent.SetDestination(controller.targetPoint + controller.RelativePosition);
            controller.navMeshAgent.isStopped = false;
    
            if (controller.navMeshAgent.remainingDistance <= controller.navMeshAgent.stoppingDistance && !controller.navMeshAgent.pathPending)
            {
                // 到目標點後,改變狀態為 standby
                controller.navMeshAgent.isStopped = true;
                controller.TransitionToState(standby);
            }
        }
    }
    
  2. Decision 也是抽象類,在某一 State 下,AI 通過 Decision 的 Decide 來判斷是否滿足某一條件,比如下面場景:當 AI 在移動過程中遇到敵人則需要進入戰鬥狀態,那麼此時需要實現的 Decision 如下

    [CreateAssetMenu (menuName = "AI/Decisions/Player/Encounter")]
    public class EncounterDecision : Decision
    {
        // 用於玩家 AI 在前往目的地的過程中做決策
        public override bool Decide(StateController controller)
        {
            bool encounter = Encounter(controller);
            return encounter;
        }
    
        private bool Encounter(StateController controller)
        {
            Collider[] objects = Physics.OverlapSphere(controller.transform.position, controller.stats.attackRange * controller.stats.visionRange, 1 << 9);
            foreach (Collider c in objects)
            {
                // TODO: 設定攻擊目標
                controller.attackObject = c;
                return true;
            }
            return false;
        }
    }
    
  3. Transition 則用於 State 之間的轉換,代表了能從當前狀態可以轉到的狀態,其包含了 Decision、true State 和 false State。

  4. State 則表示狀態,其包含了 Actions 和 Transitions。

  5. StateController 則表示當前 AI,所有有關 AI 的屬性都可放置在其中,比如在我們做的遊戲 Demo 中,StateController 就表示了兵團中的士兵,其包含了 Health、Attack、NavemeshAgent、AnimationController 等屬性,我們在遊戲中可通過 Manager 相關的類來操作這些 StateController,從而控制 AI 的啟動、暫停等。

當實現了相關的類之後,便可在 Unity 中通過建立資源的方式來建立 State、Decision 和 Action,然後拼成我們需要的資源。

小節

本打算五一期間就把這篇文章寫出來的,但是中間出去玩了兩天,然後又鹹魚了幾天,就拖到了現在。中間也在一直思考幾個問題:

  1. 要如何將現在實現的這個狀態機改進成分層狀態機,目前暫時還想不到,感覺這個分層的設計可能需要結合實際場景(希望大佬們能夠提供一些思路 orz
  2. 如何在當前狀態機框架中更好地實現動畫的表現,我目前是在實現 Action 的時候通過 Animation Controller 的狀態,以及執行時間來改變當前的動畫,但總感覺不是很好。(主要最近在知乎上看到林爺的文章之後感覺可以設計一個專門控制動畫的類)

繼續整畢設去了 orz,想想還有一個月多一點就要畢業了,好快- -

相關文章