如何簡潔實現遊戲中的AI

端午節放假總結了一下很久前寫過的一些遊戲引擎,其中NPC等遊戲AI的實現無疑是最繁瑣的部分,如今,給你們分享一下:編程

從一個簡單的情景開始

怪物,是遊戲中的一個基本概念。遊戲中的單位分類,不外乎玩家、NPC、怪物這幾種。其中,AI 必定是與三類實體都會產生交集的遊戲模塊之一。 以咱們熟悉的任意一款遊戲中的人形怪物爲例,假設有一種怪物的 AI 需求是這樣的:框架

  • 大部分狀況下,漫無目的巡邏。
  • 玩家進入視野,鎖定玩家爲目標開始攻擊。
  • Hp 低到必定程度,怪會想法設法逃跑,並說幾句話。

咱們以這個爲模型,進行這篇文章以後的全部討論。爲了簡化問題,以省去一些沒必要要的討論,將文章的核心定位到人工智能上,這裏須要注意幾點的是:異步

  • 再也不考慮 entity 之間的消息傳遞機制,例如判斷玩家進入視野,再也不經過事件機制觸發,而是經過該人形怪的輪詢觸發。
  • 再也不考慮 entity 的行爲控制機制,簡化這個 entity 的控制模型。不管是底層是基於 SteeringBehaviour 或者是瞬移,不管是異步驅的仍是主循環輪詢,都不在本文模型的討論之列。

首先能夠很容易抽象出來 IUnit:ide

public interface IUnit
    {
        void ChangeState(UnitStateEnum state);
        void Patrol(); 
        IUnit GetNearestTarget(); 
        void LockTarget(IUnit unit);
        float GetFleeBloodRate();
        bool CanMove();
        bool HpRateLessThan(float rate);
        void Flee();
        void Speak();
    }
public interface IUnit
    {
        void ChangeState(UnitStateEnum state);
        void Patrol(); 
        IUnit GetNearestTarget(); 
        void LockTarget(IUnit unit);
        float GetFleeBloodRate();
        bool CanMove();
        bool HpRateLessThan(float rate);
        void Flee();
        void Speak();
    }

而後,咱們能夠經過一個簡單的有限狀態機 (FSM) 來控制這個單位的行爲。不一樣狀態下,單位都具備不一樣的行爲準則,以造成智能體。 具體來講,咱們能夠定義這樣幾種狀態:性能

  • 巡邏狀態: 會執行巡邏,同時檢查是否有敵對單位接近,接近的話進入戰鬥狀態。
  • 戰鬥狀態: 會執行戰鬥,同時檢查本身的血量是否達到逃跑線如下,達成檢查了就會逃跑。
  • 逃跑狀態: 會逃跑,同時說一次話。

最原始的狀態機的代碼:優化

public interface IState<TState, TUnit> where TState : IConvertible
    {
        TState Enum { get; }
        TUnit Self { get; }
        void OnEnter();
        void Drive();
        void OnExit();
    }

public interface IState<TState, TUnit> where TState : IConvertible
    {
        TState Enum { get; }
        TUnit Self { get; }
        void OnEnter();
        void Drive();
        void OnExit();
    }

以逃跑狀態爲例:this

public class FleeState : UnitStateBase
    {
        public FleeState(IUnit self) : base(UnitStateEnum.Flee, self)
        {
        }
        public override void OnEnter()
        {
            Self.Flee();
        }
        public override void Drive()
        {
            var unit = Self.GetNearestTarget();
            if (unit != null)
            {
                return;
            }

            Self.ChangeState(UnitStateEnum.Patrol);
        }
    }
public class FleeState : UnitStateBase
    {
        public FleeState(IUnit self) : base(UnitStateEnum.Flee, self)
        {
        }
        public override void OnEnter()
        {
            Self.Flee();
        }
        public override void Drive()
        {
            var unit = Self.GetNearestTarget();
            if (unit != null)
            {
                return;
            }
 
            Self.ChangeState(UnitStateEnum.Patrol);
        }
    }

