AI行为树的理解
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了AI行为树的理解相关的知识,希望对你有一定的参考价值。
参考技术A 基于前面几次的面试,自己进行行了总结:在问到我项目里的怪物AI逻辑的时候,我自己项目里用的是状态机制,算是行为树的一层,但是复杂的行为树有很多很多层,比较能完成的实习NPC或者怪物行为。自己也在百度上找了许多资料,对此谈下我自己的理解!
基础-Basics
1、行为树的名字很好地解释了它是什么。不像有限状态机(Finite State Machine)或其他用于 AI 编程的系统,行为树是一棵用于控制 AI 决策行为的、包含了层级节点的树结构。树的最末端——叶子,就是这些 AI 实际上去做事情的命令;连接树叶的树枝,就是各种类型的节点,这些节点决定了 AI 如何从树的顶端根据不同的情况,来沿着不同的路径来到最终的叶子这一过程。
2、行为树可以非常地“深”,层层节点向下延伸。凭借调用实现具体功能的子行为树,开发者可以建立相互连接的行为树库来做出非常让人信服的 AI 行为。并且,行为树的开发是高度迭代的,你可以从一个很简单的行为开始,然后做一些分支来应对不同的情境或是实现不同的目标,让 AI 的诉求来驱动行为,或是允许 AI 在行为树没有覆盖到的情境下使用备用方案等等。
树的遍历-Tree Traversal
行为树的一个特点是,它会“一层一层”地去对节点依次进行检查,而这每一层都需要花费一个 tick 的时间,所以它需要花数个 tick 才能完成从顶部走到底的过程,来完成其逻辑,这和一般用代码实现功能是很不同的。
这并不是一个很有效率的方式,尤其是当你的树变得非常深的时候。我认为行为树的实现必须具备可以在一个 tick 内完成整个行为树的判断逻辑。
工作流-Flow
行为树由多种不同类型的节点构成,它们都拥有一个共同的核心功能,即它们会返回三种状态中的一个作为结果。这三种状态分别是:
成功-Success;
失败-Failure;
运行中-Running;
一、前两个,正如它们的名字,是用来向它们的父节点通知运行的成功或失败的结果。第三种是指还在运行中,结果还未决定,在下一个 tick 的时候再去检查这个节点的运行结果。
二、这个功能非常重要,它可以让一个节点持续运行一段时间来维持某些行为。比如一个“walk(行走)”的节点会在计算寻路和让角色保持行走的过程中持续返回“Running”来让角色保持这一状态。如果寻路因为某些原因失败,或是除了某些状况让行走的行为不得不中止,那么这个节点会返回“Failure”来告诉它的父节点;如果这个角色走到了指定的目的地,那么节点返回“Success”来表示这个行走的指令已经成功完成。
三、一共有三种节点类型,它们分别是:
组合节点-Composite
修饰节点-Decorator;
叶节点-Leaf;
组合节点 -Composite
1、组合节点通常可以拥有一个或更多的子节点。这些子节点会按照一定的次序或是随机地执行,并会根据执行的结果向父节点返回“Success”、“Failure”,或是在未执行完毕时“Running”这样的结果值。
2、最常用的组合节点是 Sequence(次序节点),它很简单地按照固定的次序运行子节点,任何一个子节点返回 Failure,则这个组合节点向它的父节点返回 Failure;当所有子节点都返回 Success 时,这个组合节点返回 Success。
修饰节点-Decorator
1、修饰节点也可以拥有子节点,但是不同于组合节点,它只能拥有一个子节点。取决于修饰节点的类型,它的功能要么是修改子节点返回的结果、终止子节点,或是重复执行子节点等等。
2、一个比较常见的修饰节点的例子是 Inverter(逆变节点),它可以将子节点的结果倒转,比如子节点返回了 Failure,则这个修饰节点会向上返回 Success,以此类推。
叶节点-Leaf
1、叶节点是最低层的节点,它们不会拥有子节点。叶节点是最强大的节点类型,它们是真正让你的树做具体事情的基础元素。通过与组合节点和修饰节点的配合,再加上你自己对叶节点功能的定义,你可以实现非常复杂的、智能的行为逻辑。
2、拿代码作为类比的话,组合节点和修饰节点就好比那些改变代码 flow 的 if 判断和 while loop 等等,而叶节点就是那些真正起作用的被调用的方法,去让角色做什么或是进行某些条件判断。
3、参数可以在这些节点中起到作用,比如 Walk 的这个叶节点可以包含一个具体将要移动到的位置的参数。这些参数可以从其他变量里获得,比如角色将要前往的一个地点可以被 GetSafeLocation 这个节点所决定,存入一个变量里,然后 Walk 节点可以使用这个变量来定义它的目的地。行为树的运行中,这些不同的节点通过数据上下文来共同储存或使用一些持久数据(persistent data),使得行为树的功能变得强大。
4、另一种叶节点的类型是调用其他的行为树并把当前行为树的数据传给对方。
一文足矣:Unity&行为树
目录
前言
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 getreturn _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,
//
行为树入口
之前的代码都是行为树框架本身,现在,我们需要通过节点去构建这个行为树入口,以能够真正的使用:
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();
行为树的事件GraphEvent
当发送一个事件时,场景里的所有的owener都可以同时响应这个事件。
也可以通过脚本来发送事件,做受击响应可行。
发送事件
监听事件
脚本发送事件
行为树的管理&操作
一、操作单颗树
这是我们项目里面,一个敌人绑定了行为树,自动创建的behavior tree 脚本:
将红框放大可以看到:
行为树组件包含以下几个属性:
那当我们有需要的时候,如何代码操作这些变量呢?
(1)我们必须先找到要操作的树
找树的方法1:定义一个Public的 BehaviorTree tree = new BehaviorTree();,然后面板拖拽赋值。
找树的方法2:定义一个privarte的 BehaviorTree tree = new BehaviorTree();,然后通过GameObject 的Find查找物体,然后获得物体上面的组件来得到的。
(2)代码操作该树
using BehaviorDesigner.Runtime.Tasks;//引用不可少
using BehaviorDesigner.Runtime;
public class Tree : MonoBehaviour
public BehaviorTree tree = new BehaviorTree();
void Start ()
tree.enabled = false;
var a = tree.GetAllVariables();
tree.StartWhenEnabled = false;
var b = tree.FindTasksWithName("AI_Daze");
上面代码只是简单的演示一下,可以操作行为树的数据。其实 面板截图里面的所有变量都可以操作,除此之外,tree还有很多的属性和方法都可以操作。
二、管理所有树
当行为树运行时,将会自动创建一个带有行为管理器组件的新游戏对象,并且该对象上面绑有 behavior manager组件。此组件管理你场景中所有的执行的行为树
你可以控制行为树的更新类型,以及更新时间等等
Update Interval:更新频率
Every Frame:每帧都更新行为树
Specify Seconds:定义个一个更新间隔时间
Manual:手动调用更新,选择这个后需要通过脚本来调用行为树的更新
Task Execution Type:任务执行类型
No Duplicates:不重复
Repeater Task:重复任务节点。如果设置成了5,那么每帧被执行5次
BehaviorManager.instance.Tick();
此外,如果你想让不同的行为树都有各自独立的更新间隔的话,可以这样:
BehaviorManager.instance.Tick(BehaviorTree);
更多方法,请查看BehaviorManager类
自定义Task任务
一般复合类和装饰类的Task是够用的,甚至有些根本用不到,而具体的行为类Task和条件类Task从来都不能满足我们的需求,而且自己写这类Task可以很大程度的简化整个行为树结构。
自己写Task的步骤如下:
1.引入命名空间:
using BehaviorDesigner.Runtime;
using BehaviorDesigner.Runtime.Tasks;
2.明确继承的Task类型:
public class MyInputMove : Action
public class MyIsInput : Conditional
3.知晓Task内部函数的执行流程:
观察上图就会发现和Unity中编写脚本大同小异,不一样的地方就是这里的Update有返回值,要返回该任务的执行状态,只有在Running状态时才每帧调用:
using UnityEngine;
using BehaviorDesigner.Runtime;
using BehaviorDesigner.Runtime.Tasks;
public class MyInputMove : Action
public SharedFloat speed = 5f;
public override TaskStatus OnUpdate()
float inputX = Input.GetAxis("Horizontal");
float inputZ = Input.GetAxis("Vertical");
if (inputX != 0 || inputZ != 0)
Vector3 movement = new Vector3(inputX, 0, inputZ);
transform.Translate(movement*Time.deltaTime*speed.Value);
return TaskStatus.Running;
return TaskStatus.Success;
总结
行为树的如下几种优点
> 静态性
越复杂的功能越需要简单的基础,否则最后连自己都玩不过来。
静态是使用行为树需要非常着重的一个要点:即使系统需要某些"动态"性。
其实诸如Stimulus这类动态安插的Node看似强大,但却破坏了本来易于理解的静态性,弊大于利。
Halo3相对于Halo2对BT AI的一个改进就是去除Stimulus的动态性。取而代之的做法是使用Behavior Masks,Encounter Attitude,Inhibitions。
原则就是保持全部Node静态,只是根据事件和环境来检查是否启用Node。
静态性直接带来的好处就是整棵树的规划无需再运行时动态调整,为很多优化和预编辑都带来方便。
> 直观性
行为树可以方便地把复杂的AI知识条目组织得非常直观。默认的Composite Node的从begin往end的Child Node迭代方式就像是处理一个
预设优先策略队列,也非常符合人类的正常思考模式:先最优再次优。
行为树编辑器对优秀的程序员来说也是唾手可得。
> 复用性
各种Node,包括Leaf Node,可复用性都极高。实现NPC AI的个性区别甚至可以通过在一棵共用的行为树上不同的位置来安插Impulse来达到目的。当然,当NPC需要一个完全不同的大脑,比如100级大BOSS,与其绞尽脑汁在一棵公用BT安插Impulse,不如重头设计一棵专属BT。
> 扩展性
虽然上述Node之间的组合和搭配使用几乎覆盖所有AI需求。
但也可以容易地为项目量身定做新的Composite Node或Decorator Node。
还可以积累一个项目相关的Node Lib,长远来说非常有价值。
以上是关于AI行为树的理解的主要内容,如果未能解决你的问题,请参考以下文章