国产av一二三区|日本不卡动作网站|黄色天天久久影片|99草成人免费在线视频|AV三级片成人电影在线|成年人aV不卡免费播放|日韩无码成人一级片视频|人人看人人玩开心色AV|人妻系列在线观看|亚洲av无码一区二区三区在线播放

網(wǎng)易首頁 > 網(wǎng)易號 > 正文 申請入駐

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

0
分享至

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

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

作者主頁:

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

一、前言

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

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

二、運(yùn)動(dòng)邏輯

1. 根節(jié)點(diǎn)驅(qū)動(dòng)

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

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

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

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

2. 特殊的節(jié)點(diǎn)

行為樹的一大特點(diǎn),就是將條件與行為本身進(jìn)行了分離。

什么意思呢?我們?nèi)砸陨厦婺菑垐D為例,只是稍稍修改下表現(xiàn)方式(也更接近行為樹真正的樣子):

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

總之,正是因?yàn)樾袨闃溆幸幌盗刑厥獾墓?jié)點(diǎn),使得開發(fā)者可以降低各個(gè)行為之間的關(guān)聯(lián)(也就是解耦),再配合上樹狀結(jié)構(gòu)的特點(diǎn),開發(fā)者可以靈活地進(jìn)行組裝,實(shí)現(xiàn)節(jié)點(diǎn)的重復(fù)利用,避免寫重復(fù)的代碼,提高了開發(fā)效率。(用過有限狀態(tài)機(jī)寫游戲AI的同學(xué)一定能體會到這點(diǎn)的好處。)

三、代碼實(shí)現(xiàn)

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

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

a. 順序器(Sequence)

b. 選擇器(Selector)

c. 并行器(Parallel)

d. 過濾器(Filter)

e. 主動(dòng)選擇器(ActiveSelector)

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

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

a. 取反器(Inverter)

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

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

1. 基礎(chǔ)準(zhǔn)備

正式實(shí)現(xiàn)它們之前,我們應(yīng)當(dāng)準(zhǔn)備它們的基類,畢竟它們都是節(jié)點(diǎn),有一些共性的東西可以提取出來,這樣可以減少一些重復(fù)的代碼。

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

2. 組合節(jié)點(diǎn)

首先實(shí)現(xiàn)「組合節(jié)點(diǎn)」這個(gè)基類。

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

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

a. 順序器

邏輯上來講(非代碼結(jié)構(gòu)),它長這樣:

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

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

b. 選擇器

從邏輯上講,它的結(jié)構(gòu)應(yīng)該長這樣:

每次只會選擇一個(gè)可以運(yùn)行的子節(jié)點(diǎn)來運(yùn)行。

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

所以,只要簡單地繼承「順序器」并修改它的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. 并行器

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

可這樣就有問題了:

1. 怎么「同時(shí)」運(yùn)行,要用多線程嗎?

2. 同時(shí)執(zhí)行必然會返回多個(gè)結(jié)果,該如何確定最終返回運(yùn)行結(jié)果呢?

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

對于問題2,我們可以根據(jù)需求自行設(shè)置并行器成功或失敗的標(biāo)準(zhǔn),一般可分為「全都」和「只要有一個(gè)」。

看看代碼就知道了:

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

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

d. 過濾器

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

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

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

e. 主動(dòng)選擇器

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

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

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

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

f. 監(jiān)視器

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

從邏輯上看,它有兩個(gè)子樹,一邊負(fù)責(zé)條件,一邊負(fù)責(zé)具體行為。這種分離方式是合理的,防止了同步和爭用問題,因?yàn)橹挥幸粋€(gè)子樹將運(yùn)行對世界進(jìn)行更改的操作。

從代碼上來說,其實(shí)它的改造方法和「過濾器」完全一致,因?yàn)槲覀兺耆梢园堰@兩個(gè)子樹看作一個(gè),只是前半部分全是條件,后半部分全是具體動(dòng)作而已:

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

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

3. 修飾節(jié)點(diǎn)

修飾節(jié)點(diǎn)只有一個(gè)子節(jié)點(diǎn),因?yàn)檫@樣就足夠了,想要多個(gè)條件只需要配合組合節(jié)點(diǎn)就可以實(shí)現(xiàn)。所以它的基類也十分簡單:

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

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

a. 取反器

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

它的實(shí)現(xiàn)也很簡單:

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. 重復(fù)執(zhí)行器

重復(fù)執(zhí)行某(些)行為也是常見的動(dòng)作需求,這些動(dòng)作往往都是已實(shí)現(xiàn)的單一動(dòng)作,例如,有了「點(diǎn)射」動(dòng)作,我們就可以僅給它加上一個(gè)重復(fù)執(zhí)行器,就可以實(shí)現(xiàn)「掃射」了。

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

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

4. 動(dòng)作節(jié)點(diǎn)

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

