Behavior Designer插件(上)


## 1、行為樹與狀態(tài)機(jī)對(duì)比

舉個(gè)簡(jiǎn)單的例子。設(shè)計(jì)一個(gè)簡(jiǎn)單的AI士兵,他具有以下特性:

1.他具有真實(shí)視野,發(fā)現(xiàn)敵人則進(jìn)攻。

2.進(jìn)攻時(shí)會(huì)瞄準(zhǔn)敵人并射擊,同時(shí)跑向敵人。

3.離初始位置過(guò)遠(yuǎn),有可能是被調(diào)虎離山了,這時(shí)要回到初始位置。

4.進(jìn)攻時(shí)離敵人過(guò)遠(yuǎn),代表敵人逃跑成功,也要回到初始位置。

畫一個(gè)簡(jiǎn)單的狀態(tài)機(jī):

在Update函數(shù)中用一些 if else 條件判斷,就實(shí)現(xiàn)了這個(gè)狀態(tài)機(jī),比較簡(jiǎn)單。改

進(jìn)這個(gè)狀態(tài)機(jī),但是改進(jìn)的AI士兵過(guò)于復(fù)雜了,到底能復(fù)雜到什么程度:


對(duì)于游戲設(shè)計(jì)師來(lái)說(shuō),我們只增加了一個(gè)簡(jiǎn)單的功能:當(dāng)AI士兵被遠(yuǎn)距離狙擊時(shí),就跑到建筑內(nèi)部的指定位置,以躲避狙擊手的攻擊。這個(gè)功能會(huì)增加兩個(gè)狀態(tài),就是上圖左邊的:跑向掩體和掩體待機(jī)。問(wèn)題是——數(shù)一數(shù)連線,最早我們有4條有效線段(去掉2個(gè)“無(wú)”的連線就是4條),代表4種狀態(tài)轉(zhuǎn)移的條件。而改進(jìn)后的AI士兵,多出了5條線段,比原來(lái)的復(fù)雜度擴(kuò)大了一倍還多。9種狀態(tài)轉(zhuǎn)移并不多,問(wèn)題是:這樣子的AI士兵還遠(yuǎn)遠(yuǎn)達(dá)不到設(shè)計(jì)要求,為了讓AI能應(yīng)付各種情況,我們最終可能需要12種狀態(tài)。那么這12種狀態(tài)如果用狀態(tài)機(jī)管理,線段會(huì)有多少條呢?去掉某些不可能的狀態(tài)轉(zhuǎn)移,少說(shuō)需要幾十條線段吧……這可不是什么好消息。下面我們直觀的看一下用行為樹解決同樣的問(wèn)題,是什么樣子的:


是不是有一種耳目一新的感覺(jué)?從直觀感受上來(lái)說(shuō),**狀態(tài)機(jī)是以多個(gè)狀態(tài)為核心,以狀態(tài)轉(zhuǎn)移為線索的一種圖表。而行為樹是以行為邏輯為框架,以具體行動(dòng)作為節(jié)點(diǎn)的一種樹狀圖。**上圖我們簡(jiǎn)單改變一下寫法,就變成了更容易理解的形式:


黃色節(jié)點(diǎn)翻譯為大家容易理解的 and &&、or || 和while循環(huán),暫且可以這么理解。而紅色節(jié)點(diǎn),是一種判斷行為(相當(dāng)于if語(yǔ)句),綠色節(jié)點(diǎn),是真正的行動(dòng)Action節(jié)點(diǎn)。這個(gè)圖是從示例工程中實(shí)際用到的行為樹簡(jiǎn)化而來(lái)的,非常具有說(shuō)服力。而實(shí)際使用中樹的結(jié)構(gòu)遠(yuǎn)遠(yuǎn)沒(méi)有這么簡(jiǎn)單,行為樹會(huì)引出很多新的概念與使用要點(diǎn),而且我們會(huì)用Behavior Designer行為樹插件來(lái)作為示范。

## 2、Behavior Designer 簡(jiǎn)單介紹