決策邏輯與上下文分離

上述是一個最簡單、最常規的狀態機實現。估計只有學生會這樣寫,業界確定是沒人這樣寫 AI 的,否則遊戲怎麼死的都不知道。人工智能

首先有一個很是明顯的性能問題:狀態機本質是描述狀態遷移的,並不須要記錄 entity 的 context,若是 entity 的 context 記錄在 State上,那麼狀態機這個遷移邏輯就須要每一個 entity 都來一份 instance,這麼一個簡單的狀態遷移就須要消耗大約 X 個字節,那麼一個場景 1w 個怪,這些都屬於白白消耗的內存。就目前的實現來看,具體的一個 State 實例內部 hold 住了 Unit,因此 State 實例是沒辦法複用的。spa

針對這一點,咱們作一下優化。對這個狀態機,把 Context 徹底剝離出來。設計

修改狀態機接口定義:

public interface IState<TState, TUnit> where TState : IConvertible
    {
        TState Enum { get; }
        void OnEnter(TUnit self);
        void Drive(TUnit self);
        void OnExit(TUnit self);
    }

public interface IState<TState, TUnit> where TState : IConvertible
    {
        TState Enum { get; }
        void OnEnter(TUnit self);
        void Drive(TUnit self);
        void OnExit(TUnit self);
    }

仍是拿以前實現好的逃跑狀態做爲例子:

public class FleeState : UnitStateBase
    {
        public FleeState() : base(UnitStateEnum.Flee)
        {
        }
        public override void OnEnter(IUnit self)
        {
            base.OnEnter(self);
            self.Flee();
        }
        public override void Drive(IUnit self)
        {
            base.Drive(self);

            var unit = self.GetNearestTarget();
            if (unit != null)
            {
                return;
            }

            self.ChangeState(UnitStateEnum.Patrol);
        }
    }
public class FleeState : UnitStateBase
    {
        public FleeState() : base(UnitStateEnum.Flee)
        {
        }
        public override void OnEnter(IUnit self)
        {
            base.OnEnter(self);
            self.Flee();
        }
        public override void Drive(IUnit self)
        {
            base.Drive(self);
 
            var unit = self.GetNearestTarget();
            if (unit != null)
            {
                return;
            }
 
            self.ChangeState(UnitStateEnum.Patrol);
        }
    }

這樣,就區分了動態與靜態。靜態的是狀態之間的遷移邏輯,只要不作熱更新,是不會變的結構。動態的是狀態遷移過程當中的上下文,根據不一樣的上下文來決定。

分層有限狀態機

最原始的狀態機方案除了性能存在問題,還有一個比較嚴重的問題。那就是這種狀態機框架沒法描述層級結構的狀態。 假設須要對一開始的需求進行這樣的擴展:怪在巡邏狀態下有可能進入怠工狀態,同時要求,怠工狀態下也會進行進入戰鬥的檢查。

這樣的話,雖然在以前的框架下,單獨作一個新的怠工狀態也能夠,可是仔細分析一下,咱們會發現,其實本質上巡邏狀態只是一個抽象的父狀態,其存在的意義就是進行戰鬥檢查;而具體的是在按路線巡邏仍是怠工,其實都是巡邏狀態的一個子狀態。

狀態之間就有了層級的概念,各自獨立的狀態機系統就沒法知足需求,須要一種分層次的狀態機,原先的狀態機接口設計就須要完全改掉了。

在重構狀態框架以前,須要注意兩點:

由於父狀態須要關注子狀態的運行結果,因此狀態的 Drive 接口須要一個運行結果的返回值。

子狀態,好比怠工,必定是有跨幀的需求在的,因此這個 Result,咱們定義爲 Continue、Sucess、Failure。

子狀態必定是由父狀態驅動的。

考慮這樣一個組合狀態情景:巡邏時,須要依次得先走到一個點,而後怠工一下子,再走到下一個點,而後再怠工一下子,循環往復。這樣就須要父狀態(巡邏狀態)註記當前激活的子狀態,而且根據子狀態執行結果的不一樣來修改激活的子狀態集合。這樣不只是 Unit 自身有上下文,連組合狀態也有了本身的上下文。

