Unity-Behavior Designer详解
Unity-Behavior Designer詳解
理論
基本概念
行為樹是一個包含邏輯節(jié)點和行為節(jié)點的樹結(jié)構(gòu),每次需要找出一個行為的時候,會從樹的根節(jié)點出發(fā),遍歷各個節(jié)點,找出第一個和當前數(shù)據(jù)相符合的行為。
如下圖,就是一個簡單的行為樹
當我們要決策一個AI要做什么樣的行為的時候, 我們就會自頂向下的,通過一些條件來搜索這顆樹,最終確定需要做的行為(葉節(jié)點),并且執(zhí)行它,這就是行為樹的基本原理。
Task&Status
有四種不同類型的 task(任務(wù)): 包括 action(行為),composite(復(fù)合),
conditional(條件),decorator(修飾符)
復(fù)合(Composites)
主要有三種:
- Sequence
- 順序執(zhí)行,會按照從左向右的順序依次執(zhí)行子節(jié)點
- Selector
- 選擇執(zhí)行,根據(jù)條件判斷,選擇一個子節(jié)點執(zhí)行
- Parallel
- 平行執(zhí)行,同時執(zhí)行所有子節(jié)點
修飾(Decorator)
這個類型的節(jié)點只能有一個子節(jié)點。它的功能是修改子任務(wù)的行為。在上面的例子中,我們沒有使用 decorator(修飾符),如果你需要類似于打斷操作的話會用得到這個 decorator(修飾符)類型!舉個例子:一個收集資源的操作,它可能有一個中斷節(jié)點,這個節(jié)點判斷是否被攻擊,如果被攻擊則中斷收集資源操作!decorator(修飾符)的另一個應(yīng)用場合是重復(fù)執(zhí)行子任務(wù) X 次,或
者執(zhí)行子任務(wù)直到完成
條件(Conditinals)
用來判斷某些游戲?qū)傩允欠窈线m!
行為(Action)
行為很容易理解,即為具體的動作,他們在某種程度上改變游戲的狀態(tài)和結(jié)果
返回狀態(tài)(status)
有時候一個 task(任務(wù))需要多幀才能完成。例如,大多數(shù)動畫不會在一幀開始并結(jié)束。此外有 conditional(條件)的任務(wù)需要一種方法來告訴他們的父任務(wù)條件是否正確,以便讓父節(jié)點確定子節(jié)點的執(zhí)行順序。這兩個問題都可以使用 status(狀態(tài))來解決。一個任務(wù)有三種不同狀態(tài):運行,成功或者失敗
行為樹插件
插件下載鏈接:https://download.csdn.net/download/qq_52324195/85400525
基本介紹
行為樹整體的編輯界面
- Behavior Name,行為樹名稱
- Behavior Description,行為樹簡介
- Extenral Description,外部行為樹
- 關(guān)于這一屬性,可以通過右上角的Export將行為樹導(dǎo)出,創(chuàng)建一個行為樹“Prefab”,他就成為了外部行為樹,然后將這個外部行為樹拖入該屬性,就會將外部行為樹應(yīng)用到當前行為樹
- Group
- 行為樹的分組編號,用來將行為樹分組!可以用來方便的查找到特定的行為樹
- Start When Enabled
- 如果設(shè)置為 true,那么當這個行為樹組件 enabled 的時候,這個行為樹就會被執(zhí)行
- Asynchronous Load,異步加載
- Pause When Disabled
- 如果設(shè)置為 true,那么當這個行為樹組件 disabled 的時候,這個行為樹就會被暫停
- Restart When Complete
- 如果設(shè)置為 true,那么當這個行為樹組件執(zhí)行結(jié)束的時候,這個行為樹就會被重新執(zhí)行
- Reset Values On Restart
- 如果設(shè)置為 true,那么當這個行為樹組件 reset 的時候,這個行為樹就會被重新執(zhí)行
- Log Task Changes
- 當設(shè)置為 true 是,這個行為樹下只要 task 流程發(fā)生變化就會打印一條 log 日志到控制臺中
Variable
全局變量,如圖,你可以自己創(chuàng)建規(guī)定類型的變量
Inspector
點擊任意Task,可以在Inspector界面查看該節(jié)點的屬性
類似于Unity的編輯界面,出了前三個基本屬性外,其他的都是該Task的獨有屬性,我們可以通過每個屬性右邊的點按鈕,來使用我們的全局變量
Task
在整個任務(wù)樹的最高層的節(jié)點我們稱之為 Task(任務(wù))。這些 task 任務(wù)擁有類似于 MonoBehavior 那樣的接口用于實現(xiàn)和擴展。
基本屬性
Task 任務(wù)有三個基礎(chǔ)的公共屬性:name, comment, instant
如下圖,你會發(fā)現(xiàn)任意Task都有這三種屬性
前兩個很簡單,名稱與簡介。
不過instant可能并不好理解,解釋如下:
行為樹中,當一個 task 任務(wù)返回成功或者失敗后,行為樹會在同一幀中立刻移動到下一個 task 任務(wù)。如果,你沒有選擇 instant 選項,那么在當前 task 任務(wù)執(zhí)行完畢后,都會停留在當前節(jié)點中,直到收到了下一個 tick,才會移動到下一個 task 任務(wù)!
疑問:什么是tick?在解釋這個之前,我們來講解另外一個知識:BehaviorManager
其實,你會發(fā)現(xiàn),當運行一個行為樹的時候,會在場景中自動創(chuàng)建一個名稱為 BehaviorManager的 GameObject,并添 BehaviorManage.cs
最初你看到的是下圖這樣的:
第一行:Update Interval,這很好理解,行為樹的更新間隔,他有三種選擇
- Every Frame 每幀更新
- Specify Seconds 自定義時間更新
- Manual 手動更新
如果你選擇了,這個選項。你需要通過腳本來手動調(diào)用以下函數(shù)來執(zhí)行所有行為樹的更新
BehaviorManager.instance.Tick()
它還有另外一種重載:BehaviorManager.instance.Tick(BehaviorTree);,很顯然,它傳入了一個行為樹,那么僅僅就是只對該行為樹進行更新,而不是全部。
第二行:Task Execution Type 它可以指定這次更新中行為樹的執(zhí)行次數(shù)
默認是No Dullicates,也就是無復(fù)制無重復(fù)的意思,也就是1次,每次你更新行為樹,行為樹會執(zhí)行一次。
如果你指定了5次,那么每次更新就會執(zhí)行5次
讓我們回到之前的話題,到這里我想你已經(jīng)明白了什么事Tick,簡單來說也就是行為樹更新指令
讓我們來看看執(zhí)行順序流程圖:
API
// 當行為樹被啟用時,OnAwake被調(diào)用一次。可以把它看作一個構(gòu)造函數(shù) public virtual void OnAwake(); // OnStart在執(zhí)行之前立即被調(diào)用。它用于設(shè)置需要在上次運行后重新設(shè)置的任何變量 public virtual void OnStart(); // OnUpdate運行實際的任務(wù) public virtual TaskStatus OnUpdate(); // 在執(zhí)行成功或失敗后調(diào)用OnEnd。 public virtual void OnEnd(); // 當行為暫停并恢復(fù)時,調(diào)用OnPause public virtual void OnPause(bool paused); // 優(yōu)先級選擇需要知道該任務(wù)的運行優(yōu)先級 public virtual float GetPriority(); // OnBehaviorComplete在行為樹完成執(zhí)行后被調(diào)用 public virtual void OnBehaviorComplete(); // 檢查器調(diào)用OnReset來重置公共屬性 public propertiespublic virtual void OnReset(); // 允許從任務(wù)中調(diào)用OnDrawGizmos public virtual void OnDrawGizmos(); // 保留對擁有此任務(wù)的行為的引用 public Behavior Owner;父任務(wù) Parent Tasks
behavior tree 行為樹中的父任務(wù) task 包括:composite(復(fù)合),decorator(修飾符)!
以下是他的可擴展API,雖然 Monobehaviour 沒有類似的 API,但是并不難去理解這些功能
//一個父任務(wù)可以擁有的子任務(wù)的最大數(shù)量。通常為1或int。MaxValue public virtual int MaxChildren(); //布爾值,以確定當前任務(wù)是否為并行任務(wù) public virtual bool CanRunParallelChildren(); //當前活動子節(jié)點的索引 public virtual int CurrentChildIndex(); //布爾值,以確定當前任務(wù)是否可以執(zhí)行 public virtual bool CanExecute(); //為執(zhí)行狀態(tài)應(yīng)用裝飾器,輸入?yún)?shù)為被修飾節(jié)點的狀態(tài) public virtual TaskStatus Decorate(TaskStatus status); //通知parenttask子任務(wù)已被執(zhí)行,其狀態(tài)為childStatus public virtual void OnChildExecuted(TaskStatus childStatus); //通知父任務(wù),其子任務(wù)childIndex已被執(zhí)行,其狀態(tài)為childStatus public virtual void OnChildExecuted(int childIndex, TaskStatus childStatus); //通知任務(wù)子進程已經(jīng)開始運行 public virtual void OnChildStarted(); //通知并行任務(wù),索引為childIndex的子任務(wù)已開始運行 public virtual void OnChildStarted(int childIndex); //一些父任務(wù)需要能夠覆蓋狀態(tài),例如并行任務(wù) public virtual TaskStatus OverrideStatus(TaskStatus status); //如果中斷節(jié)點被中斷,它將覆蓋狀態(tài)。 public virtual TaskStatus OverrideStatus(); //通知復(fù)合任務(wù),條件中止已被觸發(fā),子索引應(yīng)重置 public virtual void OnConditionalAbort(int childIndex);條件節(jié)點的終止
一共有四種中斷類型的 abort types: None, Self, Lower Priority, and Both.
如上圖,所有的條件節(jié)點都有一個屬性:Abort Type,也就是中止類型
- None
- 無中止
- Self
- 這是一種自包含中斷類型。也就是會檢測此節(jié)點下所有條件判斷節(jié)點,即便是被執(zhí)行過的節(jié)點,如果判斷條件不滿足則打斷當前執(zhí)行順序從新回到判斷節(jié)點判斷,并返回判斷結(jié)果!
- Lower Priority
- 當運行到后續(xù)節(jié)點時,本節(jié)點的判斷條件生效了的話則打斷當前執(zhí)行順序,返回本節(jié)點執(zhí)行!
- Both
- 同時包含Self與LowerPriority
如果你是剛剛接觸行為樹,我相信,到這里你一定還不明白,接下來,我們結(jié)合例子進行講解
行為樹應(yīng)用簡單例子
如圖:
這里我們首先說明Selector選擇節(jié)點,這是系統(tǒng)默認的選擇節(jié)點,你可以理解為if else,從左向右依次選擇,如果能執(zhí)行就選擇該子樹執(zhí)行,如果不能執(zhí)行則選擇下一個子樹。注意,我們的用詞,子樹,也就是說,如果要執(zhí)行下去,那么左邊這個子樹的條件節(jié)點必須返回Success,否則,那么就會執(zhí)行下一個子樹。
當前的條件節(jié)點是兩個Int變量的比較,其中一個我們使用了全局變量 A=100,另外一個則賦值初始值10。
其次,左邊的Sequence我們賦予了Lower Priority中止
現(xiàn)在,我們執(zhí)行它看看結(jié)果,最初是這樣的:
方框呈現(xiàn)綠色,說明該節(jié)點正在運行,X則表示返回Failure或者不能執(zhí)行,?表示該狀態(tài)本身返回Success
當我們修改第二個Int值為1000,使條件節(jié)點返回Success,行為樹就變成下面這樣:
這里有一點需要說明:中止只會發(fā)生在節(jié)點運行過程中,如果節(jié)點已經(jīng)運行完畢,也就是出現(xiàn)了綠色的勾,此時說明執(zhí)行完畢,行為樹已經(jīng)結(jié)束,中止就無效了。
如果你之前使用的都是狀態(tài)機,我想你需要轉(zhuǎn)變過來,行為樹并非一種狀態(tài),它僅僅包含了一系列行為的邏輯模式,一旦它執(zhí)行完畢,不會向狀態(tài)機一樣執(zhí)行完就回到Idle,行為樹就結(jié)束了。如果你想要再次執(zhí)行它,就需要再次通過腳本來啟用他。
當我們的條件節(jié)點通過了,此時中止就生效了,他會中止第二個Sequence,然后執(zhí)行本節(jié)點
此外,你可以通過點擊節(jié)點左上角的X,來禁用該子樹
自定義Task
自定義行為節(jié)點
這是代碼
首先,需要引入命名空間:
using BehaviorDesigner.Runtime; using BehaviorDesigner.Runtime.Tasks;對于行為節(jié)點,我們僅僅需要讓該類繼承Action,然后重寫OnUpdate函數(shù)即可,你想要實現(xiàn)的功能就寫在這個函數(shù)中,我這里實現(xiàn)的就是讓對象朝向目標移動
using System.Collections; using System.Collections.Generic; using UnityEngine; using BehaviorDesigner.Runtime; using BehaviorDesigner.Runtime.Tasks;public class MyMoveTo : Action {public GameObject enemy;public override TaskStatus OnUpdate(){if (GetComponent<Player>()){if (Vector3.Distance(transform.position, enemy.transform.position) < 0.1f){return TaskStatus.Success;}else{transform.position = Vector3.MoveTowards(transform.position,enemy.transform.position,Time.deltaTime);return TaskStatus.Running;}}else{return TaskStatus.Failure;}} }自定義條件節(jié)點
同樣引入命名空間,然后繼承Conditional
判斷敵人是否存活,如果否,那么返回Failure
public class MyConditional : Conditional {public GameObject enemy;public override TaskStatus OnUpdate(){if (enemy.GetComponent<EnemyState>().alive){return TaskStatus.Success;}else{return TaskStatus.Failure;}} }自定義修飾節(jié)點
繼承Decorator
修飾節(jié)點比較特殊,我這里重寫了四個函數(shù)
第一個函數(shù)是CanExecute(),這個函數(shù)的返回值表明該節(jié)點能否通過
第二個函數(shù)是OnChildExecuted(TaskStatus childStatus),輸入?yún)?shù)為孩子節(jié)點狀態(tài),該函數(shù)會在孩子節(jié)點執(zhí)行的時候調(diào)用
第三個函數(shù)為Decorate(TaskStatus status),輸入?yún)?shù)仍然是孩子節(jié)點狀態(tài),該函數(shù)的返回值會改變孩子節(jié)點的返回值
最后一個函數(shù)就沒什么可說的了,這里我使用的行為樹如下:
這里我仔細講解一下,行為樹調(diào)用過程,首先通過Selector嘗試調(diào)用Sequence,當他調(diào)用到MyBreak的時候,由于我們一開始的CanExecute通過條件為
executionStatus == TaskStatus.Inactive || executionStatus == TaskStatus.Running
也就是未激活或者正在運行時都允許通過,此時,通過MyBreak,走到MyConditional,然后判斷MyConditional返回的狀態(tài),如果返回Success,此時通過OnChildExecuted檢測到孩子節(jié)點狀態(tài)為Success,我們定義的變量executionStatus被賦予Success,這導(dǎo)致CanExecute返回false,此時行為樹從這條路中退出,能夠繼續(xù)運行,接下來繼續(xù)執(zhí)行MyMoveTo,這里有一點要注意,因為我們已經(jīng)執(zhí)行了條件判斷,因此才可以執(zhí)行MyMoveTo,我們之所以要在執(zhí)行完條件判斷后將該修飾節(jié)點的CanExecute返回false,是為了能夠繼續(xù)執(zhí)行下去,如果這個函數(shù)一直返回true,這會導(dǎo)致一直執(zhí)行條件判斷,就無法繼續(xù)執(zhí)行MyMoveTo了!;如果條件節(jié)點返回Failure那么該Sequence執(zhí)行失敗,Selector選擇執(zhí)行第二個Sequence。
這里要注意一點,中止導(dǎo)致的更新只有條件節(jié)點才能觸發(fā),并且在觸發(fā)時只會遵循那時刻的所有節(jié)點的變量,如果你嘗試更改其他節(jié)點使其無法執(zhí)行,這是沒有用的。只有條件節(jié)點才能觸發(fā)此類行為
using BehaviorDesigner.Runtime; using BehaviorDesigner.Runtime.Tasks;public class MyBreak : Decorator {private TaskStatus executionStatus = TaskStatus.Inactive;public override bool CanExecute(){Debug.Log(executionStatus == TaskStatus.Inactive || executionStatus == TaskStatus.Running);return executionStatus == TaskStatus.Inactive || executionStatus == TaskStatus.Running;}public override void OnChildExecuted(TaskStatus childStatus){// Update the execution status after a child has finished running.executionStatus = childStatus;}public override TaskStatus Decorate(TaskStatus status){if (GetComponent<Player>() && GetComponent<Player>().ready){return TaskStatus.Success;}else{return TaskStatus.Failure;}}public override void OnEnd(){executionStatus = TaskStatus.Inactive;} }使用腳本創(chuàng)建一個行為樹
在某些情況下,你可能想要通過腳本在運行時創(chuàng)建一個行為樹,而不是直接使用拖拽或者面板操作去創(chuàng)建!例如:如果你已經(jīng)導(dǎo)
出了一個外部行為樹,并想通過腳本創(chuàng)建它的話,可以如下這么做:
behaviorTree.StartWhenEnabled = false;使得行為樹不要再一開始的時候執(zhí)行
后續(xù)當你想要調(diào)用他的時候,使用behaviorTree.EnableBehavior();來主動執(zhí)行行為樹
using System.Collections; using System.Collections.Generic; using UnityEngine; using BehaviorDesigner.Runtime; using BehaviorDesigner.Runtime.Tasks;public class BT_Test : MonoBehaviour {public bool on;public BehaviorTree behaviorTree;public void Start(){behaviorTree = GetComponent<BehaviorTree>();behaviorTree.StartWhenEnabled = false;}public void Update(){if (on){behaviorTree.EnableBehavior();on = false;}} }Event事件
為了說明事件,我們使用下面這個行為樹
這里我們使用了一個條件節(jié)點:Has Received Event
通過該條件節(jié)點的Inspector界面,我們可以看到,它有一個屬性和三個變量
- Event 事件名稱
- 參數(shù)
我們來看看他的源碼的一個函數(shù)
很明顯,在行為樹啟動時,他會自動以給定的名稱來注冊事件,一共有四種注冊事件(0個參數(shù)到3個參數(shù))
public override void OnStart() {// Let the behavior tree know that we are interested in receiving the event specifiedif (!registered) {Owner.RegisterEvent(eventName.Value, ReceivedEvent);Owner.RegisterEvent<object>(eventName.Value, ReceivedEvent);Owner.RegisterEvent<object, object>(eventName.Value, ReceivedEvent);Owner.RegisterEvent<object, object, object>(eventName.Value, ReceivedEvent);registered = true;} }我們再來看看注冊的函數(shù)
首先將eventReceived變量賦為true,然后設(shè)置對應(yīng)的參數(shù),即獲得參數(shù)
private void ReceivedEvent() {eventReceived = true; }private void ReceivedEvent(object arg1) {ReceivedEvent();if (storedValue1 != null && !storedValue1.IsNone) {storedValue1.SetValue(arg1);} }private void ReceivedEvent(object arg1, object arg2) {ReceivedEvent();if (storedValue1 != null && !storedValue1.IsNone) {storedValue1.SetValue(arg1);}if (storedValue2 != null && !storedValue2.IsNone) {storedValue2.SetValue(arg2);} }通過Update函數(shù),我們可以知道,當注冊事件被調(diào)用時,eventReceived為true,該條件節(jié)點就會返回Success
public override TaskStatus OnUpdate() {return eventReceived ? TaskStatus.Success : TaskStatus.Failure; }我們通過一個腳本來調(diào)用函數(shù),調(diào)用對應(yīng)的事件十分簡單,因為我們使用了一個名字來注冊事件,想要調(diào)用只需要使用behaviorTree.SendEvent()這個API即可調(diào)用,也就是發(fā)送,調(diào)用了它之后,Has Received Event條件節(jié)點就會返回Success,因此條件能夠通過,你可以把這個函數(shù)當作對應(yīng)名字事件的觸發(fā)器,觸發(fā)即可使得該條件節(jié)點返回Success。
另外,再次強調(diào)只有在行為樹運行的時候才有效,如果你的行為樹結(jié)束了,也就是出現(xiàn)綠色的勾了,說明行為樹已經(jīng)結(jié)束,此時再發(fā)送就無效了
using System.Collections; using System.Collections.Generic; using UnityEngine; using BehaviorDesigner.Runtime; using BehaviorDesigner.Runtime.Tasks;public class TaskA : MonoBehaviour {public bool send;public bool on;public BehaviorTree behaviorTree;public void Start(){behaviorTree = GetComponent<BehaviorTree>();behaviorTree.StartWhenEnabled = false;}public void Update(){if (on){behaviorTree.EnableBehavior();on = false;}if (send){behaviorTree.SendEvent<object>("MyEvent",1);send = false;}} }行為樹運行后,還未收到事件調(diào)用信息,此時我們使用Wait防止行為樹結(jié)束,保持運行
我們發(fā)送信息后,條件節(jié)點收到信息后,通過執(zhí)行
Task的引用
我們先創(chuàng)建兩個行為節(jié)點,并在一個節(jié)點中創(chuàng)建第二個節(jié)點公共變量,并嘗試打印信息
public class TaskF : Action {public TaskS referencedTask;public override void OnAwake(){Debug.Log(referencedTask.some);} } public class TaskS : Action {public float some; }我們來看看Inspector界面
在屬性界面,它顯示出了一個引用任務(wù)的選擇按鈕,因為對于行為節(jié)點我們無法直接賦值,因此采取選擇的方式
點擊Select,然后點擊行為樹中對應(yīng)類型的行為節(jié)點,此時就是將選中行為節(jié)點賦予給本節(jié)點的此變量
通過點擊X,可以取消引用
在引用后,我們啟動行為樹就會正常打印,如果不引用就會報出空指針異常。
變量同步器(Variable Synchronizer)
同步全局變量,箭頭表示同步方向,下圖中就是將A的值同步給B,點按Add添加同步操作
添加之后,仍然可以改變同步方向,點按箭頭按鈕即可,啟動后才能看到同步效果
Task可用特性
HelpURL : web 連接
[HelpURL("http://www.opsive.com/assets/BehaviorDesigner/documentation.php?id=27")] public class Parallel : Composite{ }TaskIcon :任務(wù)的圖標
[TaskIcon("Assets/Path/To/{SkinColor}Icon.png")] public class MyTask : Action{ }TaskCategory:任務(wù)的顯示位置(在 Task 任務(wù)面板中的顯示位置)
[TaskCategory("Common")] public class Seek : Action{ }[TaskCategory("RTS/Harvester")] public class HarvestGold : Action{ }TaskDescription:功能描述的文本內(nèi)容,顯示在編輯器布局區(qū)域的左下角
[TaskDescription("The sequence task is similar to an \"and\" operation. ..."] public class Sequence : Composite{ }LinkedTask:應(yīng)用其他的 Task 任務(wù)
[LinkedTask] public TaskGuard[] linkedTaskGuards = **null**;InheritedField : 繼承屬性
[InheritedField] public float moveSpeed;總結(jié)
以上是生活随笔為你收集整理的Unity-Behavior Designer详解的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Office 好压 看图王 搜狗
- 下一篇: 开启七牛云CDN免费HTTPS支持