简介
我们在设计 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。
- 更新基础内容。