爲了簡化討論,咱們仍是從 non-ContextFree 層次狀態機系統設計開始。

修改後的狀態定義:

public interface IState<TState, TCleverUnit, TResult> 
        where TState : IConvertible
    {
        // ...
        TResult Drive();
        // ...
    }

public interface IState<TState, TCleverUnit, TResult> 
        where TState : IConvertible
    {
        // ...
        TResult Drive();
        // ...
    }

組合狀態的定義:

public abstract class UnitCompositeStateBase : UnitStateBase
    {
        protected readonly LinkedList<UnitStateBase> subStates = new LinkedList<UnitStateBase>();

        // ...
        protected Result ProcessSubStates()
        {
            if (subStates.Count == 0)
            {
                return Result.Success;
            }

            var front = subStates.First;
            var res = front.Value.Drive();

            if (res != Result.Continue)
            {
                subStates.RemoveFirst();
            }

            return Result.Continue;
        }
        // ...
    }
public abstract class UnitCompositeStateBase : UnitStateBase
    {
        protected readonly LinkedList<UnitStateBase> subStates = new LinkedList<UnitStateBase>();
 
        // ...
        protected Result ProcessSubStates()
        {
            if (subStates.Count == 0)
            {
                return Result.Success;
            }
 
            var front = subStates.First;
            var res = front.Value.Drive();
 
            if (res != Result.Continue)
            {
                subStates.RemoveFirst();
            }
 
            return Result.Continue;
        }
        // ...
    }

巡邏狀態如今是一個組合狀態:

public class PatrolState : UnitCompositeStateBase
    {
        // ...
        public override void OnEnter()
        {
            base.OnEnter();
            AddSubState(new MoveToState(Self));
        }

        public override Result Drive()
        {
            if (subStates.Count == 0)
            {
                return Result.Success;
            }

            var unit = Self.GetNearestTarget();
            if (unit != null)
            {
                Self.LockTarget(unit);
                return Result.Success;
            }

            var front = subStates.First;
            var ret = front.Value.Drive();

            if (ret != Result.Continue)
            {
                if (front.Value.Enum == CleverUnitStateEnum.MoveTo)
                {
                    AddSubState(new IdleState(Self));
                }
                else
                {
                    AddSubState(new MoveToState(Self));
                }
            }
            
            return Result.Continue;
        }
    }
public class PatrolState : UnitCompositeStateBase
    {
        // ...
        public override void OnEnter()
        {
            base.OnEnter();
            AddSubState(new MoveToState(Self));
        }
 
        public override Result Drive()
        {
            if (subStates.Count == 0)
            {
                return Result.Success;
            }
 
            var unit = Self.GetNearestTarget();
            if (unit != null)
            {
                Self.LockTarget(unit);
                return Result.Success;
            }
 
            var front = subStates.First;
            var ret = front.Value.Drive();
 
            if (ret != Result.Continue)
            {
                if (front.Value.Enum == CleverUnitStateEnum.MoveTo)
                {
                    AddSubState(new IdleState(Self));
                }
                else
                {
                    AddSubState(new MoveToState(Self));
                }
            }
            
            return Result.Continue;
        }
    }

看過《遊戲人工智能編程精粹》的同窗可能看到這裏就會發現,這種層次狀態機其實就是這本書裏講的目標驅動的狀態機。組合狀態就是組合目標,子狀態就是子目標。父目標 / 狀態的調度取決於子目標 / 狀態的完成狀況。

這種狀態框架與普通的 trivial 狀態機模型的區別僅僅是增長了對層次狀態的支持,狀態的遷移仍是須要靠顯式的 ChangeState 來作。

這本書裏面的狀態框架,每一個狀態的執行 status 記錄在了實例內部,不方便後續的優化,咱們這裏實現的時候首先把這個作成純驅動式的。可是還不夠。如今以前的 ContextFree 優化成果已經回退掉了,咱們還須要補充回來。