要實(shí)現(xiàn)動(dòng)作節(jié)點(diǎn),只要繼承并重寫節(jié)點(diǎn)基類就可以了,例如一個(gè)打印一些字的節(jié)點(diǎn):

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. 構(gòu)建與運(yùn)行

節(jié)點(diǎn)部分我們都講完了,現(xiàn)在就開始實(shí)現(xiàn)樹的構(gòu)建與運(yùn)行了。

我們先實(shí)現(xiàn)樹:

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

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

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

public partial classBehaviorTreeBuilder {     private readonly Stack nodeStack; //構(gòu)建樹結(jié)構(gòu)用的棧     private readonly BehaviorTree bhTree;//構(gòu)建的樹     publicBehaviorTreeBuilder()     {         bhTree = newBehaviorTree(null);//構(gòu)造一個(gè)沒有根的樹         nodeStack = newStack (); //初始化構(gòu)建棧     }     privatevoidAddBehavior(Behavior behavior)     {         if (bhTree.HaveRoot)//有根節(jié)點(diǎn)時(shí),加入構(gòu)建棧         {             nodeStack.Peek().AddChild(behavior);         }         else//當(dāng)樹沒根時(shí),新增得節(jié)點(diǎn)視為根節(jié)點(diǎn)         {             bhTree.SetRoot(behavior);         }         //只有組合節(jié)點(diǎn)和修飾節(jié)點(diǎn)需要進(jìn)構(gòu)建堆         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;     } }

這樣就實(shí)現(xiàn)了樹構(gòu)建,還把調(diào)用也再包裝了一層。用BehaviorTreeBuilder,就可以既構(gòu)建又運(yùn)行了。接下來,我們來詳細(xì)說說里面的邏輯:

最開始用AddBehavior函數(shù)添加一個(gè)節(jié)點(diǎn),它無疑會成為根:

再添加一個(gè)0,它會變成這樣:

再添加同理:

而當(dāng)我們想要為0添加第二個(gè)子節(jié)點(diǎn)時(shí),只需要先調(diào)用Back,Back會使棧頂元素彈出:

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

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

配合縮進(jìn),我們可以勉強(qiáng)實(shí)現(xiàn)可視化,至少有層次感:

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

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

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é)點(diǎn)---------     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;     } }

你可能也注意到了,這個(gè)類是partial類,它還有另一部分內(nèi)容,我將其與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;     } }

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

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

參考:

[1]《Game AI Pro》


https://www.gameaipro.com/

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

近期精彩回顧

【學(xué)堂上新】

【學(xué)堂上新】

【萬象更新】

【萬象更新】

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

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.

相關(guān)推薦
熱點(diǎn)推薦
襲擊我國人員后,該武裝組織遭我軍合成營"教科書式"圍殲

襲擊我國人員后,該武裝組織遭我軍合成營"教科書式"圍殲

小哥很OK
2026-01-05 11:07:02
突傳消息!毛戈平和妻子、姐姐等擬套現(xiàn)14億港元!“用于投資,改善個(gè)人生活等”

突傳消息!毛戈平和妻子、姐姐等擬套現(xiàn)14億港元!“用于投資,改善個(gè)人生活等”

海峽網(wǎng)
2026-01-08 09:09:01
“哥都禮共和國”宣布成立,并宣布脫離緬甸獨(dú)立

“哥都禮共和國”宣布成立,并宣布脫離緬甸獨(dú)立

曼谷陳大叔
2026-01-07 15:57:35
女子開車碾壓草場后續(xù):揚(yáng)言撞死牧民,真實(shí)身份被扒,公司被牽連

女子開車碾壓草場后續(xù):揚(yáng)言撞死牧民,真實(shí)身份被扒,公司被牽連

奇思妙想草葉君
2026-01-07 23:56:24
美國扣押一艘與委內(nèi)瑞拉有關(guān)、懸掛俄羅斯旗幟的石油運(yùn)輸船,外交部回應(yīng)

美國扣押一艘與委內(nèi)瑞拉有關(guān)、懸掛俄羅斯旗幟的石油運(yùn)輸船,外交部回應(yīng)

環(huán)球網(wǎng)資訊
2026-01-08 15:38:17
油輪被扣押,俄議員呼喊“對美開戰(zhàn)”

油輪被扣押,俄議員呼喊“對美開戰(zhàn)”

跟著老李看世界
2026-01-08 08:57:52
上海通報(bào):公職人員沈劍被查,涉嫌嚴(yán)重違紀(jì)違法

上海通報(bào):公職人員沈劍被查,涉嫌嚴(yán)重違紀(jì)違法

上觀新聞
2026-01-08 12:10:08
朝鮮不會成為第二個(gè)委內(nèi)瑞拉!因?yàn)槌r有兩個(gè)后盾

朝鮮不會成為第二個(gè)委內(nèi)瑞拉!因?yàn)槌r有兩個(gè)后盾

