Unity-行为树原理与框架实现
基本元素
BTNode:所有节点的base class。定义了一些节点的基本功能,并提供一些可继承的函数。
BTAction:行为节点,继承于BTNode。具体的游戏逻辑应该放在这个节点里面。
BTPrecondition:节点的准入条件,每一个BTNode都会有一个。具体的游戏逻辑判断可以继承于它。
BTPrioritySelector:Priority Selector逻辑节点,继承于BTNode。每次执行,先有序地遍历子节点,然后执行符合准入条件的第一个子结点。可以看作是根据条件来选择一个子结点的选择器。
BTSequence:Sequence逻辑节点,继承于BTNode。每次执行,有序地执行各个子结点,当一个子结点结束后才执行下一个。严格按照节点A、B、C的顺序执行,当最后的行为C结束后,BTSequence结束。
BTParallel:Parallel逻辑节点,继承于BTNode。同时执行各个子结点。每当任一子结点的准入条件失败,它就不会执行。
BTParallelFlexible:Parallel的一个变异,继承于BTNode。同时执行各个子节点。当所有子结点的准入条件都失败,它就不会执行。
BTTree:将所有节点组合起来的地方。
Database:一个存放共享数据的地方,可以看成是一个Key-Value的字典。为什么需要黑板呢?因为设计良好的行为逻辑,应该是独立的,可以在行为树的任何位置部署的。也就是说行为A和行为B并没有直接的沟通方法。黑板的作用就是作为一个行为树的“数据库”,让各个行为节点都可以储存数据进去,供感兴趣的行为节点利用。(同时,在Unity3d的语境下,Database继承MonoBehavior,可以提供各种Component给节点使用。)
基本框架
BTNode
行为树节点(BTNode)作为行为树所有节点的base Class,它需要有以下基本属性与函数/接口:
- 属性
- 节点名称(
name
) - 孩子节点列表(
childList
) - 节点准入条件(
precondition
) - 黑板(
Database
) - 冷却间隔(
interval
) - 是否激活(
activated
)
- 函数/接口
- 节点初始化接口(
public virtual void Activate (Database database)
) - 个性化检查接口(
protected virtual bool DoEvaluate ()
) - 检查节点能否执行:包括是否激活,是否冷却完成,是否通过准入条件以及个性化检查(
public bool Evaluate ()
) - 节点执行接口(
public virtual BTResult Tick ()
) - 节点清除接口(
public virtual void Clear ()
) - 添加/移除子节点函数(
public virtual void Add/Remove Child(BTNode aNode)
) - 检查冷却时间(
private bool CheckTimer ()
)
BTNode提供给子类的接口中最重要的两个是DoEvaluate()和Tick()。
DoEvaludate给子类提供个性化检查的接口(注意和Evaluate的不同),例如Sequence的检查和Priority Selector的检查是不一样的。例如Sequence和Priority Selector里都有节点A,B,C。第一次检查的时候,
Sequence只检查A就可以了,因为A不通过Evaluate,那么这个Sequence就没办法从头开始执行,所以Sequence的DoEvaludate也不通过。
而Priority Selector则先检查A,A不通过就检查B,如此类推,仅当所有的子结点都无法通过Evaluate的时候,才会不通过DoEvaludate。
Tick是节点执行的接口,仅仅当Evaluate通过时,才会执行。子类需要重载Tick,才能达到所想要的逻辑。例如Sequence和Priority Selector,它们的Tick也是不一样的:
Sequence里当active child节点A Tick返回Ended时,Sequence就会将当前的active child设成节点B(如果有B的话),并返回Running。当Sequence最后的子结点N Tick返回Ended时,Sequence也返回Ended。
Priority Selector则是当目前的active child返回Ended的时候,它也返回Ended。Running的时候,它也返回Running。
正是通过重载DoEvaluate和Tick,BT框架实现了Sequence,PrioritySelector,Parallel,ParalleFlexible这几个逻辑节点。如果你有特殊的需求,也可以重载DoEvaluate和Tick来实现!
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
namespace BT {
/// <summary>
/// BT node is the base of any nodes in BT framework.
/// </summary>
public abstract class BTNode {
//节点名称
public string name;
//孩子节点列表
protected List<BTNode> _children;
//节点属性
public List<BTNode> children {get{return _children;}}
// Used to check the node can be entered.
//节点准入条件
public BTPrecondition precondition;
//数据库
public Database database;
//间隔
// Cooldown function.
public float interval = 0;
//最后时间评估
private float _lastTimeEvaluated = 0;
//是否激活
public bool activated;
public BTNode () : this (null) {}
/// <summary>
/// 构造
/// </summary>
/// <param name="precondition">准入条件</param>
public BTNode (BTPrecondition precondition) {
this.precondition = precondition;
}
// To use with BTNode's constructor to provide initialization delay
// public virtual void Init () {}
/// <summary>
/// 激活数据库
/// </summary>
/// <param name="database">数据库</param>
public virtual void Activate (Database database) {
if (activated) return ;
this.database = database;
// Init();
if (precondition != null) {
precondition.Activate(database);
}
if (_children != null) {
foreach (BTNode child in _children) {
child.Activate(database);
}
}
activated = true;
}
public bool Evaluate () {
bool coolDownOK = CheckTimer();
return activated && coolDownOK && (precondition == null || precondition.Check()) && DoEvaluate();
}
protected virtual bool DoEvaluate () {return true;}
public virtual BTResult Tick () {return BTResult.Ended;}
public virtual void Clear () {}
public virtual void AddChild (BTNode aNode) {
if (_children == null) {
_children = new List<BTNode>();
}
if (aNode != null) {
_children.Add(aNode);
}
}
public virtual void RemoveChild (BTNode aNode) {
if (_children != null && aNode != null) {
_children.Remove(aNode);
}
}
// Check if cooldown is finished.
private bool CheckTimer () {
if (Time.time - _lastTimeEvaluated > interval) {
_lastTimeEvaluated = Time.time;
return true;
}
return false;
}
}
public enum BTResult {
Ended = 1,
Running = 2,
}
}
DataBase
在之前,我们已经说过了,数据库作为存放所有数据的地方,能够通过key-Value的方式去调取任意数据,你可以理解为全局变量黑板,我们可以手动添加数据,并通过节点来访问数据
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System;
/// <summary>
/// Database is the blackboard in a classic blackboard system.
/// (I found the name "blackboard" a bit hard to understand so I call it database ;p)
///
/// It is the place to store data from local nodes, cross-tree nodes, and even other scripts.
/// Nodes can read the data inside a database by the use of a string, or an int id of the data.
/// The latter one is prefered for efficiency's sake.
/// </summary>
public class Database : MonoBehaviour {
// _database & _dataNames are 1 to 1 relationship
private List<object> _database = new List<object>();
private List<string> _dataNames = new List<string>();
// Should use dataId as parameter to get data instead of this
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) {
if (BT.BTConfiguration.ENABLE_DATABASE_LOG) {
Debug.Log("Database: getting data for " + _dataNames[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;
}
}
// IMPORTANT: users may want to put Jargon in a separate file
//public enum Jargon {
// ShouldReset = 1,
//}
BTPrecondition
节点准入条件类,它继承与BTNode,他的抽象类如下:
它提供了一个新的接口:Check(),用于检查准入条件是否通过,通常用于条件节点
并重写了Tick函数,检查是否能够执行
public abstract class BTPrecondition : BTNode {
public BTPrecondition () : base (null) {}
// Override to provide the condition check.
public abstract bool Check ();
// Functions as a node
public override BTResult Tick () {
bool success = Check();
if (success) {
return BTResult.Ended;
}
else {
return BTResult.Running;
}
}
}
接下来是一些通用的准入条件,与数据库中的数据进行对比
using UnityEngine;
using System.Collections;
namespace BT {
/// <summary>
/// A pre condition that uses database.
/// </summary>
public abstract class BTPreconditionUseDB : BTPrecondition {
protected string _dataToCheck;
protected int _dataIdToCheck;
public BTPreconditionUseDB (string dataToCheck) {
this._dataToCheck = dataToCheck;
}
public override void Activate (Database database) {
base.Activate (database);
_dataIdToCheck = database.GetDataId(_dataToCheck);
}
}
/// <summary>
/// Used to check if the float data in the database is less than / equal to / greater than the data passed in through constructor.
/// </summary>
public class BTPreconditionFloat : BTPreconditionUseDB {
public float rhs;
private FloatFunction func;
public BTPreconditionFloat (string dataToCheck, float rhs, FloatFunction func) : base(dataToCheck){
this.rhs = rhs;
this.func = func;
}
public override bool Check () {
float lhs = database.GetData<float>(_dataIdToCheck);
switch (func) {
case FloatFunction.LessThan:
return lhs < rhs;
case FloatFunction.GreaterThan:
return lhs > rhs;
case FloatFunction.EqualTo:
return lhs == rhs;
}
return false;
}
public enum FloatFunction {
LessThan = 1,
GreaterThan = 2,
EqualTo = 3,
}
}
/// <summary>
/// Used to check if the boolean data in database is equal to the data passed in through constructor
/// </summary>
public class BTPreconditionBool : BTPreconditionUseDB {
public bool rhs;
public BTPreconditionBool (string dataToCheck, bool rhs) : base (dataToCheck) {
this.rhs = rhs;
}
public override bool Check () {
bool lhs = database.GetData<bool>(_dataIdToCheck);
return lhs == rhs;
}
}
/// <summary>
/// Used to check if the boolean data in database is null
/// </summary>
public class BTPreconditionNull : BTPreconditionUseDB {
private NullFunction func;
public BTPreconditionNull (string dataToCheck, NullFunction func) : base (dataToCheck) {
this.func = func;
}
public override bool Check () {
object lhs = database.GetData<object>(_dataIdToCheck);
if (func == NullFunction.NotNull) {
return lhs != null;
}
else {
return lhs == null;
}
}
public enum NullFunction {
NotNull = 1, // return true when dataToCheck is not null
Null = 2, // return true when dataToCheck is not null
}
}
public enum CheckType {
Same,
Different
}
}
行为节点
BTAction
至此,行为树的基本结构已经有了,Database作为我们的全局变量存放处,BTNode作为所有节点的base class
BTPrecondition作为节点的准入条件,我们可以开始编写我们的最重要的节点:行为节点
继承BTNode,行为节点基本属性与函数
- 属性
- 节点状态
行为节点无需准入条件,因此节点构造函数的的准入条件参数为null即可
- 接口
- 可扩展的执行接口
- 进入/退出接口
- 函数重写/独有函数
- 清除函数
- Tick函数
- 添加/删除孩子节点,由于行为节点应该是叶子节点,因此它无法添加/删除孩子节点,因此要重写函数使其失去这个功能
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
namespace BT {
public class BTAction : BTNode {
private BTActionStatus _status = BTActionStatus.Ready;
public BTAction (BTPrecondition precondition = null) : base (precondition) {}
protected virtual void Enter () {
if (BTConfiguration.ENABLE_BTACTION_LOG) { // For debug
Debug.Log("Enter " + this.name + " [" + this.GetType().ToString() + "]");
}
}
protected virtual void Exit () {
if (BTConfiguration.ENABLE_BTACTION_LOG) { // For debug
Debug.Log("Exit " + this.name + " [" + this.GetType().ToString() + "]");
}
}
protected virtual BTResult Execute () {
return BTResult.Running;
}
public override void Clear () {
if (_status != BTActionStatus.Ready) { // not cleared yet
Exit();
_status = BTActionStatus.Ready;
}
}
public override BTResult Tick () {
BTResult result = BTResult.Ended;
if (_status == BTActionStatus.Ready) {
Enter();
_status = BTActionStatus.Running;
}
if (_status == BTActionStatus.Running) { // not using else so that the status changes reflect instantly
result = Execute();
if (result != BTResult.Running) {
Exit();
_status = BTActionStatus.Ready;
}
}
return result;
}
public override void AddChild (BTNode aNode) {
Debug.LogError("BTAction: Cannot add a node into BTAction.");
}
public override void RemoveChild (BTNode aNode) {
Debug.LogError("BTAction: Cannot remove a node into BTAction.");
}
private enum BTActionStatus {
Ready = 1,
Running = 2,
}
}
}
逻辑节点
BTParallel(并行节点)
继承BTNode,并行节点的作用是同时执行他的孩子节点
- 属性
- 存储孩子节点执行结果的列表
- 并行类型:所有孩子节点都能通过,并行节点才能够通过/只要有任意一个孩子节点能通过,并行节点就能通过
- 函数重写
- 个性化检查:检查其孩子节点能够执行
- Tick函数
- 清除函数
- 添加/删除孩子节点函数
- 独有函数
- 重置结果,重置所有孩子节点结果
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using BT;
namespace BT {
/// <summary>
/// BTParallel evaluates all children, if any of them fails the evaluation, BTParallel fails.
///
/// BTParallel ticks all children, if
/// 1. ParallelFunction.And: ends when all children ends
/// 2. ParallelFunction.Or: ends when any of the children ends
///
/// NOTE: Order of child node added does matter!
/// </summary>
public class BTParallel : BTNode {
protected List<BTResult> _results;
protected ParallelFunction _func;
public BTParallel (ParallelFunction func) : this (func, null) {}
public BTParallel (ParallelFunction func, BTPrecondition precondition) : base (precondition) {
_results = new List<BTResult>();
this._func = func;
}
protected override bool DoEvaluate () {
foreach (BTNode child in children) {
if (!child.Evaluate()) {
return false;
}
}
return true;
}
public override BTResult Tick () {
int endingResultCount = 0;
for (int i=0; i<children.Count; i++) {
if (_func == ParallelFunction.And) {
if (_results[i] == BTResult.Running) {
_results[i] = children[i].Tick();
}
if (_results[i] != BTResult.Running) {
endingResultCount++;
}
}
else {
if (_results[i] == BTResult.Running) {
_results[i] = children[i].Tick();
}
if (_results[i] != BTResult.Running) {
ResetResults();
return BTResult.Ended;
}
}
}
if (endingResultCount == children.Count) { // only apply to AND func
ResetResults();
return BTResult.Ended;
}
return BTResult.Running;
}
public override void Clear () {
ResetResults();
foreach (BTNode child in children) {
child.Clear();
}
}
public override void AddChild (BTNode aNode) {
base.AddChild (aNode);
_results.Add(BTResult.Running);
}
public override void RemoveChild (BTNode aNode) {
int index = _children.IndexOf(aNode);
_results.RemoveAt(index);
base.RemoveChild (aNode);
}
private void ResetResults () {
for (int i=0; i<_results.Count; i++) {
_results[i] = BTResult.Running;
}
}
public enum ParallelFunction {
And = 1, // returns Ended when all results are not running
Or = 2, // returns Ended when any result is not running
}
}
}
行为树入口
之前的代码都是行为树框架本身,现在,我们需要通过节点去构建这个行为树入口,以能够真正的使用
- 属性
- 数据库
- 布尔变量,是否正在运行
- 重置变量,用于重置行为树
- 函数
- 初始化函数
- 更新函数
这个比较重要,每次更新函数,首先要判断行为树是否正在运行,然后判断是否重置。
其次才是判断节点的检查函数,如果检查通过则执行节点的Tick函数也就是执行函数
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using BT;
// How to use:
// 1. Initiate values in the database for the children to use.
// 2. Initiate BT _root
// 3. Some actions & preconditions that will be used later
// 4. Add children nodes
// 5. Activate the _root, including the children nodes' initialization
public abstract class BTTree : MonoBehaviour {
protected BTNode _root = null;
[HideInInspector]
public Database database;
[HideInInspector]
public bool isRunning = true;
public const string RESET = "Rest";
private static int _resetId;
void Awake () {
Init();
_root.Activate(database);
}
void Update () {
if (!isRunning) return;
if (database.GetData<bool>(RESET)) {
Reset();
database.SetData<bool>(RESET, false);
}
// Iterate the BT tree now!
if (_root.Evaluate()) {
_root.Tick();
}
}
void OnDestroy () {
if (_root != null) {
_root.Clear();
}
}
// Need to be called at the initialization code in the children.
protected virtual void Init () {
database = GetComponent<Database>();
if (database == null) {
database = gameObject.AddComponent<Database>();
}
_resetId = database.GetDataId(RESET);
database.SetData<bool>(_resetId, false);
}
protected void Reset () {
if (_root != null) {
_root.Clear();
}
}
}