分層的上下文

咱們對以前重構出來的層次狀態機框架再進行一次 Context 分離優化。 要優化的點有這樣幾個:

首先是繼續以前的,unit 不該該做爲一個 state 本身的內部 status。

組合狀態的實例內部不該該包括自身執行的 status。目前的組合狀態,能夠動態增刪子狀態,也就是根據 status 決定告終構的狀態,理應分離靜態與動態。巡邏狀態組合了兩個子狀態——A 和 B,邏輯中是一個完成了就添加另外一個,這樣一想的話,其實巡邏狀態應該從新描述——先進行 A,再進行 B,循環往復。      因爲有了父狀態的概念,其實狀態接口的設計也能夠再迭代,理論上只須要一個 drive 便可。由於狀態內部的上下文要所有分離出來,因此也不必對外提供 OnEnter、OnExit,提供這兩個接口的意義只是作一層內部信息的隱藏,可是如今內部的 status 沒了,也就不必隱藏了。    具體分析一下須要拆出的 status:

  • 一部分是 entity 自己的 status,這裏能夠簡單的認爲是 unit。
  • 另外一部分是 state 自己的 status。
  • 對於組合狀態,這個 status 描述的是我當前執行到哪一個 substate。
  • 對於原子狀態,這個 status 描述的種類可能有所區別。
  • 例如 MoveTo/Flee,OnEnter 的時候,修改了 unit 的 status,而後 Drive 的時候去 check。
  • 例如 Idle,OnEnter 時改了本身的 status,而後 Drive 的時候去 check。 通過總結,咱們能夠發現,每一個狀態的 status 本質上均可以經過一個變量來描述。一個 State 做爲一個最小粒度的單元,具備這樣的 Concept: 輸入一個 Context,輸出一個 Result。

Context 暫時只須要包括這個 Unit,和以前所說的 status。同時,考慮這樣一個問題:

  • 父狀態 A,子狀態 B。
  • 子狀態 B 向上返回 Continue 的同時,status 記錄下來爲 b。
  • 父狀態 ADrive 子狀態的結果爲 Continue,自身也須要向上拋出 Continue,同時本身也有 status 爲 a。 這樣,再還原現場時,就須要即給 A 一個 a,還須要讓 A 有能力從 Context 中拿到須要給 B 的 b。所以上下文的結構理應是遞歸定義的,是一個層級結構。

Context 以下定義:

public class Continuation
    {
        public Continuation SubContinuation { get; set; }
        public int NextStep { get; set; }
        public object Param { get; set; }
    }

    public class Context<T>
    {
        public Continuation Continuation { get; set; }
        public T Self { get; set; }
    }

public class Continuation
    {
        public Continuation SubContinuation { get; set; }
        public int NextStep { get; set; }
        public object Param { get; set; }
    }
 
    public class Context<T>
    {
        public Continuation Continuation { get; set; }
        public T Self { get; set; }
    }

修改 State 的接口定義爲:

public interface IState<TCleverUnit, TResult>
     {
         TResult Drive(Context<TCleverUnit> ctx);
    }

public interface IState<TCleverUnit, TResult>
     {
         TResult Drive(Context<TCleverUnit> ctx);
    }

已經至關簡潔了。

這樣,咱們對以前的巡邏狀態也作下修改,達到一個 ContextFree 的效果。利用 Context 中的 Continuation 來肯定當前結點應該從什麼狀態繼續:

