從上古卷軸中形形色色的人物,到NBA2K中揮灑汗水的球員,從使命召喚中詭計多端的敵人,到刺客信條中栩栩如生的人羣。遊戲AI幾乎存在於遊戲中的每一個角落,默默構建出一個使人神往的龐大遊戲世界。
那麼這些複雜的AI又是怎麼實現的呢?下面就讓咱們來了解並親手實現一下游戲AI基礎架構之一的行爲樹。git
行爲樹是一種樹狀的數據結構,樹上的每個節點都是一個行爲。每次調用會從根節點開始遍歷,經過檢查行爲的執行狀態來執行不一樣的節點。他的優勢是耦合度低擴展性強,每一個行爲能夠與其餘行爲徹底獨立。目前的行爲樹已經能夠將幾乎任意架構(如規劃器,效用論等)應用於AI之上。github
class BehaviorTree { public: BehaviorTree(Behavior* InRoot) { Root = InRoot; } void Tick() { Root->Tick(); } bool HaveRoot() { return Root?true:false; } void SetRoot(Behavior* InNode) { Root= InNode; } void Release() { Root->Release(); } private: Behavior* Root; };
上面提供了行爲樹的實現,行爲樹有一個根節點和一個Tick()方法,在遊戲過程當中每一個一段時間會調用依次Tick方法,令行爲樹從根節點開始執行。數據結構
行爲(behavior)是行爲樹最基礎的概念,是幾乎全部行爲樹節點的基類,是一個抽象接口,而如動做條件等節點則是它的具體實現。
下面是Behavior的實現,省略掉了一些簡單的判斷狀態的方法完整源碼能夠參照文尾的github連接架構
class Behavior { public: //釋放對象所佔資源 virtual void Release() = 0; //包裝函數,防止打破調用契約 EStatus Tick(); EStatus GetStatus() { return Status; } virtual void AddChild(Behavior* Child){}; protected: //建立對象請調用Create()釋放對象請調用Release() Behavior():Status(EStatus::Invalid){} virtual ~Behavior() {} virtual void OnInitialize() {}; virtual EStatus Update() = 0; virtual void OnTerminate(EStatus Status) {}; protected: EStatus Status; };
Behavior接口是全部行爲樹節點的核心,且我規定全部節點的構造和析構方法都必須是protected,以防止在棧上建立對象,全部的節點對象經過Create()靜態方法在堆上建立,經過Release()方法銷燬,因爲Behavior是個抽象接口,故沒有提供Create()方法,本接口知足以下契約ide
爲了保證契約不被打破,咱們將這三個方法包裝在Tick()方法裏。Tick()的實現以下函數
//update方法被首次調用前執行OnInitlize方法,每次行爲樹更新時調用一次update方法 //當剛剛更新的行爲再也不運行時調用OnTerminate方法 if (Status != EStatus::Running) { OnInitialize(); } Status = Update(); if (Status != EStatus::Running) { OnTerminate(Status); } return Status;
其中返回值Estatus是一個枚舉值,表示節點運行狀態。優化
enum class EStatus:uint8_t { Invalid, //初始狀態 Success, //成功 Failure, //失敗 Running, //運行 Aborted, //終止 };
動做是行爲樹的葉子節點,表示角色作的具體操做(如攻擊,上彈,防護等),負責改變遊戲世界的狀態。動做節點可直接繼承自Behavior節點,經過實現不一樣的Update()方法實現不一樣的邏輯,在OnInitialize()方法中獲取數據和資源,在OnTerminate中釋放資源。ui
//動做基類 class Action :public Behavior { public: virtual void Release() { delete this; } protected: Action() {} virtual ~Action() {} };
在這裏我實現了一個動做基類,主要是爲了一個公用的Release方法負責釋放節點內存空間,全部動做節點都可繼承自這個方法this
條件一樣是行爲樹的葉子節點,用於查看遊戲世界信息(如敵人是否在攻擊範圍內,周圍是否有可攀爬物體等),經過返回狀態表示條件的成功。spa
//條件基類 class Condition :public Behavior { public: virtual void Release() { delete this; } protected: Condition(bool InIsNegation):IsNegation(InIsNegation) {} virtual ~Condition() {} protected: //是否取反 bool IsNegation=false; };
這裏我實現了條件基類,一個IsNegation來標識條件是否取反(好比是否看見敵人能夠變爲是否沒有看見敵人)
裝飾器(Decorator)是隻有一個子節點的行爲,顧名思義,裝飾便是在子節點的原有邏輯上增添細節(如重複執行子節點,改變子節點返回狀態等)
//裝飾器 class Decorator :public Behavior { public: virtual void AddChild(Behavior* InChild) { Child=InChild; } protected: Decorator() {} virtual ~Decorator(){} Behavior* Child; };
實現了裝飾器基類,下面咱們來實現下具體的裝飾器,也就是上面提到的重複執行屢次子節點的裝飾器
class Repeat :public Decorator { public: static Behavior* Create(int InLimited) { return new Repeat(InLimited); } virtual void Release() { Child->Release(); delete this; } protected: Repeat(int InLimited) :Limited(InLimited) {} virtual ~Repeat(){} virtual void OnInitialize() { Count = 0; } virtual EStatus Update()override; virtual Behavior* Create() { return nullptr; } protected: int Limited = 3; int Count = 0; };
正如上面提到的,Create函數負責建立節點,Release負責釋放
其中Update()方法的實現以下
EStatus Repeat::Update() { while (true) { Child->Tick(); if (Child->IsRunning())return EStatus::Success; if (Child->IsFailuer())return EStatus::Failure; if (++Count == Limited)return EStatus::Success; Child->Reset(); } return EStatus::Invalid; }
邏輯很簡單,若是執行失敗就當即返回,執行中就繼續執行,執行成功就把計數器+1重複執行
咱們將行爲樹中具備多個子節點的行爲稱爲複合節點,經過複合節點咱們能夠將簡單節點組合爲更有趣更復雜的行爲邏輯。
下面實現了一個符合節點的基類,將一些公用的方法放在了裏面(如添加清除子節點等)
//複合節點基類 class Composite:public Behavior { virtual void AddChild(Behavior* InChild) override{Childern.push_back(InChild);} void RemoveChild(Behavior* InChild); void ClearChild() { Childern.clear(); } virtual void Release() { for (auto it : Childern) { it->Release(); } delete this; } protected: Composite() {} virtual ~Composite() {} using Behaviors = std::vector<Behavior*>; Behaviors Childern; };
順序器(Sequence)是複合節點的一種,它依次執行每一個子行爲,直到全部子行爲執行成功或者有一個失敗爲止。
//順序器:依次執行全部節點直到其中一個失敗或者所有成功位置 class Sequence :public Composite { public: virtual std::string Name() override { return "Sequence"; } static Behavior* Create() { return new Sequence(); } protected: Sequence() {} virtual ~Sequence(){} virtual void OnInitialize() override { CurrChild = Childern.begin();} virtual EStatus Update() override; protected: Behaviors::iterator CurrChild; };
其中Update()方法的實現以下
EStatus Sequence::Update() { while (true) { EStatus s = (*CurrChild)->Tick(); //若是執行成功了就繼續執行,不然返回 if (s != EStatus::Success) return s; if (++CurrChild == Childern.end()) return EStatus::Success; } return EStatus::Invalid; //循環意外終止 }
選擇器(Selector)是另外一種經常使用的複合行爲,它會依次執行每一個子行爲直到其中一個成功執行或者所有失敗爲止
因爲與順序器僅僅是Update函數不一樣,下面僅貼出Update方法
EStatus Selector::Update() { while (true) { EStatus s = (*CurrChild)->Tick(); if (s != EStatus::Failure) return s; //若是執行失敗了就繼續執行,不然返回 if (++CurrChild == Childern.end()) return EStatus::Failure; } return EStatus::Invalid; //循環意外終止 }
顧名思義,並行器(Parallel)是一種讓多個行爲並行執行的節點。但仔細觀察便會發現實際上只是他們的更新函數在同一幀被屢次調用而已。
//並行器:多個行爲並行執行 class Parallel :public Composite { public: static Behavior* Create(EPolicy InSucess, EPolicy InFailure){return new Parallel(InSucess, InFailure); } virtual std::string Name() override { return "Parallel"; } protected: Parallel(EPolicy InSucess, EPolicy InFailure) :SucessPolicy(InSucess), FailurePolicy(InFailure) {} virtual ~Parallel() {} virtual EStatus Update() override; virtual void OnTerminate(EStatus InStatus) override; protected: EPolicy SucessPolicy; EPolicy FailurePolicy; };
這裏的Epolicy是一個枚舉類型,表示成功和失敗的條件(是成功或失敗一個仍是所有成功或失敗)
//Parallel節點成功與失敗的要求,是所有成功/失敗,仍是一個成功/失敗 enum class EPolicy :uint8_t { RequireOne, RequireAll, };
update函數實現以下
EStatus Parallel::Update() { int SuccessCount = 0, FailureCount = 0; int ChildernSize = Childern.size(); for (auto it : Childern) { if (!it->IsTerminate()) it->Tick(); if (it->IsSuccess()) { ++SuccessCount; if (SucessPolicy == EPolicy::RequireOne) { it->Reset(); return EStatus::Success; } } if (it->IsFailuer()) { ++FailureCount; if (FailurePolicy == EPolicy::RequireOne) { it->Reset(); return EStatus::Failure; } } } if (FailurePolicy == EPolicy::RequireAll&&FailureCount == ChildernSize) { for (auto it : Childern) { it->Reset(); } return EStatus::Failure; } if (SucessPolicy == EPolicy::RequireAll&&SuccessCount == ChildernSize) { for (auto it : Childern) { it->Reset(); } return EStatus::Success; } return EStatus::Running; }
在代碼中,並行器每次更新都執行每個還沒有終結的子行爲,並檢查成功和失敗條件,若是知足則當即返回。
另外,當並行器知足條件提早退出時,全部正在執行的子行爲也應該當即被終止,咱們在OnTerminate()函數中調用每一個子節點的終止方法
void Parallel::OnTerminate(EStatus InStatus) { for (auto it : Childern) { if (it->IsRunning()) it->Abort(); } }
監視器是並行器的應用之一,經過在行爲運行過程當中不斷檢查是否知足某條件,若是不知足則馬上退出。將條件放在並行器的尾部便可。
主動選擇器是選擇器的一種,與普通的選擇器不一樣的是,主動選擇器會不斷的主動檢查已經作出的決策,並不斷的嘗試高優先級行爲的可行性,當高優先級行爲可行時胡當即打斷低優先級行爲的執行(如正在巡邏的過程當中發現敵人,即時中斷巡邏,當即攻擊敵人)。
其Update()方法和OnInitialize方法實現以下
//初始化時將CurrChild初始化爲子節點的末尾 virtual void OnInitialize() override { CurrChild = Childern.end(); } EStatus ActiveSelector::Update() { //每次執行前先保存的當前節點 Behaviors::iterator Previous = CurrChild; //調用父類OnInlitiallize函數讓選擇器每次從新選取節點 Selector::OnInitialize(); EStatus result = Selector::Update(); //若是優先級更高的節點成功執行或者原節點執行失敗則終止當前節點的執行 if (Previous != Childern.end()&CurrChild != Previous) { (*Previous)->Abort(); } return result; }
這裏我建立了一名角色,該角色一開始處於巡邏狀態,一旦發現敵人,先檢查本身生命值是否太低,若是是就逃跑,不然就攻擊敵人,攻擊過程當中若是生命值太低也會中斷攻擊,當即逃跑,若是敵人死亡則當即中止攻擊,這裏咱們使用了構建器來建立了一棵行爲樹,關於構建器的實現後面會講到,這裏每一個函數建立了對應函數名字的節點,
//構建行爲樹:角色一開始處於巡邏狀態,一旦發現敵人,先檢查本身生命值是否太低,若是是就逃跑,不然就攻擊敵人,攻擊過程當中若是生命值太低也會中斷攻擊,當即逃跑,若是敵人死亡則當即中止攻擊 BehaviorTreeBuilder* Builder = new BehaviorTreeBuilder(); BehaviorTree* Bt=Builder ->ActiveSelector() ->Sequence() ->Condition(EConditionMode::IsSeeEnemy,false) ->Back() ->ActiveSelector() -> Sequence() ->Condition(EConditionMode::IsHealthLow,false) ->Back() ->Action(EActionMode::Runaway) ->Back() ->Back() ->Monitor(EPolicy::RequireAll,EPolicy::RequireOne) ->Condition(EConditionMode::IsEnemyDead,true) ->Back() ->Action(EActionMode::Attack) ->Back() ->Back() ->Back() ->Back() ->Action(EActionMode::Patrol) ->End(); delete Builder;
而後我經過一個循環模擬行爲樹的執行。同時在各條件節點內部經過隨機數表示條件是否執行成功(具體見文末github源碼)
//模擬執行行爲樹 for (int i = 0; i < 10; ++i) { Bt->Tick(); std::cout << std::endl; }
執行結果以下,因爲隨機數的存在每次執行結果都不同
上面建立行爲樹的時候用到了構建器,下面我就介紹一下本身的構建器實現
//行爲樹構建器,用來構建一棵行爲樹,經過前序遍歷方式配合Back()和End()方法進行構建 class BehaviorTreeBuilder { public: BehaviorTreeBuilder() { } ~BehaviorTreeBuilder() { } BehaviorTreeBuilder* Sequence(); BehaviorTreeBuilder* Action(EActionMode ActionModes); BehaviorTreeBuilder* Condition(EConditionMode ConditionMode,bool IsNegation); BehaviorTreeBuilder* Selector(); BehaviorTreeBuilder* Repeat(int RepeatNum); BehaviorTreeBuilder* ActiveSelector(); BehaviorTreeBuilder* Filter(); BehaviorTreeBuilder* Parallel(EPolicy InSucess, EPolicy InFailure); BehaviorTreeBuilder* Monitor(EPolicy InSucess, EPolicy InFailure); BehaviorTreeBuilder* Back(); BehaviorTree* End(); private: void AddBehavior(Behavior* NewBehavior); private: Behavior* TreeRoot=nullptr; //用於存儲節點的堆棧 std::stack<Behavior*> NodeStack; };
BehaviorTreeBuilder* BehaviorTreeBuilder::Sequence() { Behavior* Sq=Sequence::Create(); AddBehavior(Sq); return this; } void BehaviorTreeBuilder::AddBehavior(Behavior* NewBehavior) { assert(NewBehavior); //若是沒有根節點設置新節點爲根節點 if (!TreeRoot) { TreeRoot=NewBehavior; } //不然設置新節點爲堆棧頂部節點的子節點 else { NodeStack.top()->AddChild(NewBehavior); } //將新節點壓入堆棧 NodeStack.push(NewBehavior); } BehaviorTreeBuilder* BehaviorTreeBuilder::Back() { NodeStack.pop(); return this; } BehaviorTree* BehaviorTreeBuilder::End() { while (!NodeStack.empty()) { NodeStack.pop(); } BehaviorTree* Tmp= new BehaviorTree(TreeRoot); TreeRoot = nullptr; return Tmp; }
在上面的實現中,我在每一個方法裏建立對應節點,檢測當前是否有根節點,若是沒有則將其設爲根節點,若是有則將其設爲堆棧頂部節點的子節點,隨後將其壓入堆棧,每次調用back則退棧,每一個建立節點的方法都返回this以方便調用下一個方法,最後經過End()表示行爲樹建立完成並返回構建好的行爲樹。
那麼上面就是行爲樹的介紹和實現了,下一篇咱們將對行爲樹進行優化,慢慢進入第二代行爲樹。
github地址