借用Behavior Designer官方文檔的介紹:Behavior Designer 是一個(gè)行為樹插件!是為了讓設(shè)計(jì)師,程序員,美術(shù)人員方便使用的可視化編輯器!Behavior Designer 提供了強(qiáng)大的 API 可以讓你輕松的創(chuàng)建 tasks(任務(wù)),配合 uScript 和 PlayMaker 這樣的插件,可以不費(fèi)吹灰之力就能夠創(chuàng)建出強(qiáng)大的 AI 系統(tǒng),而無(wú)需寫一行代碼!

其實(shí),按照我的理解,Behavior Designer的主要作用并非可以不寫代碼(還是要寫不少代碼的),而是能讓游戲中邏輯最混亂的模塊——AI模塊能更有序的組織,方便查看、調(diào)試和修改。

1、打開(kāi)Behavior Designer窗口的方法如圖:


2、為任意對(duì)象添加Behavior組件:


如圖,在上面的菜單里選擇“Add Behavior Tree”即可。觀察該GameObject的屬性,可以在下圖中可以看到這個(gè)組件實(shí)際上是一個(gè)腳本,默認(rèn)參數(shù)目前不需要任何修改。


下面開(kāi)始按步驟實(shí)現(xiàn)并講解一個(gè)基本的行為添加過(guò)程,如有懵圈的情況,及時(shí)詢問(wèn)或查找網(wǎng)上詳細(xì)的Behavior Designer資料。3、現(xiàn)在可以編輯這個(gè)對(duì)象的Behavior Tree了:要點(diǎn)1、按照步驟1可以打開(kāi)Behavior Designer編輯窗口,在窗口打開(kāi)的情況下點(diǎn)擊包含了BTree組件的對(duì)象,就可以對(duì)它進(jìn)行編輯:


這里添加一個(gè)Task -> Decorators -> UntilSuccess節(jié)點(diǎn),它是一種Decorator即修飾器,修飾器在行為樹中起到骨架的作用,就像是程序里的循環(huán)和判斷一樣不可或缺。我現(xiàn)在要實(shí)現(xiàn)視野范圍的功能,在發(fā)現(xiàn)敵人以后,把敵人信息記下來(lái)。這就需要一個(gè)定制化的動(dòng)作——判斷敵人是否在視野中,這個(gè)動(dòng)作起名為WithInSight。先添加一個(gè)WithInSight.cs腳本才能加入到Behavior Designer窗口里,代碼如下,已經(jīng)加上了詳細(xì)注釋:

```

public class WithinSight : Conditional{

? ? // 視野角度

? ? public float fieldOfViewAngle;

? ? // 目標(biāo)物體的Tag

? ? public string targetTag;

? ? // 發(fā)現(xiàn)目標(biāo)時(shí),將目標(biāo)對(duì)象設(shè)置到BahaviorTree共享變量里面去

? ? public SharedTransform target;

? ? public SharedVector3 targetPos;

? ? // 所有指定Tag的物體的數(shù)組

? ? private Transform[] possibleTargets;

? ? // 重載函數(shù),Behavior Designer專用的Awake

? ? public override void OnAwake()

? ? {

? ? ? ? // 根據(jù)Tag查找到所有物體,全部加入數(shù)組

? ? ? ? var targets = GameObject.FindGameObjectsWithTag(targetTag);

? ? ? ? possibleTargets = new Transform[targets.Length];

? ? ? ? for (int i = 0; i < targets.Length; ++i)

? ? ? ? {

? ? ? ? ? ? possibleTargets[i] = targets[i].transform;

? ? ? ? }

? ? }

? ? // 重載函數(shù),Behavior Designer專用的Update

? ? public override TaskStatus OnUpdate()

? ? {

? ? ? ? // 判斷目標(biāo)是否在視野內(nèi),這個(gè)返回值TaskStatus很關(guān)鍵,會(huì)影響樹的執(zhí)行流程

? ? ? ? for (int i = 0; i < possibleTargets.Length; ++i)

? ? ? ? {

? ? ? ? ? ? if (withinSight(possibleTargets[i], fieldOfViewAngle, 10))

? ? ? ? ? ? {

? ? ? ? ? ? ? ? // 將目標(biāo)信息填寫到共享變量里面,這樣其它Action就可以訪問(wèn)它們了

? ? ? ? ? ? ? ? target.Value = possibleTargets[i];

? ? ? ? ? ? ? ? targetPos.Value = target.Value.position;

? ? ? ? ? ? ? ? Debug.Log("Find Target" + targetPos.Value);

? ? ? ? ? ? ? ? // 成功則返回 TaskStatus.Success

? ? ? ? ? ? ? ? return TaskStatus.Success;

? ? ? ? ? ? }

? ? ? ? }

? ? ? ? // 沒(méi)找到目標(biāo)就在下一幀繼續(xù)執(zhí)行此任務(wù)

? ? ? ? return TaskStatus.Running;

? ? }

? ? // 判斷物體是否在視野范圍內(nèi)的方法

? ? public bool withinSight(Transform targetTransform, float fieldOfViewAngle, float distance)

? ? {

? ? ? ? Vector3 direction = targetTransform.position - transform.position;

? ? ? ? if (direction.magnitude > distance)

? ? ? ? {

? ? ? ? ? ? return false;

? ? ? ? }

? ? ? ? return Vector3.Angle(direction, transform.forward) < fieldOfViewAngle;

? ? }}

```

