BehaviorDesigner——行為樹,用於控制和實現AI邏輯,類似於這樣:
上面這個行為樹實現了這樣的邏輯:
當Player有Input時按照Input值來移動,無Input時查找最近的可攻擊目標,如果能找到就執行攻擊;當既沒有Input也沒有找到攻擊目標時,那就一直處於Idle狀態。
下面總結BehaviorDesigner最常見的基礎知識:
首先要明確一個行為樹必須有一個依賦對象,它詮釋的是該對象的一系列行為模式。
這些行為模式由Task節點構成,圖中的每一個可執行的方框就是一個Task節點,將這些節點按照設計的邏輯進行連接,就組成了該對象的行為樹。
行為樹從根節點開始,從上至下,從左至右依次執行其下每一Task節點,任何被執行的Task將返回一種狀態,當根節點Task返回成功(或失敗)狀態時,意味着該行為樹單次執行結束。
Task有以下狀態:
正在執行,執行成功,執行失敗,未激活。
任何Task都處於這幾種狀態之一。
Task分為不同類型,每種類型的Task作用各不相同,首先必須弄清楚每類Task的大致作用:
Composites(復合類):主要用於控制行為樹的走向,也是用的最多最重要的一類,任何一個相對復雜的行為樹都包含這類Task節點,但它本身不做任何具體行為,所以它們一般位於父節點或根節點。
Decorators(裝飾類):多用於對其下的子Task節點執行額外操作,例如反轉結果,重復執行等。
Actions(行為類):數量最多,為具體執行行為的Task,一般位於行為樹的葉子節點右側,該類Task可能並非單幀就能完成。沒必要每個Action都搞清楚,因為可以很容易的自己擴展Action。后面會具體介紹如何擴展。
Conditionals(條件類):一般放在Action節點左側進行約束,只有當條件滿足(或不滿足)時才繼續往下執行,單幀內完成一次判斷。更多時候配合復合節點進行打斷或任務跳轉,這一點后面會詳細說明。
最基礎最常用的復合類Task是下面兩個:
Selector(選擇):相當於Or操作,下面的子Task節點只要有一個返回成功了它就返回成功,只有當所有的都返回失敗了才返回失敗。
Sequence(序列):相當於And操作,下面的子Task節點只要有一個返回失敗了它就返回失敗,只有當所有的都返回成功了才返回成功。
其余的只是在這兩個的基礎上變形,例如子節點同時執行或隨機順序執行等,具體可看官方文檔說明。
復合類Task的優先級和打斷:
這一點非常重要,是復合類Task的唯一特殊屬性:分為四種——不打斷,可打斷自身,可打斷低於該Task優先級的其他Task,既可以打斷自身也可以打斷低於其優先級的。
需要注意的是,該復合節點的打斷條件是其下子節點必須有條件節點,此時該條件節點的判斷一直處於運行狀態,一旦該條件節點在某一刻發生改變,此時行為樹將重新跳轉到該復合節點位置繼續運行,從而打斷其他正在運行的低優先級節點。
例如最上面的行為樹中,Player通過判斷是否接入Input移動指令可以打斷比它優先級低的攻擊節點和Idle節點的運行,而攻擊節點可以打斷Idle節點。
這意味着,當Player處於Idle狀態時一旦成功找到敵人,那么就變為攻擊狀態,攻擊或Idle狀態時一旦接受到Input指令又跳轉到移動;只有當既沒有Input指令輸入又沒有查找到敵人時,才Idle。
一旦某一時刻比它優先級高的節點滿足了前置條件,就會跳轉執行優先級高的。
所以在設計行為樹時,一般會把優先級高的Task節點置於行為樹的左側,將優先級低的置於右側,因為復合節點並不能打斷比該它優先級高的Task節點。
開啟了Abort Type后Task方框的左上角會出現向右或向下的箭頭作為標志提示。
自定義Task任務:
一般復合類和裝飾類的Task是夠用的,甚至有些根本用不到,而具體的行為類Task和條件類Task從來都不能滿足我們的需求,而且自己寫這類Task可以很大程度的簡化整個行為樹結構。
自己寫Task的步驟如下:
1.引入命名空間:
using BehaviorDesigner.Runtime; using BehaviorDesigner.Runtime.Tasks;
2.明確繼承的Task類型:
public class MyInputMove : Action public class MyIsInput : Conditional
3.知曉Task內部函數的執行流程:(可以看官方文檔,寫的很清楚,這里把最重要的一張圖貼上來)
觀察上圖就會發現和Unity中編寫腳本大同小異,不一樣的地方就是這里的Update有返回值,要返回該任務的執行狀態,只有在Running狀態時才每幀調用。
using UnityEngine; using BehaviorDesigner.Runtime; using BehaviorDesigner.Runtime.Tasks; public class MyInputMove : Action { public SharedFloat speed = 5f; public override TaskStatus OnUpdate() { float inputX = Input.GetAxis("Horizontal"); float inputZ = Input.GetAxis("Vertical"); if (inputX != 0 || inputZ != 0) { Vector3 movement = new Vector3(inputX, 0, inputZ); transform.Translate(movement*Time.deltaTime*speed.Value); return TaskStatus.Running; } return TaskStatus.Success; } }
還有一點不同,就是Task中封裝了一層Share的類型的屬性,它和非Share類型有何區別呢?
下面是對比:
using UnityEngine; using BehaviorDesigner.Runtime.Tasks; using BehaviorDesigner.Runtime; public class MyLog : Action { public string staticLog; public SharedString shareLog; public override TaskStatus OnUpdate() { Debug.Log(staticLog + shareLog.Value); return TaskStatus.Success; } }
可以看到,這里的Share的類型就是一個方便在行為樹中傳遞和修改的變量,因為Task之間是不方便直接修改其他Task變量的,那怎么辦呢,於是就增加一種Share的類型變量在行為樹的各個Task之間進行交流。
比如這里,每次找到的最近的敵人是不一樣的,要根據上一個Task返回的值去執行下一個Task的攻擊或打印結果,這時固定的屬性就無法滿足要求,但直接調用別的Task又增加了耦合性,於是就單獨用Share變量來傳遞值。這樣也方便在面板中統一查看管理。
另外Share變量也可以增加自定義類型,全局的和本地變量的區別就是一個在所有的行為樹中有,一個只有這棵樹中有。
上面就是將查找到的最近的敵人和名字返回,其他Task例如攻擊和打印時直接就可以取到這里返回的值。在取Share變量值時需要.Value。
using UnityEngine; using BehaviorDesigner.Runtime; using BehaviorDesigner.Runtime.Tasks; public class MyCanFindLatestTarget : Conditional { public SharedGameObjectList origins; public bool bUseTag = false; public SharedString tag; public SharedGameObject result; public SharedString resultName; public override void OnStart() { if (bUseTag) { origins.Value.Clear(); origins.Value.AddRange(GameObject.FindGameObjectsWithTag(tag.Value)); } } public override TaskStatus OnUpdate() { if (origins.Value.Count > 0) { float minDistance=Mathf.Infinity; GameObject minDistanceObj=null; foreach(var item in origins.Value) { if (item == null) continue; float sqrDistance = (item.transform.position - transform.position).sqrMagnitude; if (sqrDistance < minDistance) { minDistance = sqrDistance; minDistanceObj = item; } } result.Value = minDistanceObj; resultName.Value = minDistanceObj.name; return TaskStatus.Success; } return TaskStatus.Failure; } }
using UnityEngine; using BehaviorDesigner.Runtime.Tasks; using BehaviorDesigner.Runtime; public class MyAttack : Action { public SharedGameObject target; public SharedFloat damage=10f; public SharedFloat attackSpeed = 10f; private Enemy enemy; private float timer; public override void OnStart() { if (target.Value != null) { enemy = target.Value.GetComponent<Enemy>(); } timer = 0f; } public override TaskStatus OnUpdate() { if (enemy == null) return TaskStatus.Failure; timer += Time.deltaTime; if (timer > (10f /attackSpeed.Value)) { timer = 0f; enemy.Hit(damage.Value); Debug.Log(target.Value.name + "剩余HP:" + enemy.hp); if (enemy.hp <= 0) { enemy.Dead(); return TaskStatus.Success; } } return TaskStatus.Running; } }