<cite id="ffb66"></cite><cite id="ffb66"><track id="ffb66"></track></cite>
      <legend id="ffb66"><li id="ffb66"></li></legend>
      色婷婷久,激情色播,久久久无码专区,亚洲中文字幕av,国产成人A片,av无码免费,精品久久国产,99视频精品3
      網易首頁 > 網易號 > 正文 申請入駐

      游戲AI行為決策——Behavior Tree(行為樹)

      0
      分享至


      【USparkle專欄】如果你深懷絕技,愛“搞點研究”,樂于分享也博采眾長,我們期待你的加入,讓智慧的火花碰撞交織,讓知識的傳遞生生不息!

      這是侑虎科技第1825篇文章,感謝作者狐王駕虎供稿。歡迎轉發(fā)分享,未經作者授權請勿轉載。如果您有任何獨到的見解或者發(fā)現也歡迎聯系我們,一起探討。(QQ群:793972859)

      作者主頁:

      https://home.cnblogs.com/u/OwlCat

      一、前言

      行為樹,是目前游戲中應用較為廣泛的一種行為決策模型。這離不開它成熟的可視化編輯工具,例如Unity商城中的「Behaviour Designer」,甚至是虛幻引擎也自帶此類編輯工具。而且它的設計邏輯并不復雜,其所利用的樹狀結構,很符合人的思考方式。

      接下來,我們會先對它的運作邏輯進行介紹,然后再試圖用代碼實現它。樹狀結構在不借助可視化工具的情況下是不容易呈現清楚的,這里我借鑒了Steve Rabin的《Game AI Pro》[1]中行為樹的實現方式,利用代碼縮進稍稍實現了一些可視化(本教程使用C#代碼實現)。下面我們就開始吧!

      二、運動邏輯

      1. 根節(jié)點驅動

      如果你已經了解「有限狀態(tài)機(FSM)」的話,你應該知道,有限狀態(tài)機在運作時常常會停留在一個狀態(tài)中,不斷執(zhí)行該狀態(tài)的邏輯,直至接受到狀態(tài)轉移的指令才變化到其它狀態(tài)。而行為樹則是會不斷從根節(jié)點向下搜索,即「根節(jié)點驅動」,來找到合適的「動作」執(zhí)行,執(zhí)行完畢后會再回到根節(jié)點重復這個過程。以下面這個「怪物攻擊玩家」行為樹為例:


      假設「攻擊」動作的邏輯是「向玩家揮一拳」,現在怪物發(fā)現玩家且玩家在攻擊范圍內。那么,按照行為樹的邏輯,它會經過「戰(zhàn)斗 ? 試圖攻擊 ? 攻擊」一路下來,最終向玩家揮出一拳。

      至此,「攻擊」就算是完成了,若是在狀態(tài)機中,攻擊也算是一種狀態(tài)的話,怪物必然會停留于此,等待下一幀時再揮出一拳。但在行為樹中呢?它確實也會在下一幀時再揮出一拳,只是會再經過「戰(zhàn)斗 ? 試圖攻擊 ? 攻擊」這個過程,也就是前面所說的,從根節(jié)點再次出發(fā)。

      你可能也發(fā)現了,這明顯是多此一舉的行為,它確實可以算是行為樹的小缺點。但其實行為樹的深度通常并不會太深,多幾次布爾判斷或小遍歷倒也不打緊;而且有一種事件驅動的行為樹實現方法,能以“空間換時間”的手段改善這種情況,感興趣的同學可以去了解一下。

      2. 特殊的節(jié)點

      行為樹的一大特點,就是將條件與行為本身進行了分離

      什么意思呢?我們仍以上面那張圖為例,只是稍稍修改下表現方式(也更接近行為樹真正的樣子):


      好像多了幾個圈?那現在,請你將這些圈也視為和「攻擊」一樣類型的節(jié)點。這樣一來,我們將「判斷邏輯」、「順序遍歷(圖中的紅色箭頭)」、「動作」都用節(jié)點來表示了。這有什么好處呢?好處就在于我們可以將它們進行各種各樣的組合!比如,如果有一個怪物比較膽小,遇到玩家后會逃跑,我們就可以用圖中的「發(fā)現玩家」+「移動到該位置(逃跑的位置)」來實現;也可以配合新的節(jié)點來組合,比如「已知玩家最后出現的位置」+ 新節(jié)點:「朝指定位置開火」,就可以實現遠程追擊。

      總之,正是因為行為樹有一系列特殊的節(jié)點,使得開發(fā)者可以降低各個行為之間的關聯(也就是解耦),再配合上樹狀結構的特點,開發(fā)者可以靈活地進行組裝,實現節(jié)點的重復利用,避免寫重復的代碼,提高了開發(fā)效率。(用過有限狀態(tài)機寫游戲AI的同學一定能體會到這點的好處。)

      三、代碼實現

      現在,我們就一起來實現行為樹,先看看我們有哪些要實現的(它們的具體含義后面會解釋):

      1. 組合節(jié)點(Composite),指有多個子節(jié)點的特殊節(jié)點,具體包括:

      a. 順序器(Sequence)

      b. 選擇器(Selector)

      c. 并行器(Parallel)

      d. 過濾器(Filter)

      e. 主動選擇器(ActiveSelector)

      f. 監(jiān)視器(Monitor)

      2. 修飾節(jié)點(Decorator),指僅有一個子節(jié)點的特殊節(jié)點,具體包括:

      a. 取反器(Inverter)

      b. 重復執(zhí)行器(Repeat)

      3. 動作節(jié)點,指可以自定義的節(jié)點,比如「攻擊」、「巡視」之類的。

      1. 基礎準備

      正式實現它們之前,我們應當準備它們的基類,畢竟它們都是節(jié)點,有一些共性的東西可以提取出來,這樣可以減少一些重復的代碼。

      /// /// 運行結果狀態(tài)的枚舉 /// publicenumEStatus {     //失敗,成功,運行中,中斷,無效     Failure, Success, Running, Aborted, Invalid } /// /// 行為樹節(jié)點基類 /// publicabstractclassBehavior {     publicboolIsTerminated=> IsSuccess || IsFailure;//是否運行結束     publicboolIsSuccess=> status == EStatus.Success;//是否成功     publicboolIsFailure=> status == EStatus.Failure;//是否失敗     publicboolIsRunning=> status == EStatus.Running;//是否正在運行     protected EStatus status;//運行狀態(tài)     publicBehavior()     {         status = EStatus.Invalid;     }     //當進入該節(jié)點時才會執(zhí)行一次的函數,類似FSM的OnEnter     protected virtual voidOnInitialize() {}     //該節(jié)點的運行邏輯,會時時返回執(zhí)行結果的狀態(tài),類似FSM的OnUpdate     protectedabstract EStatus OnUpdate();     //當運行結束時才會執(zhí)行一次的函數,類似FSM的OnExit     protected virtual voidOnTerminate() {}     //節(jié)點運行,從中應該更能了解上述三個函數的功能     //它會返回本次調用的結果,為父節(jié)點接下來的運行提供依據     public EStatus Tick()     {         if(!IsRunning)             OnInitialize();         status = OnUpdate();         if(!IsRunning)             OnTerminate();         return status;     }     //添加子節(jié)點     public virtual voidAddChild(Behavior child) {}     //重置該節(jié)點的運作     publicvoidReset()     {         status = EStatus.Invalid;     }     //強行打斷該節(jié)點的運作     publicvoidAbort()     {         OnTerminate();         status = EStatus.Aborted;     } }

      2. 組合節(jié)點

      首先實現「組合節(jié)點」這個基類。

      using System.Collections.Generic; publicabstractclassComposite : Behavior {     protected LinkedList children; //用雙向鏈表構建子節(jié)點列表     publicComposite()     {         children = newLinkedList ();     }     //移除指定子節(jié)點     public virtual voidRemoveChild(Behavior child)     {         children.Remove(child);     }     publicvoidClearChildren()//清空子節(jié)點列表     {         children.Clear();     }     public override voidAddChild(Behavior child)//添加子節(jié)點     {         //默認是尾插入,如:0插入「1,2,3」中,就會變成「1,2,3,0」         children.AddLast(child);     } }

      接下來就是具體類的實現了,我會對這些節(jié)點的功能作出解釋(有參考虛幻引擎的行為樹節(jié)點介紹),再進行代碼實現。

      a. 順序器

      邏輯上來講(非代碼結構),它長這樣:


      順序器會按從左到右的順序執(zhí)行其子節(jié)點。當其中一個子節(jié)點執(zhí)行失敗時,將停止執(zhí)行,也就是說,任一子節(jié)點失敗,順序器就會失敗。只有所有子節(jié)點運行都成功,順序器才算成功。

      public classSequence : Composite {     protected LinkedListNode currentChild; //當前運行的子節(jié)點     protected override voidOnInitialize()     {         currentChild = children.First;//從第一個子節(jié)點開始     }     protected override EStatus OnUpdate()     {         while(true)         {             vars= currentChild.Value.Tick();//記錄子節(jié)點運行返回的結果狀態(tài)             /*             如果子節(jié)點運行,還沒有成功,就直接返回該結果。             是「運行中」那就表明本節(jié)點也是運行中,有記錄當前節(jié)點,下次還會繼續(xù)執(zhí)行;             是「失敗」就表明本節(jié)點也運行失敗了,下次會再經歷OnInitialize,從頭開始。             */             if( s != EStatus.Success)                 return s;             //如果運行成功,就換到下一個子節(jié)點             currentChild = currentChild.Next;             //如果全都成功運行完了,就返回「成功」             if(currentChild == null)                 return EStatus.Success;         }     } }

      b. 選擇器

      從邏輯上講,它的結構應該長這樣:


      每次只會選擇一個可以運行的子節(jié)點來運行。

      但從代碼上來說,選擇器的結構和順序器完全一致,只是運行邏輯變化了:按從左到右的順序執(zhí)行其子節(jié)點。當其中一個子節(jié)點執(zhí)行成功時,就停止執(zhí)行,也就是說,任一子節(jié)點成功運行,就算運行成功。只有所有子節(jié)點運行都失敗,選擇器才算運行失敗。

      所以,只要簡單地繼承「順序器」并修改它的OnUpdate邏輯,就能得到選擇器啦!

      public classSelector : Sequence {     protected override EStatus OnUpdate()     {         while(true)         {             vars= currentChild.Value.Tick();             if( s != EStatus.Failure)                 return s;             currentChild = currentChild.Next;             if(currentChild == null)                 return EStatus.Failure;         }     } }

      c. 并行器

      并行器,它會同時執(zhí)行所有子節(jié)點。


      可這樣就有問題了:

      1. 怎么「同時」運行,要用多線程嗎?

      2. 同時執(zhí)行必然會返回多個結果,該如何確定最終返回運行結果呢?

      對于問題1,是不用多線程的,我們只要在一幀中把所有子節(jié)點都執(zhí)行一次,就算是「同時」執(zhí)行了;

      對于問題2,我們可以根據需求自行設置并行器成功或失敗的標準,一般可分為「全都」和「只要有一個」。

      看看代碼就知道了:

      public classParallel : Composite {     protected Policy mSuccessPolicy;//成功的標準     protected Policy mFailurePolicy;//失敗的標準     ///     /// Parallel節(jié)點成功與失敗的要求,是全部成功/失敗,還是一個成功/失敗     ///     publicenumPolicy     {         RequireOne, RequireAll,     }     //構造函數初始化時,會要求給定成功和失敗的標準     publicParallel(Policy mSuccessPolicy, Policy mFailurePolicy)     {         this.mSuccessPolicy = mSuccessPolicy;         this.mFailurePolicy = mFailurePolicy;     }     protected override EStatus OnUpdate()     {         intsuccessCount=0, failureCount = 0;//記錄執(zhí)行成功和執(zhí)行失敗的節(jié)點數         varb= children.First;//從第一個子節(jié)點開始         varsize= children.Count;         for (inti=0; i < size; ++i)         {             varbh= b.Value;             if(!bh.IsTerminated)//如果該子節(jié)點還沒運行結束,就運行它                 bh.Tick();             b = b.Next;             if(bh.IsSuccess)//該子節(jié)點運行結束后,如果運行成功了             {                 ++successCount;//成功執(zhí)行的節(jié)點數+1                 //如果是「只要有一個」標準的話,那就可以返回結果了                 if(mSuccessPolicy == Policy.RequireOne)                     return EStatus.Success;             }             if(bh.IsFailure)//該子節(jié)點運行失敗的情況同理             {                 ++failureCount;                 if(mFailurePolicy == Policy.RequireOne)                     return EStatus.Failure;             }         }         //如果是「全都」標準的話,就需要比對當前成功/失敗個數與總子節(jié)點數         if(mFailurePolicy == Policy.RequireAll && failureCount == size)             return EStatus.Failure;         if(mSuccessPolicy == Policy.RequireAll && successCount == size)             return EStatus.Success;         return EStatus.Running;     }     //結束函數,只要簡單地把所有子節(jié)點設為「中斷」就可以了     protected override voidOnTerminate()     {         foreach(var b in children)         {             if(b.IsRunning)                 b.Abort();         }     } }

      至此,基礎的組合節(jié)點就講完了,但還有一些常用的組合節(jié)點,它們是在這些基礎的組合節(jié)點上稍稍變形而來的。

      d. 過濾器

      過濾器,由順序器改造而來,就是在進入子節(jié)點之前,加了些條件判斷,如果不滿足任意一個,就不能執(zhí)行后續(xù)的子節(jié)點,此即為「過濾」。


      你會發(fā)現,它們甚至可以直接看作是在同一個列表里,只是「條件」都在前半段,真正要運行的子節(jié)點都在后半段。代碼也確實是這么設計的:

      public classFilter : Sequence {     publicvoidAddCondition(Behavior condition)//添加條件,就用頭插入     {         children.AddFirst(condition);     }     publicvoidAddAction(Behavior action)//添加動作,就用尾插入     {         children.AddLast(action);     } }

      e. 主動選擇器

      假設,某人正在砍樹,但突然電鋸故障了,迫不得已,他只能換斧頭來砍樹;但突然被扔在一旁的電鋸又好起來了,那他還會繼續(xù)費力的用斧子來砍樹嗎?


      我想,只要他還沒因為中暑把CPU干燒就不會這么做。但他如果是一個NPC的話,按照之前「選擇器」的邏輯,確實會出現這種荒謬的行為。所以我們需要一個特殊的選擇器,能始終執(zhí)行最具優(yōu)先級的子節(jié)點,甚至可以因此打斷正在運行的低優(yōu)先級的子節(jié)點。

      我們只需對「選擇器」的OnUpdate進行改造,在每次調用時,也從頭到尾進行選擇(默認高優(yōu)先級的行為在前面)即可:

      public classActiveSelector: Selector {     protected override EStatus OnUpdate()     {         varprev= currentChild;         base.OnInitialize();//注意這里,currentChild 會被賦值為children.First         varres= base.OnUpdate();//按Selector的OnUpdate執(zhí)行,順序遍歷選擇         /*         只要不是遍歷結束或可執(zhí)行節(jié)點不變,都應該中斷上一次執(zhí)行的節(jié)點,無論優(yōu)先是高是低。         因為如果當前優(yōu)先級比之前的高,理應中斷之前的;         而如果比之前的低,那就證明之前高優(yōu)先級的行為無法繼續(xù)了,         否則怎么會輪到現在的低優(yōu)先級的行為呢?所以也應中斷它。         */         if(prev != null && currentChild != prev)             prev.Value.Abort();         return res;     } }

      f. 監(jiān)視器

      監(jiān)視器是對「并行器」的改造,改造的目的也是為了能持續(xù)檢查并行行為的條件。


      從邏輯上看,它有兩個子樹,一邊負責條件,一邊負責具體行為。這種分離方式是合理的,防止了同步和爭用問題,因為只有一個子樹將運行對世界進行更改的操作。

      從代碼上來說,其實它的改造方法和「過濾器」完全一致,因為我們完全可以把這兩個子樹看作一個,只是前半部分全是條件,后半部分全是具體動作而已:

      public classMonitor: Parallel {     publicMonitor(Policy mSuccessPolicy, Policy mFailurePolicy)      : base(mSuccessPolicy, mFailurePolicy)     {     }     publicvoidAddCondition(Behavior condition)     {         children.AddFirst(condition);     }     publicvoidAddAction(Behavior action)     {         children.AddLast(action);     } }

      終于,所有常用的組合節(jié)點我們都實現了,下面就該講講修飾節(jié)點了。

      3. 修飾節(jié)點

      修飾節(jié)點只有一個子節(jié)點,因為這樣就足夠了,想要多個條件只需要配合組合節(jié)點就可以實現。所以它的基類也十分簡單:

      public abstract class Decorator : Behavior {     protected Behavior child;     public override void AddChild(Behavior child)     {         this.child = child;     } }

      修飾節(jié)點理論上可以擴展成各種「條件」,完全取決于開發(fā)者的需求。所以這里,我們就不在這方面展開,就說說幾個比較實用的修飾器吧。

      a. 取反器

      簡單地對子節(jié)點執(zhí)行結果的「成功」或「失敗」進行顛倒而已,但這小小的功能卻能幫我們省去很多冗余的代碼,比如有「存在敵人」的條件節(jié)點時,再想要「不存在敵人」的條件節(jié)點,就不必去寫代碼了,只需要在「存在敵人」前加上這樣一個「取反器」就可以了。

      它的實現也很簡單:

      public class Inverter : Decorator {     protected override EStatus OnUpdate()     {         child.Tick();         if(child.IsFailure)             return EStatus.Success;         if(child.IsSuccess)             return EStatus.Failure;         return EStatus.Running;     } }

      b. 重復執(zhí)行器

      重復執(zhí)行某(些)行為也是常見的動作需求,這些動作往往都是已實現的單一動作,例如,有了「點射」動作,我們就可以僅給它加上一個重復執(zhí)行器,就可以實現「掃射」了。

      重復執(zhí)行器的邏輯也很直接:

      public classRepeat : Decorator {     privateint conunter;//當前重復次數     privateint limit;//重復限度     publicRepeat(int limit)     {         this.limit = limit;     }     protected override voidOnInitialize()     {         conunter = 0;//進入時,將次數清零     }     protected override EStatus OnUpdate()     {         while (true)         {             child.Tick();             if(child.IsRunning)                 return EStatus.Running;             if(child.IsFailure)                 return EStatus.Failure;             //子節(jié)點執(zhí)行成功,就增加一次計算,達到設定限度才返回成功             if(++conunter >= limit)                 return EStatus.Success;         }     } }

      4. 動作節(jié)點

      動作節(jié)點也是自由發(fā)揮的節(jié)點,具體功能隨需求,但有一點要嚴格遵守——不能有子節(jié)點。

      要實現動作節(jié)點,只要繼承并重寫節(jié)點基類就可以了,例如一個打印一些字的節(jié)點:

      public class DebugNode : Behavior {     private string word;     public DebugNode(string word)     {         this.word = word;     }     protected override EStatus OnUpdate()     {         Debug.Log(word);         return EStatus.Success;     } }

      5. 構建與運行

      節(jié)點部分我們都講完了,現在就開始實現樹的構建與運行了。

      我們先實現樹:

      public classBehaviorTree {     publicboolHaveRoot=> root != null;     private Behavior root;//根節(jié)點     publicBehaviorTree(Behavior root)     {         this.root = root;     }     publicvoidTick()     {         root.Tick();     }     publicvoidSetRoot(Behavior root)     {         this.root = root;     } }

      很簡短吧,實際上樹只需要記錄根節(jié)點就可以了,其余節(jié)點都會由各個節(jié)點用自己的子節(jié)點/子節(jié)點列表存儲。這么說來,其實一個普通的節(jié)點,也可以視為一棵樹嗎?是的,只是將二者進行區(qū)分還是很有必要的,省得邏輯混亂。它的運行,也只是簡單地遞歸調用子節(jié)點的Tick。當然,這只是對于簡單實現的行為樹來說是這樣而已,至于更加成熟的實現方式(如之前提到的事件驅動的行為樹)就不是這樣了。

      言歸正傳,那這只是一棵樹而已,怎么向它增加節(jié)點呢?這里我們再單獨造一個管理樹邏輯的類:

      public partial classBehaviorTreeBuilder {     private readonly Stack nodeStack; //構建樹結構用的棧     private readonly BehaviorTree bhTree;//構建的樹     publicBehaviorTreeBuilder()     {         bhTree = newBehaviorTree(null);//構造一個沒有根的樹         nodeStack = newStack (); //初始化構建棧     }     privatevoidAddBehavior(Behavior behavior)     {         if (bhTree.HaveRoot)//有根節(jié)點時,加入構建棧         {             nodeStack.Peek().AddChild(behavior);         }         else//當樹沒根時,新增得節(jié)點視為根節(jié)點         {             bhTree.SetRoot(behavior);         }         //只有組合節(jié)點和修飾節(jié)點需要進構建堆         if (behavior is Composite || behavior is Decorator)         {             nodeStack.Push(behavior);         }     }     publicvoidTreeTick()     {         bhTree.Tick();     }     public BehaviorTreeBuilder Back()     {         nodeStack.Pop();         returnthis;     }     public BehaviorTree End()     {         nodeStack.Clear();         return bhTree;     } }

      這樣就實現了樹構建,還把調用也再包裝了一層。用BehaviorTreeBuilder,就可以既構建又運行了。接下來,我們來詳細說說里面的邏輯:

      最開始用AddBehavior函數添加一個節(jié)點,它無疑會成為根:


      再添加一個0,它會變成這樣:


      再添加同理:


      而當我們想要為0添加第二個子節(jié)點時,只需要先調用Back,Back會使棧頂元素彈出:


      之后,再調用添加函數,由于該函數是向棧頂元素添加子節(jié)點,所以就變成了:


      通過AddBehavior和Back,我們就可以設置樹的結構。如果又想給1添加子節(jié)點該怎么辦?可以直接在調用Back之前的代碼里,加上給1節(jié)點添加子節(jié)點的代碼。

      配合縮進,我們可以勉強實現可視化,至少有層次感:

      public classTest0 : MonoBehaviour {     BehaviorTreeBuilder builder;     privatevoidAwake()     {         builder = newBehaviorTreeBuilder();     }     privatevoidStart()     {         builder.Repeat(3)                     .Sequence()                         .DebugNode("Ok,")//由于動作節(jié)點不進棧,所以不用Back                         .DebugNode("It's ")                         .DebugNode("My time")                     .Back()                 .End();         builder.TreeTick();     } }

      這里的Repeat,實際上就是對添加節(jié)點的包裝,以下是該類的完整代碼:

      public partial classBehaviorTreeBuilder {     private readonly Stack nodeStack;     private readonly BehaviorTree bhTree;     publicBehaviorTreeBuilder()     {         bhTree = newBehaviorTree(null);         nodeStack = newStack ();     }     privatevoidAddBehavior(Behavior behavior)     {         if (bhTree.HaveRoot)         {             nodeStack.Peek().AddChild(behavior);         }         else         {             bhTree.SetRoot(behavior);         }         if (behavior is Composite || behavior is Decorator)         {             nodeStack.Push(behavior);         }     }     publicvoidTreeTick()     {         bhTree.Tick();     }     public BehaviorTreeBuilder Back()     {         nodeStack.Pop();         returnthis;     }     public BehaviorTree End()     {         nodeStack.Clear();         return bhTree;     }     //---------包裝各節(jié)點---------     public BehaviorTreeBuilder Sequence()     {         vartp=newSequence();         AddBehavior(tp);         returnthis;     }     public BehaviorTreeBuilder Seletctor()     {         vartp=newSelector();         AddBehavior(tp);         returnthis;     }     public BehaviorTreeBuilder Filter()     {         vartp=newFilter();         AddBehavior(tp);         returnthis;     }     public BehaviorTreeBuilder Parallel(Parallel.Policy success, Parallel.Policy failure)     {         vartp=newParallel(success, failure);         AddBehavior(tp);         returnthis;     }     public BehaviorTreeBuilder Monitor(Parallel.Policy success, Parallel.Policy failure)     {         vartp=newMonitor(success, failure);         AddBehavior(tp);         returnthis;     }     public BehaviorTreeBuilder ActiveSelector()     {         vartp=newActiveSelector();         AddBehavior(tp);         returnthis;     }     public BehaviorTreeBuilder Repeat(int limit)     {         vartp=newRepeat(limit);         AddBehavior(tp);         returnthis;     }     public BehaviorTreeBuilder Inverter()     {         vartp=newInverter();         AddBehavior(tp);         returnthis;     } }

      你可能也注意到了,這個類是partial類,它還有另一部分內容,我將其與DebugNode寫在同一處:

      public classDebugNode : Behavior {     private string word;     publicDebugNode(string word)     {         this.word = word;     }     protected override EStatus OnUpdate()     {         Debug.Log(word);         return EStatus.Success;     } } public partial classBehaviorTreeBuilder {     public BehaviorTreeBuilder DebugNode(string word)     {         varnode=newDebugNode(word);         AddBehavior(node);         returnthis;     } }

      個人還沒想到好辦法,這種包裝確實好看,但要另寫這樣的函數屬實有點繁瑣。倒也可以修改AddBehavior類讓它也返回BehaviorTreeBuilder,但這樣在構建樹時,代碼會變得有些長,總之看個人選擇。如果你的Test0能輸出三次"Ok,It's My time",那就說明你的構建沒錯。

      內容到這也差不多了,個人其實還并沒有正式用過這個行為樹來做游戲,當然還有其它的行為決策方法我比較青睞,比如「分層任務網絡(HTN)」,個人就用的比較多。在我個人的一些游戲中就有使用到,有時間的話,也可以和大家交流下它的運行和實現。

      參考:

      [1]《Game AI Pro》


      https://www.gameaipro.com/

      文末,再次感謝狐王駕虎 的分享, 作者主頁:https://home.cnblogs.com/u/OwlCat, 如果您有任何獨到的見解或者發(fā)現也歡迎聯系我們,一起探討。(QQ群: 793972859 )。

      近期精彩回顧

      【學堂上新】

      【學堂上新】

      【萬象更新】

      【萬象更新】

      特別聲明:以上內容(如有圖片或視頻亦包括在內)為自媒體平臺“網易號”用戶上傳并發(fā)布,本平臺僅提供信息存儲服務。

      Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

      相關推薦
      熱點推薦
      笑死了,這才是普通人最真實的存款

      笑死了,這才是普通人最真實的存款

      夜深愛雜談
      2025-12-12 19:25:44
      美軍押寶特朗普級戰(zhàn)列艦?為何中國一笑而過,美國卻很認真?

      美軍押寶特朗普級戰(zhàn)列艦?為何中國一笑而過,美國卻很認真?

      兵國大事
      2025-12-24 00:05:10
      她曾一年換7任男友,風流成性未婚生女,如今43歲無人敢娶

      她曾一年換7任男友,風流成性未婚生女,如今43歲無人敢娶

      小熊侃史
      2025-12-20 11:01:12
      冬至后包餃子,3種餡是首選,時令蔬菜勝過韭菜,鮮嫩多汁又營養(yǎng)

      冬至后包餃子,3種餡是首選,時令蔬菜勝過韭菜,鮮嫩多汁又營養(yǎng)

      江江食研社
      2025-12-24 13:30:06
      土耳其中部農業(yè)區(qū)出現近700個巨大天坑,疑因氣候變化導致

      土耳其中部農業(yè)區(qū)出現近700個巨大天坑,疑因氣候變化導致

      環(huán)球網資訊
      2025-12-24 13:51:56
      藍營支持度暴跌5.2%!蔡正元敲響警鐘:別再用民調不準自我安慰了

      藍營支持度暴跌5.2%!蔡正元敲響警鐘:別再用民調不準自我安慰了

      達文西看世界
      2025-12-24 21:18:45
      全新一代寶馬5系曝光,外觀變化大,雙腎格柵縮小,預計明年發(fā)布

      全新一代寶馬5系曝光,外觀變化大,雙腎格柵縮小,預計明年發(fā)布

      紅濤說車
      2025-12-22 15:43:21
      央行終于出手,2026年2月1日起正式執(zhí)行!拒收現金正式納入嚴管!

      央行終于出手,2026年2月1日起正式執(zhí)行!拒收現金正式納入嚴管!

      今朝牛馬
      2025-12-24 22:30:26
      全球最精確預言家:2026,歷史性的一年, 能說的都在這里

      全球最精確預言家:2026,歷史性的一年, 能說的都在這里

      神奇故事
      2025-12-24 23:34:15
      張?zhí)m直播被調侃!有新孫子了,玥玥霖霖還疼嗎?張?zhí)m的回答很溫暖

      張?zhí)m直播被調侃!有新孫子了,玥玥霖霖還疼嗎?張?zhí)m的回答很溫暖

      小徐講八卦
      2025-12-24 08:48:59
      評論炸了,網友卻不敢看她坐下來

      評論炸了,網友卻不敢看她坐下來

      章眽八卦
      2025-12-07 11:32:57
      炒股成功的人是如何一步步走出來的?此文熬夜也要細細閱讀幾遍!

      炒股成功的人是如何一步步走出來的?此文熬夜也要細細閱讀幾遍!

      一方聊市
      2025-10-17 09:54:32
      何穗曬2個月寶寶會抬頭,頭發(fā)稀耳朵大有福,對陳偉霆稱呼顯親昵

      何穗曬2個月寶寶會抬頭,頭發(fā)稀耳朵大有福,對陳偉霆稱呼顯親昵

      心靜物娛
      2025-12-24 10:39:01
      38歲已婚女與37歲情人,在石凳子上發(fā)生關系,溫存后被殘忍殺害

      38歲已婚女與37歲情人,在石凳子上發(fā)生關系,溫存后被殘忍殺害

      胖胖侃咖
      2024-06-08 08:00:08
      女人真正的“性”高潮,原來是如此獨特而深邃的體驗

      女人真正的“性”高潮,原來是如此獨特而深邃的體驗

      精彩分享快樂
      2025-12-25 00:05:10
      今天,A股觸及3947,做好準備,明天,12月25號,大概率會這樣走

      今天,A股觸及3947,做好準備,明天,12月25號,大概率會這樣走

      有范又有料
      2025-12-24 14:57:09
      這身打扮就是一種特別舒服又特別抓人的好看

      這身打扮就是一種特別舒服又特別抓人的好看

      美女穿搭分享
      2025-12-22 20:56:32
      直面輿論話語權轉移現實,重建中國社會信任感

      直面輿論話語權轉移現實,重建中國社會信任感

      明叔雜談
      2025-12-24 15:05:20
      何晴去世第九天,許亞軍怒了,發(fā)文要追究網暴他現任妻兒者的責任

      何晴去世第九天,許亞軍怒了,發(fā)文要追究網暴他現任妻兒者的責任

      法老不說教
      2025-12-23 20:19:40
      2005年必將載入人類史冊的7大事件

      2005年必將載入人類史冊的7大事件

      史政先鋒
      2025-12-24 15:13:06
      2025-12-25 02:16:49
      侑虎科技UWA incentive-icons
      侑虎科技UWA
      游戲/VR性能優(yōu)化平臺
      1532文章數 986關注度
      往期回顧 全部

      科技要聞

      智譜和MiniMax拿出了“血淋淋”的賬本

      頭條要聞

      幼兒園8人遇難學生家屬:女兒4歲 今年9月才入讀

      頭條要聞

      幼兒園8人遇難學生家屬:女兒4歲 今年9月才入讀

      體育要聞

      26歲廣西球王,在質疑聲中成為本土得分王

      娛樂要聞

      懷孕增重30斤!闞清子驚傳誕一女夭折?

      財經要聞

      北京進一步放松限購 滬深是否會跟進?

      汽車要聞

      “運動版庫里南”一月份亮相???或命名極氪9S

      態(tài)度原創(chuàng)

      藝術
      游戲
      房產
      家居
      公開課

      藝術要聞

      巨星劉德華1000萬善款建村,為何如今竟成山間“空心村”?

      前《DOTA2》選手起訴LGD 稱拖欠近14萬賽事獎金

      房產要聞

      硬核!央企海口一線江景頂流紅盤,上演超預期交付!

      家居要聞

      法式大平層 智能家居添彩

      公開課

      李玫瑾:為什么性格比能力更重要?

      無障礙瀏覽 進入關懷版 主站蜘蛛池模板: 日本人妻中文| 中文字幕乱码亚洲中文在线| 18禁黄无遮挡网站免费| 亚洲人午夜精品射精日韩| 2021亚洲爆乳无码专区| 国产精品99久久久久久董美香 | 亚洲国产成人久久综合电影| 男人下部进女人下部视频| 国产a网站| 国产成人a∨激情视频厨房| 狠狠色丁香婷婷综合尤物 | 国产精品一亚洲AV日韩AV欧| 男人天堂一区| 把插八插露脸对白内射| 97精品国产一区二区三区| 国产成人精选视频在线观看| 天天摸日日摸狠狠添| 欧美成人精品手机在线| 成人网站免费看黄a站视频| 日韩无码AV电影网| 久久黄色片| 人妻少妇精品视频| 中文字幕人妻中出制服诱惑| 国内精品久久久久久无码不卡 | 91孕妇精品一区二区三区| 色欲色香天天天综合网站免费| 无码精品久久久久久人妻中字| 日日噜噜夜夜狠狠久久蜜桃| 国产精品无码dvd在线观看| 久久发布国产伦子伦精品| 诸城市| 日韩色区| 久久人人妻人人爽人人爽| 中文字幕亚洲综合久久菠萝蜜| 亚洲精品国产精品国自产| 欧美牲交a欧美在线| 中文字幕亚洲综合久久| 国产午夜福利短视频| 国产人妖乱国产精品人妖| 丁香婷婷社区| 国产传媒淫语对白AV|