前言
unity行为树简介
目前在Unity3D游戏中一般复杂的AI都可以看到行为树的身影,简单的AI使用状态机来实现就可以了,建议提前学习,做好准备,这叫“不打无准备之仗”哈哈哈。
行为树的概念出现已经很多年了,总的来说,就是使用各种经典的控制节点+行为节点进行组合,从而实现复杂的AI。
Behavior Designer插件里,主要有四种概念节点,都称之为Task。包括:
(1) Composites 组合节点,包括经典的:Sequence,Selector,Parallel
(2) Decorator 装饰节点,顾名思义,就是为仅有的一个子节点额外添加一些功能,比如让子task一直运行直到其返回某个运行状态值,或者将task的返回值取反等等
(3) Actions 行为节点,行为节点是真正做事的节点,其为叶节点。Behavior Designer插件中自带了不少Action节点,如果不够用,也可以编写自己的Action。一般来说都要编写自己的Action,除非用户是一个不懂脚本的美术或者策划,只想简单地控制一些物件的属性。
(4) Conditinals 条件节点 ,用于判断某条件是否成立。目前看来,是Behavior Designer为了贯彻职责单一的原则,将判断专门作为一个节点独立处理,比如判断某目标是否在视野内,其实在攻击的Action里面也可以写,但是这样Action就不单一了,不利于视野判断处理的复用。一般条件节点出现在Sequence控制节点中,其后紧跟条件成立后的Action节点。
行为树(Behavior Tree)具有如下的特性:
它的4大类型的节点:1. Composite 2.Decorator 3.Condition 4. Action Node
任何Node被执行后,必须向其Parent Node报告执行结果:成功 / 失败。
这简单的成功 / 失败汇报原则被很巧妙地用于控制整棵树的决策方向。
一个简单的敌人AI
当处于监视范围内,跑向玩家,当处于攻击范围内,攻击玩家,否则呆在原地,用行为树表示如下:
正文
个人对行为树的理解
目前为止我的理解是有的时候行为树式可以看成一个状态机的。
selecter选择大状态,大状态里的selecter选择小状态,这些同级的状态存在从左到右的优先级,从而简化了一些判断条件。
既然有状态就有判断状态执不执行的判断语句,判断语句可以sequencer与condition组合使用,也可直接用conditional节点其实时一样的。
光这样还不行,因为不是动态的,进入一个action之后的每帧会等待这个任务完成,而不会重新从左到右检测条件去选择任务。(比如小怪在巡逻,他见到玩家可能不会攻击,它此时进入巡逻状态了,没执行检测玩家语句,所以看不见玩家。)这样就应该把selecter设为Dynamic,虽然巡逻的任务没有结束,但每帧都按优先级先判断左侧的条件,看到玩家就会切换到chase状态。
if(){} else if(){} else if(){ if(){} else{} }
if(){ if(){} else if() {} else{} else{}
有限状态机与行为树
为什么很多人认为有限状态机很麻烦?
因为从某些方面来说,有限状态机则是舍掉了每个状态的优先级,而这样换来的则是高拓展性,每新增状态时只要加转换条件就行了。另外每个状态都分开也增加了可维护性。但是因为舍掉优先级把任何两个状态的转换都用条件判断来实现这样的不便之处是每个状态都要为它可以转换到的状态写转换条件,这样无疑增加了工作量。可以参考unity的动画状态机当状态太多的时候。
行为树则更像是我们平时写脚本,既保留了每个状态的优先级关系,省略了状态机因舍弃状态优先级而增加的状态转换条件,又可以模块化出各个状态,实现高拓展性和高维护性(每个selecter下面的子树都是一个状态,如果优先级和树的层级关系设计的好的话是可以弄出状态机那味儿的,这行为树多是件美事啊 看下边儿),行为树设计的好写代码的结构一定也很清晰。
Tips:构建一个行为树的时候不应该是盲目的而是有一个整体的通过selecter和sequencer规划清晰的结构,这样才不会盲目的乱连节点。
基本框架
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, } }