WithInSight是一種判斷條件,而不是實(shí)際的行為動(dòng)作,所以繼承了Conditional,這種繼承表示了對(duì)Behavior的擴(kuò)展。重點(diǎn)函數(shù)是OnAwake和OnUpdate,這個(gè)有別于MonoBehavior,是Behavior Designer插件專用的。寫好了這段代碼之后,再到行為樹窗口里去,就多了一個(gè)選項(xiàng):

順理成章,把該連的線連起來(lái)。


簡(jiǎn)單來(lái)說(shuō)就是用Decorator(修飾器)和Composites(組合器)搭邏輯框架,然后自定義Conditional(條件)和Action(動(dòng)作)來(lái)實(shí)際判斷和實(shí)際做出行為,僅此而已?,F(xiàn)在進(jìn)行測(cè)試,在玩家靠近AI時(shí),應(yīng)該能觸發(fā)Debug.Log,如果OK的話,說(shuō)明你邁出了第一步。這還沒(méi)完,咱們處理一下代碼中的2個(gè)shared變量。

4、關(guān)于Shared變量的介紹

注意代碼中的這兩個(gè)變量:

```

// 發(fā)現(xiàn)目標(biāo)時(shí),將目標(biāo)對(duì)象設(shè)置到BahaviorTree共享變量里面去

? ? public SharedTransform target;

? ? public SharedVector3 targetPos;

```

SharedXXXX類型代表這個(gè)變量雖然是在這個(gè)類中定義的,但是在正確綁定以后,其他Action或者Conditional也可以訪問(wèn)到。簡(jiǎn)單來(lái)說(shuō),它們就是專門用在Behavior Designer內(nèi)部的變量。對(duì)這種變量不僅要在代碼里聲明,還要在Behavior Designer窗口里進(jìn)行正確設(shè)置。


之前咱們直接畫圖了,沒(méi)有用到這里的四個(gè)窗口。介紹一下

(1) Behavior 狀態(tài)樹整體的名稱和屬性,對(duì)咱們的小項(xiàng)目來(lái)說(shuō)默認(rèn)就行,不用管。

(2) Tasks所有Conditional(條件)和Action(動(dòng)作)的列表,按照我的開(kāi)發(fā)習(xí)慣,較少用到內(nèi)置的條件和動(dòng)作。理由是內(nèi)置動(dòng)作功能太單一,組合起來(lái)樹狀圖會(huì)變得極其復(fù)雜,還不如用代碼清晰。這個(gè)問(wèn)題見(jiàn)仁見(jiàn)智了。

