接上篇,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