public class PatrolState : IState<ICleverUnit, Result>
    {
        private readonly List<IState<ICleverUnit, Result>> subStates;
        public PatrolState()
        {
            subStates = new List<IState<ICleverUnit, Result>>()
            {
                new MoveToState(),
                new IdleState(),
            };
        }
        public Result Drive(Context<ICleverUnit> ctx)
        {
            var unit = ctx.Self.GetNearestTarget();
            if (unit != null)
            {
                ctx.Self.LockTarget(unit);

                return Result.Success;
            }

            var nextStep = 0;
            if (ctx.Continuation != null)
            {
                // Continuation
                var thisContinuation = ctx.Continuation;

                ctx.Continuation = thisContinuation.SubContinuation;

                var ret = subStates[nextStep].Drive(ctx);

                if (ret == Result.Continue)
                {
                    thisContinuation.SubContinuation = ctx.Continuation;
                    ctx.Continuation = thisContinuation;

                    return Result.Continue;
                }
                else if (ret == Result.Failure)
                {
                    ctx.Continuation = null;

                    return Result.Failure;
                }

                ctx.Continuation = null;
                nextStep = thisContinuation.NextStep + 1;
            }

            for (; nextStep < subStates.Count; nextStep++)
            {
                var ret = subStates[nextStep].Drive(ctx);
                if (ret == Result.Continue)
                {
                    ctx.Continuation = new Continuation()
                    {
                        SubContinuation = ctx.Continuation,
                        NextStep = nextStep,
                    };

                    return Result.Continue;
                } 
                else if (ret == Result.Failure) 
                {
                    ctx.Continuation = null;

                    return Result.Failure;
                }
            }

            ctx.Continuation = null;

            return Result.Success;
        }
    }
public class PatrolState : IState<ICleverUnit, Result>
    {
        private readonly List<IState<ICleverUnit, Result>> subStates;
        public PatrolState()
        {
            subStates = new List<IState<ICleverUnit, Result>>()
            {
                new MoveToState(),
                new IdleState(),
            };
        }
        public Result Drive(Context<ICleverUnit> ctx)
        {
            var unit = ctx.Self.GetNearestTarget();
            if (unit != null)
            {
                ctx.Self.LockTarget(unit);
 
                return Result.Success;
            }
 
            var nextStep = 0;
            if (ctx.Continuation != null)
            {
                // Continuation
                var thisContinuation = ctx.Continuation;
 
                ctx.Continuation = thisContinuation.SubContinuation;
 
                var ret = subStates[nextStep].Drive(ctx);
 
                if (ret == Result.Continue)
                {
                    thisContinuation.SubContinuation = ctx.Continuation;
                    ctx.Continuation = thisContinuation;
 
                    return Result.Continue;
                }
                else if (ret == Result.Failure)
                {
                    ctx.Continuation = null;
 
                    return Result.Failure;
                }
 
                ctx.Continuation = null;
                nextStep = thisContinuation.NextStep + 1;
            }
 
            for (; nextStep < subStates.Count; nextStep++)
            {
                var ret = subStates[nextStep].Drive(ctx);
                if (ret == Result.Continue)
                {
                    ctx.Continuation = new Continuation()
                    {
                        SubContinuation = ctx.Continuation,
                        NextStep = nextStep,
                    };
 
                    return Result.Continue;
                } 
                else if (ret == Result.Failure) 
                {
                    ctx.Continuation = null;
 
                    return Result.Failure;
                }
            }
 
            ctx.Continuation = null;
 
            return Result.Success;
        }
    }

subStates 是 readonly 的,在組合狀態構造的一開始就肯定了值。這樣結構自己就是靜態的,而上下文是動態的。不一樣的 entity instance 共用同一個樹的 instance。

優化到這個版本,至少在性能上已經符合要求了,全部實例共享一個靜態的狀態遷移邏輯。面對以前提出的需求,也可以解決。至少算是一個通過對《遊戲人工智能編程精粹》中提出的目標驅動狀態機模型優化後的一個符合工業應用標準的 AI 框架。拿來作小遊戲或者是一些 AI 很簡單的遊戲已經綽綽有餘了。

心動了嗎?還不趕忙動起來,打造屬於本身的遊戲世界!頓時滿滿的自豪感,真的很想知道你們的想法,還請持續關注更新,更多幹貨和資料請直接聯繫我,也能夠加羣710520381,邀請碼:柳貓,歡迎你們共同討論

相關文章
相關標籤/搜索