(3) Variables變量窗口,接下來(lái)咱們主要介紹這個(gè)。

(4) 行為樹節(jié)點(diǎn)的Inspector,是針對(duì)某個(gè)節(jié)點(diǎn)的詳細(xì)屬性。在這里面不僅可以設(shè)置參數(shù),還能綁定變量,接下來(lái)也要用到。

5、添加Shared變量

只要切換到Variables變量窗口,輸入變量名稱,選擇咱們腳本里定好的類型,然后Add即可。Add之后如下圖:


我們要讓AI角色發(fā)現(xiàn)敵人時(shí),記住他的transform和位置,以便后續(xù)處理,這些變量就和他的大腦記憶一樣。所以,要把WithInSight節(jié)點(diǎn)和這些變量關(guān)聯(lián)起來(lái)。

6、關(guān)聯(lián)節(jié)點(diǎn)和變量


點(diǎn)擊WithInSight節(jié)點(diǎn),點(diǎn)擊Inspector,可以看到這個(gè)節(jié)點(diǎn)的所有屬性。普通的屬性就直接設(shè)定初始值就ok了,比如第一個(gè)屬性可視范圍是45。重點(diǎn)是對(duì)Shared變量進(jìn)行綁定操作,現(xiàn)在是紅色的None。如果這里和我的截圖不一致,就點(diǎn)擊黑色圓點(diǎn),切換一下綁定方式。如下圖操作:


這樣就能把Variables窗口里剛才新建的Target變量,和WithinSight判斷中的Target變量徹底聯(lián)系起來(lái)。同理對(duì)TargetPos也要做同樣操作。

7、試著加一個(gè)動(dòng)作,進(jìn)行試驗(yàn)增加一個(gè)動(dòng)作AimAction瞄準(zhǔn)動(dòng)作,實(shí)際上就是轉(zhuǎn)向Target的方向即可,如果AI能轉(zhuǎn)向敵人方向,就代表我們的綁定成功了。先按咱們第3步也就是創(chuàng)建WithinSight的方法,創(chuàng)建一個(gè)腳本叫AimAction.cs,內(nèi)容如下:

```

public class AimAction : Action{

? ? public SharedTransform target;

? ? // 是否正在面對(duì)入侵者,即已經(jīng)正確瞄準(zhǔn)

? ? bool IsFacingTarget()

? ? {

? ? ? ? if (target.Value == null)

? ? ? ? {

? ? ? ? ? ? return false;

? ? ? ? }

? ? ? ? Vector3 v1 = target.Value.position - transform.position;

? ? ? ? v1.y = 0;

? ? ? ? if (Vector3.Angle(transform.forward, v1) < 1)

? ? ? ? {

? ? ? ? ? ? return true;

? ? ? ? }

? ? ? ? return false;

? ? }

? ? // 轉(zhuǎn)向入侵者方向,每次只轉(zhuǎn)一點(diǎn),速度受turnSpeed控制

? ? void RotateToTarget()

? ? {

? ? ? ? if (target.Value == null)

? ? ? ? {

? ? ? ? ? ? return;

? ? ? ? }

? ? ? ? Vector3 v1 = target.Value.position - transform.position;

? ? ? ? v1.y = 0;

? ? ? ? Vector3 cross = Vector3.Cross(transform.forward, v1);

? ? ? ? float angle = Vector3.Angle(transform.forward, v1);

? ? ? ? transform.Rotate(cross, Mathf.Min(2, Mathf.Abs(angle)));

? ? }

? ? public override void OnAwake()

? ? {

? ? }

? ? public override TaskStatus OnUpdate()

? ? {

? ? ? ? if (IsFacingTarget())

? ? ? ? {

? ? ? ? ? ? // 返回值不同對(duì)狀態(tài)樹會(huì)產(chǎn)生巨大影響,可以對(duì)比測(cè)試

? ? ? ? ? ? //return TaskStatus.Success;

? ? ? ? ? ? return TaskStatus.Running;

? ? ? ? }

? ? ? ? RotateToTarget();

? ? ? ? return TaskStatus.Running;

? ? }}

```

