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();	
		}
	}
}