端午節放假總結了一下很久前寫過的一些遊戲引擎,其中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); } }
這樣,就區分了動態與靜態。靜態的是狀態之間的遷移邏輯,只要不作熱更新,是不會變的結構。動態的是狀態遷移過程當中的上下文,根據不一樣的上下文來決定。
最原始的狀態機方案除了性能存在問題,還有一個比較嚴重的問題。那就是這種狀態機框架沒法描述層級結構的狀態。 假設須要對一開始的需求進行這樣的擴展:怪在巡邏狀態下有可能進入怠工狀態,同時要求,怠工狀態下也會進行進入戰鬥的檢查。
這樣的話,雖然在以前的框架下,單獨作一個新的怠工狀態也能夠,可是仔細分析一下,咱們會發現,其實本質上巡邏狀態只是一個抽象的父狀態,其存在的意義就是進行戰鬥檢查;而具體的是在按路線巡邏仍是怠工,其實都是巡邏狀態的一個子狀態。
狀態之間就有了層級的概念,各自獨立的狀態機系統就沒法知足需求,須要一種分層次的狀態機,原先的狀態機接口設計就須要完全改掉了。
在重構狀態框架以前,須要注意兩點:
子狀態,好比怠工,必定是有跨幀的需求在的,因此這個 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 分離優化。 要優化的點有這樣幾個:
組合狀態的實例內部不該該包括自身執行的 status。目前的組合狀態,能夠動態增刪子狀態,也就是根據 status 決定告終構的狀態,理應分離靜態與動態。巡邏狀態組合了兩個子狀態——A 和 B,邏輯中是一個完成了就添加另外一個,這樣一想的話,其實巡邏狀態應該從新描述——先進行 A,再進行 B,循環往復。 因爲有了父狀態的概念,其實狀態接口的設計也能夠再迭代,理論上只須要一個 drive 便可。由於狀態內部的上下文要所有分離出來,因此也不必對外提供 OnEnter、OnExit,提供這兩個接口的意義只是作一層內部信息的隱藏,可是如今內部的 status 沒了,也就不必隱藏了。 具體分析一下須要拆出的 status:
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; } }
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 很簡單的遊戲已經綽綽有餘了。