米君文史
2026-01-07 10:01:47
李慧瓊當(dāng)選香港特別行政區(qū)第八屆立法會主席

李慧瓊當(dāng)選香港特別行政區(qū)第八屆立法會主席

界面新聞
2026-01-08 12:17:34
收評:創(chuàng)業(yè)板指震蕩調(diào)整跌0.82% 全市場超百股漲停

收評:創(chuàng)業(yè)板指震蕩調(diào)整跌0.82% 全市場超百股漲停

財(cái)聯(lián)社
2026-01-08 15:02:07
外交部:美方在公海海域隨意扣押他國船只嚴(yán)重違反國際法

外交部:美方在公海海域隨意扣押他國船只嚴(yán)重違反國際法

澎湃新聞
2026-01-08 15:36:26
景德鎮(zhèn)一家三口被撞亡案將宣判,代理律師發(fā)聲!家屬要求嚴(yán)懲

景德鎮(zhèn)一家三口被撞亡案將宣判,代理律師發(fā)聲!家屬要求嚴(yán)懲

南方都市報(bào)
2026-01-08 15:11:05
瓦良格號送到中國后有多震撼?專家刮掉表面的銹跡:鋼材品質(zhì)極佳

瓦良格號送到中國后有多震撼?專家刮掉表面的銹跡:鋼材品質(zhì)極佳

古書記史
2026-01-06 16:31:56
新華社官宣:轟-20和殲-36的正式亮相非常值得期待

新華社官宣:轟-20和殲-36的正式亮相非常值得期待

烽火觀天下
2026-01-08 11:52:17
鄭州9歲女孩課堂上寫試卷時(shí)昏倒去世,家屬不忍尸檢“她怕疼”,當(dāng)?shù)爻闪0嗾{(diào)查

鄭州9歲女孩課堂上寫試卷時(shí)昏倒去世,家屬不忍尸檢“她怕疼”,當(dāng)?shù)爻闪0嗾{(diào)查

大風(fēng)新聞
2026-01-08 14:41:04
72%煙草倒掛逼哭零售戶!寧可不訂也不賠錢,市場根基正在爛根

72%煙草倒掛逼哭零售戶!寧可不訂也不賠錢,市場根基正在爛根

老特有話說
2026-01-07 00:40:03
女大學(xué)生餐館訛錢后續(xù):正臉曝光很漂亮 家人輪番找店主 目的曝光

女大學(xué)生餐館訛錢后續(xù):正臉曝光很漂亮 家人輪番找店主 目的曝光

鋭娛之樂
2026-01-08 08:34:40
微信辟謠網(wǎng)傳新規(guī)則

微信辟謠網(wǎng)傳新規(guī)則

界面新聞
2026-01-08 14:53:45
2025年的中國車市,教會了合資車企如何生存

2025年的中國車市,教會了合資車企如何生存

汽車公社
2026-01-08 08:33:54
雷軍全面回應(yīng)“營銷大師”標(biāo)簽:娛樂節(jié)目中劉強(qiáng)東團(tuán)隊(duì)開個(gè)玩笑,被人放大利用,現(xiàn)在聽到營銷兩個(gè)字都有點(diǎn)惡心

雷軍全面回應(yīng)“營銷大師”標(biāo)簽:娛樂節(jié)目中劉強(qiáng)東團(tuán)隊(duì)開個(gè)玩笑,被人放大利用,現(xiàn)在聽到營銷兩個(gè)字都有點(diǎn)惡心

每日經(jīng)濟(jì)新聞
2026-01-08 00:48:20
2026-01-08 16:28:49
侑虎科技UWA incentive-icons
侑虎科技UWA
游戲/VR性能優(yōu)化平臺
1537文章數(shù) 986關(guān)注度
往期回顧 全部

科技要聞

智譜拿下“全球大模型第一股”,憑什么

頭條要聞

中方被指正考慮進(jìn)一步收緊中重稀土出口 日本業(yè)界慌了

頭條要聞

中方被指正考慮進(jìn)一步收緊中重稀土出口 日本業(yè)界慌了

體育要聞

約基奇倒下后,一位故人邪魅一笑

娛樂要聞

2026春節(jié)檔將有六部電影強(qiáng)勢上映

財(cái)經(jīng)要聞

微軟CTO韋青:未來人類會花錢"戒手機(jī)"

汽車要聞

從量變到"智"變 吉利在CES打出了五張牌

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

游戲
教育
時(shí)尚
本地
親子

歐洲評級泄露《奇異人生》新游 但是開發(fā)商沒公布

教育要聞

中考數(shù)學(xué),求陰影面積?

藍(lán)色+灰色、紅色+棕色,這4組配色怎么搭都好看!

本地新聞

1986-2026,一通電話的時(shí)空旅程

親子要聞

富豪階層,正在批量生產(chǎn)“超級嬰兒”

無障礙瀏覽 進(jìn)入關(guān)懷版