生活随笔
收集整理的這篇文章主要介紹了
游戏AI——行为树理论及实现
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
從上古卷軸中形形色色的人物,到NBA2K中揮灑汗水的球員,從使命召喚中詭計多端的敵人,到刺客信條中栩栩如生的人群。游戲AI幾乎存在于游戲中的每個角落,默默構建出一個令人神往的龐大游戲世界。
那么這些復雜的AI又是怎么實現的呢?下面就讓我們來了解并親手實現一下游戲AI基礎架構之一的行為樹。
行為樹簡介
行為樹是一種樹狀的數據結構,樹上的每一個節點都是一個行為。每次調用會從根節點開始遍歷,通過檢查行為的執行狀態來執行不同的節點。他的優點是耦合度低擴展性強,每個行為可以與其他行為完全獨立。目前的行為樹已經可以將幾乎任意架構(如規劃器,效用論等)應用于AI之上。
?
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)是行為樹最基礎的概念,是幾乎所有行為樹節點的基類,是一個抽象接口,而如動作條件等節點則是它的具體實現。
下面是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()方法,本接口滿足如下契約
在Update方法被首次調用前,調用一次OnInitialize函數,負責初始化等操作
Update()方法在行為樹每次更新時調用且僅調用一次。
當行為不再處于運行狀態時,調用一次OnTerminate(),并根據返回狀態不同執行不同的邏輯
為了保證契約不被打破,我們將這三個方法包裝在Tick()方法里。Tick()的實現如下
?
//update方法被首次調用前執行OnInitlize方法,每次行為樹更新時調用一次update方法? ? //當剛剛更新的行為不再運行時調用OnTerminate方法? ? if (Status != EStatus::Running)? ? {? ?? ???OnInitialize();? ? }? ? Status = Update();? ?? ? if (Status != EStatus::Running)? ? {? ?? ???OnTerminate(Status);? ? } 復制代碼
其中返回值Estatus是一個枚舉值,表示節點運行狀態。
?
enum class EStatus:uint8_t{? ? Invalid,? ?//初始狀態? ? Success,? ?//成功? ? Failure,? ?//失敗? ? Running,? ?//運行? ? Aborted,? ?//終止}; 復制代碼
動作(Action)
動作是行為樹的葉子節點,表示角色做的具體操作(如攻擊,上彈,防御等),負責改變游戲世界的狀態。動作節點可直接繼承自Behavior節點,通過實現不同的Update()方法實現不同的邏輯,在OnInitialize()方法中獲取數據和資源,在OnTerminate中釋放資源。
?
//動作基類class Action :public Behavior{public:? ? virtual void Release() { delete this; }protected:? ? Action() {}? ? virtual ~Action() {}}; 復制代碼
在這里我實現了一個動作基類,主要是為了一個公用的Release方法負責釋放節點內存空間,所有動作節點均可繼承自這個方法
條件
條件同樣是行為樹的葉子節點,用于查看游戲世界信息(如敵人是否在攻擊范圍內,周圍是否有可攀爬物體等),通過返回狀態表示條件的成功。
?
//條件基類class Condition :public Behavior{public:? ? virtual void Release() { delete this; }protected:? ? Condition(bool InIsNegation):IsNegation(InIsNegation) {}? ? virtual ~Condition() {}protected:? ? //是否取反? ? bool??IsNegation=false;}; 復制代碼
這里我實現了條件基類,一個IsNegation來標識條件是否取反(比如是否看見敵人可以變為是否沒有看見敵人)
裝飾器(Decorator)
裝飾器(Decorator)是只有一個子節點的行為,顧名思義,裝飾即是在子節點的原有邏輯上增添細節(如重復執行子節點,改變子節點返回狀態等)
?
<p>//裝飾器</p><p>class Decorator :public Behavior</p><p>{</p><p>public:</p><p>? ? virtual void AddChild(Behavior* InChild) { Child=InChild; }</p><p>protected:</p><p>? ? Decorator() {}</p><p>? ? virtual ~Decorator(){}</p><p>? ? Behavior* Child;</p><p>};</p> 復制代碼
實現了裝飾器基類,下面我們來實現下具體的裝飾器,也就是上面提到的重復執行多次子節點的裝飾器
?
<p>class Repeat :public Decorator</p><p>{</p><p>public:</p><p>? ? static Behavior* Create(int InLimited) { return new Repeat(InLimited); }</p><p>? ? virtual void Release() { Child->Release(); delete this; }</p><p>protected:</p><p>? ? Repeat(int InLimited) :Limited(InLimited) {}</p><p>? ? virtual ~Repeat(){}</p><p>? ? virtual void OnInitialize() { Count = 0; }</p><p>? ? virtual EStatus Update()override;</p><p>? ? virtual Behavior* Create() { return nullptr; }</p><p>protected:</p><p>? ? int Limited = 3;</p><p>? ? int Count = 0;</p><p>};</p> 復制代碼
正如上面提到的,Create函數負責創建節點,Release負責釋放
其中Update()方法的實現如下
?
<p>EStatus Repeat::Update()</p><p>{</p><p>? ? while (true)</p><p>? ? {</p><p>? ?? ???Child->Tick();</p><p>? ?? ???if (Child->IsRunning())return EStatus::Success;</p><p>? ?? ???if (Child->IsFailuer())return EStatus::Failure;</p><p>? ?? ???if (++Count == Limited)return EStatus::Success;</p><p>? ?? ???Child->Reset();</p><p>? ? }</p><p>? ? return EStatus::Invalid;</p><p>}</p> 復制代碼
邏輯很簡單,如果執行失敗就立即返回,二手手機拍賣執行中就繼續執行,執行成功就把計數器+1重復執行
復合行為
我們將行為樹中具有多個子節點的行為稱為復合節點,通過復合節點我們可以將簡單節點組合為更有趣更復雜的行為邏輯。
下面實現了一個符合節點的基類,將一些公用的方法放在了里面(如添加清除子節點等)
<p>//復合節點基類</p><p>class Composite:public Behavior</p><p>{??</p><p>? ? virtual void AddChild(Behavior* InChild) override{Childern.push_back(InChild);}</p><p>? ? void RemoveChild(Behavior* InChild);</p><p>? ? void ClearChild() { Childern.clear(); }</p><p>? ? virtual void Release()</p><p>? ? {</p><p>? ?? ???for (auto it : Childern)</p><p>? ?? ???{</p><p>? ?? ?? ?? ?it->Release();</p><p>? ?? ???}</p><p></p><p>? ?? ???delete this;</p><p>? ? }</p><p></p><p>protected:</p><p>? ? Composite() {}</p><p>? ? virtual ~Composite() {}</p><p>? ? using Behaviors = std::vector<Behavior*>;</p><p>? ? Behaviors Childern;</p><p>};</p> 復制代碼
順序器(Sequence)
順序器(Sequence)是復合節點的一種,它依次執行每個子行為,直到所有子行為執行成功或者有一個失敗為止。
?
<p>//順序器:依次執行所有節點直到其中一個失敗或者全部成功位置</p><p>class Sequence :public Composite</p><p>{</p><p>public:</p><p>? ? virtual std::string Name() override { return "Sequence"; }</p><p>? ? static Behavior* Create() { return new Sequence(); }</p><p>protected:</p><p>? ? Sequence() {}</p><p>? ? virtual ~Sequence(){}</p><p>? ? virtual void OnInitialize() override { CurrChild = Childern.begin();}</p><p>? ? virtual EStatus Update() override;</p><p></p><p>protected:</p><p>? ? Behaviors::iterator CurrChild;</p><p>};</p> 復制代碼
其中Update()方法的實現如下
?
<p>EStatus Sequence::Update()</p><p>{</p><p>? ? while (true)</p><p>? ? {</p><p>? ?? ???EStatus s = (*CurrChild)->Tick();</p><p>? ?? ???//如果執行成功了就繼續執行,否則返回</p><p>? ?? ???if (s != EStatus::Success)</p><p>? ?? ?? ?? ?return s;</p><p>? ?? ???if (++CurrChild == Childern.end())</p><p>? ?? ?? ?? ?return EStatus::Success;</p><p>? ? }</p><p>? ? return EStatus::Invalid;??//循環意外終止</p><p>}</p> 復制代碼
選擇器(Selector)
選擇器(Selector)是另一種常用的復合行為,它會依次執行每個子行為直到其中一個成功執行或者全部失敗為止
由于與順序器僅僅是Update函數不同,下面僅貼出Update方法
?
<p>EStatus Selector::Update()</p><p>{</p><p>? ? while (true)</p><p>? ? {</p><p>? ?? ???EStatus s = (*CurrChild)->Tick();</p><p>? ?? ???if (s != EStatus::Failure)</p><p>? ?? ?? ?? ?return s;? ?</p><p>? ?? ???//如果執行失敗了就繼續執行,否則返回</p><p>? ?? ???if (++CurrChild == Childern.end())</p><p>? ?? ?? ?? ?return EStatus::Failure;</p><p>? ? }</p><p>? ? return EStatus::Invalid;??//循環意外終止</p><p>}</p> 復制代碼
并行器(Parallel)
顧名思義,并行器(Parallel)是一種讓多個行為并行執行的節點。但仔細觀察便會發現實際上只是他們的更新函數在同一幀被多次調用而已。
?
<p>//并行器:多個行為并行執行</p><p>class Parallel :public Composite</p><p>{</p><p>public:</p><p>? ? static Behavior* Create(EPolicy InSucess, EPolicy InFailure){return new Parallel(InSucess, InFailure); }</p><p>? ? virtual std::string Name() override { return "Parallel"; }</p><p></p><p>protected:</p><p>? ? Parallel(EPolicy InSucess, EPolicy InFailure) :SucessPolicy(InSucess), FailurePolicy(InFailure) {}</p><p>? ? virtual ~Parallel() {}</p><p>? ? virtual EStatus Update() override;</p><p>? ? virtual void OnTerminate(EStatus InStatus) override;</p><p></p><p>protected:</p><p>? ? EPolicy SucessPolicy;</p><p>? ? EPolicy FailurePolicy;</p><p>};</p> 復制代碼
這里的Epolicy是一個枚舉類型,表示成功和失敗的條件(是成功或失敗一個還是全部成功或失敗)
?
<p>//Parallel節點成功與失敗的要求,是全部成功/失敗,還是一個成功/失敗</p><p>enum class EPolicy :uint8_t</p><p>{</p><p>? ? RequireOne,</p><p>? ? RequireAll,</p><p>};</p> 復制代碼
update函數實現如下
?
<p>EStatus Parallel::Update()</p><p>{</p><p>? ? int SuccessCount = 0, FailureCount = 0;</p><p>? ? int ChildernSize = Childern.size();</p><p>? ? for (auto it : Childern)</p><p>? ? {</p><p>? ?? ???if (!it->IsTerminate())</p><p>? ?? ?? ?? ?it->Tick();</p><p></p><p>? ?? ???if (it->IsSuccess())</p><p>? ?? ???{</p><p>? ?? ?? ?? ?++SuccessCount;</p><p>? ?? ?? ?? ?if (SucessPolicy == EPolicy::RequireOne)</p><p>? ?? ?? ?? ?{</p><p>? ?? ?? ?? ?? ? it->Reset();</p><p>? ?? ?? ?? ?? ? return EStatus::Success;</p><p>? ?? ?? ?? ?}</p><p>? ?? ?? ?? ?? ? </p><p>? ?? ???}</p><p></p><p>? ?? ???if (it->IsFailuer())</p><p>? ?? ???{</p><p>? ?? ?? ?? ?++FailureCount;</p><p>? ?? ?? ?? ?if (FailurePolicy == EPolicy::RequireOne)</p><p>? ?? ?? ?? ?{</p><p>? ?? ?? ?? ?? ? it->Reset();</p><p>? ?? ?? ?? ?? ? return EStatus::Failure;</p><p>? ?? ?? ?? ?}? ?? ? </p><p>? ?? ???}</p><p>? ? }</p><p></p><p>? ? if (FailurePolicy == EPolicy::RequireAll&&FailureCount == ChildernSize)</p><p>? ? {</p><p>? ?? ???for (auto it : Childern)</p><p>? ?? ???{</p><p>? ?? ?? ?? ?it->Reset();</p><p>? ?? ???}</p><p>? ?? ???</p><p>? ?? ???return EStatus::Failure;</p><p>? ? }</p><p>? ? if (SucessPolicy == EPolicy::RequireAll&&SuccessCount == ChildernSize)</p><p>? ? {</p><p>? ?? ???for (auto it : Childern)</p><p>? ?? ???{</p><p>? ?? ?? ?? ?it->Reset();</p><p>? ?? ???}</p><p>? ?? ???return EStatus::Success;</p><p>? ? }</p><p></p><p>? ? return EStatus::Running;</p><p>}</p> 復制代碼
在代碼中,并行器每次更新都執行每一個尚未終結的子行為,并檢查成功和失敗條件,如果滿足則立即返回。
另外,當并行器滿足條件提前退出時,所有正在執行的子行為也應該立即被終止,我們在OnTerminate()函數中調用每個子節點的終止方法
?
<p>void Parallel::OnTerminate(EStatus InStatus)</p><p>{</p><p>? ???for (auto it : Childern)</p><p>? ? {</p><p>? ?? ???if (it->IsRunning())</p><p>? ?? ?? ?? ?it->Abort();</p><p>? ? }</p><p>}</p> 復制代碼
監視器(Monitor)
監視器是并行器的應用之一,通過在行為運行過程中不斷檢查是否滿足某條件,如果不滿足則立刻退出。將條件放在并行器的尾部即可。
主動選擇器
主動選擇器是選擇器的一種,與普通的選擇器不同的是,主動選擇器會不斷的主動檢查已經做出的決策,并不斷的嘗試高優先級行為的可行性,當高優先級行為可行時胡立即打斷低優先級行為的執行(如正在巡邏的過程中發現敵人,即時中斷巡邏,立即攻擊敵人)。
其Update()方法和OnInitialize方法實現如下
?
<p>//初始化時將CurrChild初始化為子節點的末尾</p><p>virtual void OnInitialize() override { CurrChild = Childern.end(); }</p><p></p><p>? ? EStatus ActiveSelector::Update()</p><p>? ? {</p><p>? ?? ???//每次執行前先保存的當前節點</p><p>? ?? ???Behaviors::iterator Previous = CurrChild;</p><p>? ?? ???//調用父類OnInlitiallize函數讓選擇器每次重新選取節點</p><p>? ?? ???Selector::OnInitialize();</p><p>? ?? ???EStatus result = Selector::Update();</p><p>? ?? ???//如果優先級更高的節點成功執行或者原節點執行失敗則終止當前節點的執行</p><p>? ?? ???if (Previous != Childern.end()&CurrChild != Previous)</p><p>? ?? ???{</p><p>? ?? ?? ?? ?(*Previous)->Abort();? ?</p><p>? ?? ???}</p><p></p><p>? ?? ???return result;</p><p>? ? }</p> 復制代碼
示例
這里我創建了一名角色,該角色一開始處于巡邏狀態,一旦發現敵人,先檢查自己生命值是否過低,如果是就逃跑,否則就攻擊敵人,攻擊過程中如果生命值過低也會中斷攻擊,立即逃跑,如果敵人死亡則立即停止攻擊,這里我們使用了構建器來創建了一棵行為樹,關于構建器的實現后面會講到,這里每個函數創建了對應函數名字的節點,
?
<p>//構建行為樹:角色一開始處于巡邏狀態,一旦發現敵人,先檢查自己生命值是否過低,如果是就逃跑,否則就攻擊敵人,攻擊過程中如果生命值過低也會中斷攻擊,立即逃跑,如果敵人死亡則立即停止攻擊</p><p>? ? BehaviorTreeBuilder* Builder = new BehaviorTreeBuilder();</p><p>? ? BehaviorTree* Bt=Builder</p><p>? ?? ???->ActiveSelector()</p><p>? ?? ?? ?? ?->Sequence()</p><p>? ?? ?? ?? ?? ? ->Condition(EConditionMode::IsSeeEnemy,false)</p><p>? ?? ?? ?? ?? ?? ?? ?->Back()? ?? ? </p><p>? ?? ?? ?? ?? ? ->ActiveSelector()</p><p>? ?? ?? ?? ?? ?? ?? ?-> Sequence()</p><p>? ?? ?? ?? ?? ?? ?? ?? ???->Condition(EConditionMode::IsHealthLow,false)</p><p>? ?? ?? ?? ?? ?? ?? ?? ?? ?? ? ->Back()</p><p>? ?? ?? ?? ?? ?? ?? ?? ???->Action(EActionMode::Runaway)</p><p>? ?? ?? ?? ?? ?? ?? ?? ?? ?? ???->Back()</p><p>? ?? ?? ?? ?? ?? ?? ?? ???->Back()</p><p>? ?? ?? ?? ?? ?? ???->Monitor(EPolicy::RequireAll,EPolicy::RequireOne)</p><p>? ?? ?? ?? ?? ?? ?? ?? ???->Condition(EConditionMode::IsEnemyDead,true)</p><p>? ?? ?? ?? ?? ?? ?? ?? ?? ?? ???->Back()</p><p>? ?? ?? ?? ?? ?? ?? ?? ???->Action(EActionMode::Attack)</p><p>? ?? ?? ?? ?? ?? ?? ?? ?? ?? ???->Back()</p><p>? ?? ?? ?? ?? ?? ?? ?? ???->Back()</p><p>? ?? ?? ?? ?? ?? ???->Back()</p><p>? ?? ?? ?? ?? ? ->Back()</p><p>? ?? ?? ?? ?->Action(EActionMode::Patrol)</p><p>? ? ->End();</p><p></p><p>? ? delete Builder;</p> 復制代碼
然后我通過一個循環模擬行為樹的執行。同時在各條件節點內部通過隨機數表示條件是否執行成功(具體見文末github源碼)
?
<p>? ? //模擬執行行為樹</p><p>? ? for (int i = 0; i < 10; ++i)</p><p>? ? {</p><p>? ?? ???Bt->Tick();</p><p>? ?? ???std::cout << std::endl;</p><p>? ? }</p> 復制代碼
執行結果如下,由于隨機數的存在每次執行結果都不一樣
?
構建器的實現
上面創建行為樹的時候用到了構建器,下面我就介紹一下自己的構建器實現
?
<p>//行為樹構建器,用來構建一棵行為樹,通過前序遍歷方式配合Back()和End()方法進行構建</p><p>class BehaviorTreeBuilder</p><p>{</p><p>public:</p><p>? ? BehaviorTreeBuilder() { }</p><p>? ? ~BehaviorTreeBuilder() { }</p><p>? ? BehaviorTreeBuilder* Sequence();</p><p>? ? BehaviorTreeBuilder* Action(EActionMode ActionModes);</p><p>? ? BehaviorTreeBuilder* Condition(EConditionMode ConditionMode,bool IsNegation);</p><p>? ? BehaviorTreeBuilder* Selector();</p><p>? ? BehaviorTreeBuilder* Repeat(int RepeatNum);</p><p>? ? BehaviorTreeBuilder* ActiveSelector();</p><p>? ? BehaviorTreeBuilder* Filter();</p><p>? ? BehaviorTreeBuilder* Parallel(EPolicy InSucess, EPolicy InFailure);</p><p>? ? BehaviorTreeBuilder* Monitor(EPolicy InSucess, EPolicy InFailure);</p><p>? ? BehaviorTreeBuilder* Back();</p><p>? ? BehaviorTree* End();</p><p></p><p>private:</p><p>? ? void AddBehavior(Behavior* NewBehavior);</p><p></p><p>private:</p><p>? ? Behavior* TreeRoot=nullptr;</p><p>? ? //用于存儲節點的堆棧</p><p>? ? std::stack<Behavior*> NodeStack;</p><p>};</p><p>BehaviorTreeBuilder* BehaviorTreeBuilder::Sequence()</p><p>{</p><p>? ? Behavior* Sq=Sequence::Create();</p><p>? ? AddBehavior(Sq);</p><p>? ? return this;</p><p>}</p><p></p><p>void BehaviorTreeBuilder::AddBehavior(Behavior* NewBehavior)</p><p>{</p><p>? ? assert(NewBehavior);</p><p>? ? //如果沒有根節點設置新節點為根節點</p><p>? ? if (!TreeRoot)</p><p>? ? {</p><p>? ?? ???TreeRoot=NewBehavior;</p><p>? ? }</p><p>? ? //否則設置新節點為堆棧頂部節點的子節點</p><p>? ? else</p><p>? ? {</p><p>? ?? ???NodeStack.top()->AddChild(NewBehavior);</p><p>? ? }</p><p></p><p>? ? //將新節點壓入堆棧</p><p>? ? NodeStack.push(NewBehavior);</p><p>}</p><p></p><p>BehaviorTreeBuilder* BehaviorTreeBuilder::Back()</p><p>{</p><p>? ? NodeStack.pop();</p><p>? ? return this;</p><p>}</p><p></p><p>BehaviorTree* BehaviorTreeBuilder::End()</p><p>{</p><p>? ? while (!NodeStack.empty())</p><p>? ? {</p><p>? ?? ???NodeStack.pop();</p><p>? ? }</p><p>? ? BehaviorTree* Tmp= new BehaviorTree(TreeRoot);</p><p>? ? TreeRoot = nullptr;</p><p>? ? return Tmp;</p><p>}</p> 復制代碼
在上面的實現中,我在每個方法里創建對應節點,檢測當前是否有根節點,如果沒有則將其設為根節點,如果有則將其設為堆棧頂部節點的子節點,隨后將其壓入堆棧,每次調用back則退棧,每個創建節點的方法都返回this以方便調用下一個方法,最后通過End()表示行為樹創建完成并返回構建好的行為樹。
那么上面就是行為樹的介紹和實現了,下一篇我們將對行為樹進行優化,慢慢進入第二代行為樹。
總結
以上是生活随笔為你收集整理的游戏AI——行为树理论及实现的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。