接上篇,AI預定義邏輯可以把條件和行為邏輯看成一個Task,一個AI有多個Task,行為樹把單個Task中條件和行為、多個Task之間的關系進行細分成節點關系,通過樹形結構來分層,系統每幀從樹的根向下周遊,根據各節點的功能來執行子節點。
行為樹主要由以下四種節點抽象而成組合節點、裝飾節點、條件節點、行為節點。
①組合節點(Composites)
主要包含:Sequence順序條件,Selector選擇條件,Parallel平行條件以及他們之間互相組合的條件。
②修飾節點(Decorator)
連接配接樹葉的樹枝,就是各種類型的修飾節點,這些節點決定了 AI 如何從樹的頂端根據不同的情況,來沿着不同的路徑來到最終的葉子這一過程。
如讓子節點循環操作(LOOP)或者讓子task一直運作直到其傳回某個運作狀态值(Util),或者将task的傳回值取反(NOT)等等
③條件節點(Conditinals)
用于判斷某條件是否成立。目前看來,是Behavior Designer為了貫徹職責單一的原則,将判斷專門作為一個節點獨立處理,比如判斷某目标是否在視野内,其實在攻擊的Action裡面也可以寫,但是這樣Action就不單一了,不利于視野判斷處理的複用。一般條件節點出現在Sequence控制節點中,其後緊跟條件成立後的Action節點。
④行為節點(Action)
行為節點是真正做事的節點,行為節點在樹的最末端,都是葉子節點(注意葉子節點是沒有子節點的),就是這些 AI 實際上去做事情的指令;
通過使用行為樹内的節點之間的關聯來驅動角色的行為,比直接用具體的代碼告訴一個角色去做什麼事情,要來得有意思得多,這也是行為樹最讓人興奮的一點。這樣我們隻要抽象好行為,就不用去理會戰鬥中具體發生了什麼。
像修飾節點和條件節點可以放在組合節點中進行處理,是以組成三個節點:
1.根節點Root
2.邏輯節點:負責子節點的執行順序和子節點能否執行。
Sequence : 選擇第一個成功計算的子對象作為活動子對象。
PrioritySelector :計算算目前活動子節點,或者第一個子節點(如果沒有活動子節點)。如果通過計算,标記目前活動子節點,或者第一個子節點(如果沒有可用的活動子節點),如果結果是結束,那麼将活動子節點更改為下一個。
Parallel : 計算所有子節點,如果其中任何子節點計算失敗,則目前節點失敗。
3.行為節點
案例:一個怪物在指定路徑巡邏,當玩家怪物與玩家距離小于等于5米時會追逐玩家,當大于5米時會繼續巡邏。
條件:大于5五米、小于等于5米
行為:巡邏、追逐玩家
所有節點的根節點:NodeBase
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace VAE.BehaviourTree
{
public abstract class NodeBase
{
public enum NodeResult //節點傳回狀态
{
Ended = 1,
Running = 2,
}
public PreconditionBase preconditionBase; // 用來檢查節點是否可以輸入
public Database database;
public bool activated; //節點激活狀态
public float interval; //節點更新間隔
private float lastTimeEvaluated = 0;
protected List<NodeBase> children;
public List<NodeBase> Children { get { return this.children; } }
public NodeBase() : this(null) { }
public NodeBase(PreconditionBase preconditionBase)
{
this.preconditionBase = preconditionBase;
}
//激活節點
public virtual void Active(Database database)
{
if (this.activated) return;
this.database = database;
if (this.preconditionBase != null)
{
this.preconditionBase.Active(database);
}
if (this.children != null)
{
foreach (var child in this.children)
{
child.Active(database);
}
}
this.activated = true;
}
//沒幀進行校驗
public virtual NodeResult Update()
{
return NodeResult.Ended;
}
protected virtual bool DoEvaluate() { return true; }
public bool Evaluate()
{
bool coolDownOK = this.CheckTimer();
return this.activated && coolDownOK && (this.preconditionBase == null || preconditionBase.Check()) && this.DoEvaluate();
}
public virtual void Clear() { }
public virtual void AddChild(NodeBase nodeBase)
{
if (this.children == null)
{
this.children = new List<NodeBase>();
}
if (nodeBase != null)
{
this.children.Add(nodeBase);
}
}
public virtual void RemoveChild(NodeBase nodeBase)
{
if (this.children != null && nodeBase != null)
{
this.children.Remove(nodeBase);
}
}
//用來處理每個節點的檢查間隔
private bool CheckTimer()
{
if (Time.time - this.lastTimeEvaluated > this.interval)
{
this.lastTimeEvaluated = Time.time;
return true;
}
return false;
}
}
}
條件基類:PreconditionBase 條件會注冊到邏輯節點進行判定目前節點是否可以進行
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace VAE.BehaviourTree
{
public abstract class PreconditionBase : NodeBase
{
public PreconditionBase() : base(null) { }
public abstract bool Check(); // 條件檢測
public override NodeResult Update()
{
bool success = this.Check();
if (success)
{
return NodeResult.Ended;
}
else
{
return NodeResult.Running;
}
}
}
}
邏輯節點:PrioritySelector
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace VAE.BehaviourTree
{
/*
* 優先級選擇器
選擇第一個成功計算的子對象作為活動子對象。
*/
public class PrioritySelector : NodeBase
{
private NodeBase activeChild; //活動子對象
public PrioritySelector(PreconditionBase preconditionBase = null) : base(preconditionBase)
{
}
protected override bool DoEvaluate()
{
foreach (var child in this.children)
{
if (child.Evaluate()) //選擇計算成功的子對象
{
if (this.activeChild != null && this.activeChild != child)
{
this.activeChild.Clear();
}
this.activeChild = child;
return true;
}
}
this.activeChild = null;
return false;
}
public override NodeResult Update()
{
if (this.activeChild == null)
{
return NodeResult.Ended;
}
var result = this.activeChild.Update();
if (result != NodeResult.Running)
{
this.activeChild.Clear();
this.activeChild = null;
}
return result;
}
}
}
行為節點:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace VAE.BehaviourTree
{
/*
行為節點基類
該節點不能進行增加和删除子節點,隻用來處理行為
*/
public class ActionBase : NodeBase
{
private enum ActionStatus
{
Ready = 1,
Running = 2,
}
private ActionStatus actionStatus = ActionStatus.Ready;
public ActionBase(PreconditionBase preconditionBase = null) : base(preconditionBase)
{
}
protected virtual void Enter()
{
// 當行為進入時觸發的函數 一般用來處理:播放動畫等
}
protected virtual void Exit()
{
// 當行為退出時觸發的函數
}
// 執行邏輯
protected virtual NodeResult Execute()
{
return NodeResult.Running;
}
public override NodeResult Update()
{
NodeResult result = NodeResult.Ended;
if (this.actionStatus == ActionStatus.Ready)
{
this.Enter(); // 進入執行行為
this.actionStatus = ActionStatus.Running;
}
if (this.actionStatus == ActionStatus.Running)
{
result = this.Execute();
if (result != NodeResult.Running) //代表執行結束
{
this.Exit();
this.actionStatus = ActionStatus.Ready;
}
}
return result;
}
public override void Clear()
{
if (this.actionStatus != ActionStatus.Ready)
{
this.Exit();
this.actionStatus = ActionStatus.Ready;
}
}
public override void AddChild(NodeBase nodeBase)
{
Debug.LogError("Action: Cannot add a node into Action.");
}
public override void RemoveChild(NodeBase nodeBase)
{
Debug.LogError("Action: Cannot remove a node into Action.");
}
}
}
用于AI行為樹,各節點之間資料修改與擷取的:Database
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System;
namespace VAE.BehaviourTree
{
public class Database : MonoBehaviour
{
private List<object> _database = new List<object>();
private List<string> _dataNames = new List<string>();
public T GetData<T>(string dataName)
{
int dataId = IndexOfDataId(dataName);
if (dataId == -1) Debug.LogError("Database: Data for " + dataName + " does not exist!");
return (T)_database[dataId];
}
// Should use this function to get data!
public T GetData<T>(int dataId)
{
return (T)_database[dataId];
}
public void SetData<T>(string dataName, T data)
{
int dataId = GetDataId(dataName);
_database[dataId] = (object)data;
}
public void SetData<T>(int dataId, T data)
{
_database[dataId] = (object)data;
}
public int GetDataId(string dataName)
{
int dataId = IndexOfDataId(dataName);
if (dataId == -1)
{
_dataNames.Add(dataName);
_database.Add(null);
dataId = _dataNames.Count - 1;
}
return dataId;
}
private int IndexOfDataId(string dataName)
{
for (int i = 0; i < _dataNames.Count; i++)
{
if (_dataNames[i].Equals(dataName)) return i;
}
return -1;
}
public bool ContainsData(string dataName)
{
return IndexOfDataId(dataName) != -1;
}
}
}
行為樹驅動腳本:BTTreeBase 處理行為樹初始化,條件判定和更新
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace VAE.BehaviourTree
{
/*
*/
public class BTTreeBase : MonoBehaviour
{
protected NodeBase root = null;
[HideInInspector]
public Database database;
[HideInInspector]
public bool isRunning = true;
public const string RESET = "RESET";
private static int resetId;
private void Awake()
{
this.Init();
//當所有 限定節點 邏輯條件 添加完畢 再進行資料激活
this.root.Active(this.database);
}
private void Update()
{
if (!this.isRunning) return;
if (this.database.GetData<bool>(BTTreeBase.RESET))
{
this.Reset();
this.database.SetData<bool>(BTTreeBase.RESET, false);
}
if (this.root.Evaluate())
{
this.root.Update();
}
}
protected virtual void Init()
{
this.database = this.GetComponent<Database>();
if (this.database == null)
{
this.database = this.gameObject.AddComponent<Database>();
}
BTTreeBase.resetId = this.database.GetDataId(BTTreeBase.RESET);
this.database.SetData<bool>(BTTreeBase.resetId, false);
}
protected void Reset()
{
this.root.Clear();
}
}
}
外部邏輯:
處理限制條件:大于5五米、小于等于5米
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using VAE.BehaviourTree;
public class Precdontion_TransformDistance : PreconditionBase
{
public enum precdFunction
{
LessThan = 1,
GreaterThan = 2,
}
private string minChaseDistanceStr;
private int minChaseDistanceId;
private Transform itemTran; // 目标對象
private Transform enemyAI;
private precdFunction func;
public Precdontion_TransformDistance(string minChaseDistanceStr, Transform itemTran, precdFunction func)
{
this.minChaseDistanceStr = minChaseDistanceStr;
this.itemTran = itemTran;
this.func = func;
}
public override void Active(Database database)
{
base.Active(database);
this.minChaseDistanceId = this.database.GetDataId(this.minChaseDistanceStr);
this.enemyAI = this.database.transform;
}
public override bool Check()
{
if (itemTran == null) return false;
Vector3 offset = itemTran.position - this.enemyAI.position;
var minDistance = this.database.GetData<float>(this.minChaseDistanceId);
if (this.func == precdFunction.GreaterThan)
{
return (offset.sqrMagnitude >= minDistance);
}
else
{
return (offset.sqrMagnitude <= minDistance);
}
}
}
行為邏輯:
巡邏:Action_Partoll
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using VAE.BehaviourTree;
public class Action_Partoll : ActionBase
{
private List<Transform> partollPos;
private int index = 0;
private Transform enemyAI;
private float moveSpeed;
public Action_Partoll(List<Transform> partollPos, float speed)
{
this.partollPos = partollPos;
this.moveSpeed = speed;
}
public override void Active(Database database)
{
base.Active(database);
this.enemyAI = this.database.transform;
}
protected override NodeResult Execute()
{
var distance = Vector3.Distance(this.enemyAI.position, this.partollPos[this.index].position);
if (distance <= 0.1f)
{
this.index++;
this.index %= this.partollPos.Count;
}
Vector3 direction = (this.partollPos[this.index].position - this.enemyAI.position).normalized;
this.enemyAI.position += direction * this.moveSpeed * Time.deltaTime;
return NodeResult.Running;
}
}
追逐指定目标:Action_ChaseItem
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using VAE.BehaviourTree;
public class Action_ChaseItem : ActionBase
{
private Transform itemTran; //目标
private float moveSpeed;
private Transform enemyAI;
private string minChaseDistanceStr;
private int minChaseDistanceId;
public Action_ChaseItem(string minChaseDistanceStr, Transform itemTran, float speed)
{
this.itemTran = itemTran;
this.moveSpeed = speed;
this.minChaseDistanceStr = minChaseDistanceStr;
}
public override void Active(Database database)
{
base.Active(database); // 基類函數一定要執行
this.minChaseDistanceId = this.database.GetDataId(this.minChaseDistanceStr);
this.enemyAI = this.database.transform;
}
protected override NodeResult Execute()
{
if (this.CheckArrived())
{
return NodeResult.Ended;
}
Vector3 direction = (this.itemTran.position - this.enemyAI.position).normalized;
this.enemyAI.position += direction * this.moveSpeed * Time.deltaTime ;
return NodeResult.Running;
}
//檢驗是否應超出追逐範圍
private bool CheckArrived()
{
Vector3 offset = this.itemTran.position - this.enemyAI.position; // 怪物AI -》目的地的向量
float tmpMinDistance = this.database.GetData<float>(this.minChaseDistanceId);
return offset.sqrMagnitude > tmpMinDistance * tmpMinDistance;
}
}
EnemyAI:AI初始化
using System.Collections;
using System.Collections.Generic;
using VAE.BehaviourTree;
using UnityEngine;
public class EnemyAI : BTTreeBase
{
public Transform itemTran;
public float speed = 3;
public List<Transform> partollPos;
public float partollSpeed = 4;
public float minChaseDistance = 5; // 最小追逐距離
public const string MINCHASEDISTANCE = "MINCHASEDISTANCE";
protected override void Init()
{
base.Init(); // 一定要調用基類函數
this.root = new PrioritySelector(); // 建立行為樹根節點
//一個怪物在指定路徑巡邏,當玩家怪物與玩家距離小于等于5米時會追逐玩家,當大于5米時會繼續巡邏。這裡就分為了巡邏、追逐兩種狀态。
//
this.database.SetData<float>(MINCHASEDISTANCE, this.minChaseDistance);
Precdontion_TransformDistance precd_TransformDistance_greater = new Precdontion_TransformDistance(MINCHASEDISTANCE, itemTran, Precdontion_TransformDistance.precdFunction.GreaterThan);
Precdontion_TransformDistance precd_TransformDistance_less = new Precdontion_TransformDistance(MINCHASEDISTANCE, itemTran, Precdontion_TransformDistance.precdFunction.LessThan);
PrioritySelector tree2_1 = new PrioritySelector(precd_TransformDistance_greater);
Action_Partoll action_Partoll = new Action_Partoll(this.partollPos, this.partollSpeed);
tree2_1.AddChild(action_Partoll);
PrioritySelector tree2_2 = new PrioritySelector(precd_TransformDistance_less);
Action_ChaseItem action_ChaseItem = new Action_ChaseItem(MINCHASEDISTANCE, itemTran, this.speed);
tree2_2.AddChild(action_ChaseItem);
this.root.AddChild(tree2_1);
this.root.AddChild(tree2_2);
}
}
像上述,條件節點,行為節點還可以繼續進行細劃分,分的越詳細代碼複用越高。比如:AI巡邏行為接着可劃分為朝指定位置移動行為,這就需要新的邏輯節點來處理,進而達到複用追逐和巡邏兩個地方的行為功能。
行為樹缺點:為樹是依賴設計者的固定架構的,很不靈活,做的選擇不一定是最優選擇,而且每次都要經過大量的邏輯判斷,性能消耗嚴重。
而後另一種AI算法:GOAP(目标導向型行動計劃)更好的解決問題。
目前對行為樹了解還不太深刻,後續會繼續增加。。。
主要參考:
https://zhuanlan.zhihu.com/p/94850561
https://www.jianshu.com/p/23f79a365c10