然后修改行為樹的圖,添加AimAction和一些聯(lián)系用的Composite節(jié)點(diǎn),改為下面的形式:


中間的Sequence代表下面的兩個(gè)子節(jié)點(diǎn)依次執(zhí)行,UntilSuccess構(gòu)成一個(gè)局部的反復(fù)執(zhí)行邏輯,AI會(huì)在左邊的子節(jié)點(diǎn)重復(fù),直到發(fā)現(xiàn)敵人,Until節(jié)點(diǎn)中斷,執(zhí)行Aim瞄準(zhǔn)動(dòng)作。別忘了給AimAction節(jié)點(diǎn)也綁定兩個(gè)Shared變量。如果發(fā)現(xiàn)像下圖這樣,和之前說(shuō)好的不一樣,就點(diǎn)擊黑色圓點(diǎn),切換一下綁定方式。點(diǎn)擊黑點(diǎn)實(shí)際上是切換兩種不同的變量使用方式。


現(xiàn)在,如果你操作沒(méi)錯(cuò),那么播放游戲,看看敵人是否在發(fā)現(xiàn)你之后,就瞄準(zhǔn)你:


如上圖,不僅AI角色能正確瞄準(zhǔn)主角,而且在Behavior Designer窗口中,還能實(shí)時(shí)看到目前邏輯進(jìn)行的狀態(tài),這個(gè)是Behavior Designer插件威力最大的功能之一——查看邏輯進(jìn)展?fàn)顟B(tài),將AI思考過(guò)程可視化。

8、擴(kuò)展實(shí)現(xiàn)所有功能

老師領(lǐng)進(jìn)門,修行在個(gè)人。所有基本功能都介紹完畢,至于實(shí)際的使用方法需要大家自己分析一下了。下面是實(shí)現(xiàn)開(kāi)頭狀態(tài)機(jī)的功能,得到了很好的效果,而且很好修改。


而且雖然節(jié)點(diǎn)很多,加上注釋之后,其實(shí)也就分三大塊而已,看起來(lái)還是有狀態(tài)機(jī)的影子,不難理解。


如上圖,和咱們講行為樹原理時(shí)用的概念圖基本是一致的。某些Composite前面沒(méi)講,下一篇文章會(huì)繼續(xù)深入,當(dāng)然自己查資料試驗(yàn)才是最好的學(xué)習(xí)方法。

## 3、總結(jié)

狀態(tài)機(jī)和行為樹的問(wèn)題,屬于工程問(wèn)題,不是科學(xué)問(wèn)題,每個(gè)人會(huì)找到自己的理解,而且還有可能發(fā)現(xiàn)更厲害的抽象方法。對(duì)于工程問(wèn)題來(lái)說(shuō),就和寫代碼一樣,有很多要點(diǎn):

1、細(xì)節(jié)多且雜,函數(shù)返回值、Composite的使用這些細(xì)節(jié)的設(shè)計(jì)決定了成敗。

2、除非自己動(dòng)手試驗(yàn),發(fā)現(xiàn)問(wèn)題、解決問(wèn)題,否則不可能掌握。所以,我會(huì)在之后再次深入討論Behavior Designer。

行為樹的實(shí)際使用博大精深,遠(yuǎn)遠(yuǎn)不是幾篇文章能覆蓋到的。畢竟AI是游戲開(kāi)發(fā)中最龐大的系統(tǒng)之一,而行為樹又是AI的核心。希望讀者們能理解方法,體會(huì)樂(lè)趣,不要過(guò)早陷入技術(shù)細(xì)節(jié)之中。咱們下一節(jié)還是繼續(xù)行為樹,下期再見(jiàn)。

感謝皮皮關(guān)的分享,轉(zhuǎn)載于皮皮關(guān)的給貓看的游戲AI實(shí)戰(zhàn)(六)行為樹和Behavior Designer插件(上篇) - 知乎

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容