简介
我们在设计 AI 的时候,可以通过状态机来实现,也可以通过行为树来实现。行为树更加灵活,适合逻辑更为复杂的 AI。
最基本的行为树每次都需要从根节点轮询,如果树深度比较大的情况下,每次都从头开始运行效率就没有那么高了。因此本项目存储和管理行为树最后运行的节点,如果行为树在中间某一节点停止运行,那下次就从这一节点开始运行,省去了重新查找的过程。
对于行为树的运行,本项目既可以选择轮询,也可以通过其他方法更新,例如事件来驱动更新。
行为树构建的工具源自于 Untiy-节点编辑器开发优化框架 GraphViewExtension 。
使用方法见 BehaviourTree 文档
原理
行为树节点
所有行为树的节点都继承自节点基础类 BhBaseNode 。
节点基础类包含以下字段:
- 父节点。
- 子节点列表。
- 节点状态。
- 最近运行节点索引。
- 是否允许打断。
以及字段操作相关的方法和以下可重写的核心方法:
Init初始化。Run节点运行逻辑。CheckState判断节点状态是否能够继续运行。CheckStop判断节点是否运行结束。Reset重置节点数据。
1 | using System.Collections.Generic; |
行为树管理类
TreeManager 负责管理行为树的创建、运行。
BlackboardManager 负责黑板数据的管理
行为树创建
通过配置初始化得到的行为树数据信息创建完整的行为树,并且返回根节点。
利用行为树数据信息中的节点类型,我们通过反射创建对应类型的节点。
由于行为树节点的数据是 ExpandoObject 类型,即 dynamic 运行时解析的,因此在初始化数据 node?.Init(data) 的时候要注意判断好数据中是否存在对应的字段,并且 node 字段是一定要存在的,这是创建节点的根据。
创建好节点后我们还会根据数据递归创建其子节点。
typeName 中的 BhTree. 是命名空间的前缀,如果你拓展的节点不在该命名空间,则修改为对应的空间。并且 typeName 必须提前拼接好,否则 GetType 会报错
1 | /// <summary> |
行为树运行
行为树的运行分为两个步骤:
graph TD; A["获取第一个运行的叶子结点"] B["自底向上运行节点"] A --> B
第一个步骤的运行逻辑如下:
graph TD; A["判断是否有未运行完的节点"] B["节点不变"] C["节点变为未运行完的节点"] D["运行第二步"] A --"有"--> C A --"没有"--> B B --> D C --> D
第二个步骤的运行逻辑如下:
graph TD;
A["节点是否为空"]
B["节点是否有子节点"]
C["判断节点是否和传入的父节点一致"]
D["节点是否可打断"]
E["获取节点索引"]
F["重置节点索引"]
subgraph G["遍历子节点"]
H["子节点递归执行RunNode
目的是从当前节点的叶子节点开始运行"]
I["运行节点"]
J["判断是否需要停止下一个子节点"]
K["遍历结束"]
L["遍历终止"]
T["保存当前索引和节点状态"]
end
M["判断节点是否执行结束"]
N["执行父节点"]
O["判断是否允许中断"]
P["设置未运行完成节点为当前节点"]
Q["向上循环获取最后一个允许中断的节点"]
R["清除未完成节点
重置节点树"]
S["结束运行"]
A --"是"--> R
A --"否"--> B
B --> C
C --"是"--> R
C --"否"--> D
D --"否"--> E
D --"是"--> F
E --> G
F --> G
H --> I
I --> J
J --"否"--> K
J --"是"--> L
L --> T
K --> T
G --> M
M --"是"--> N
M --"否"--> O
O --"否"--> P
O --"是"--> Q
所有运行都是基于子节点运行,当前节点本身是不执行 Run 的
1 | public void Run(BhBaseNode node) |
黑板数据管理
黑板数据管理就是利用字典来存储,原理很简单。
1 | public class BlackboardManager<T> : Singleton<BlackboardManager<T>> |
配置初始化工具类
本项目对行为树数据的存储结构为:
1 | public class SaveJson |
包括行为树的数据 data ,以及子节点 children 。
本项目使用 Newtonsoft.Json 来进行 Json 数据的反序列化。
- 首先我们从本地读取 Json 文件。
- 然后我们反序列化 Json。
- 最后返回 SaveJson 数据。
1 | public static SaveJson Load(string path) |
反序列化的流程如下:
1 | /// <summary> |
由于数据是以 dynamic 保存的,因此要把数据转为对应类型再保存。
从本地加载数据的方法可以按照自己的需求实现。
代码
行为树节点
1 | using System.Collections.Generic; |
行为树管理
行为树管理类
1 | using System; |
黑板类
1 | using System.Collections.Generic; |
数据初始化
行为树节点数据结构
1 | using System.Collections.Generic; |
数据初始化工具
1 | using System.Collections.Generic; |
行为树 Json 配置参考
1 | [ |
项目

更新日志
- 修复行为树逻辑 Bug。
- 更新基础内容。