行为树-Unity
前言
本章节博客是在项目需求设计支持Lua AI系统的前提下,驱动深入学习行为树设计实现。关于AI的相关基础概念学习可以参考:Programming Game AI by Example。而本章节的重点就是行为树,最终目的是实现一份Unity里能够支持项目级使用的支持Lua测使用AI的行为树框架设计。源代码考虑到公司正在使用所以暂时不放源码,大家可以参考设计思路自行实现。
行为树
行为树介绍
行为树首先要知道的一点,是一颗树形结构,树里包含控制决策走向的控制节点和负责展示表现的叶子结点。
行为树抽象一个角色的简单AI角色逻辑如下:
优点:
- 可视化决策逻辑
- 可复用控制节点
- 逻辑和实现低耦合(叶子结点才是与上层逻辑挂钩的)
节点分类
从设计上来说节点功能划分为两类:
- 决策节点(决定行为树走向的非叶子节点)
- 行为节点(叶子节点决定最后的行为表现)
决策节点分类:
- 组合节点(序列节点(相当于and)、选择节点(相当于or)、并行节点(相当于for循环)等)
- 装饰节点(可以作为某种节点的一种额外的附加条件,如允许次数限制,时间限制,错误处理等。Note:装饰节点只有一个子节点)
行为节点分类:
- 条件节点(控制节点结果)
- 动作节点(实际的行为表现)
节点状态
所有的节点都有三种状态:
- 运行中(Running)
- 运行完毕(Success / Failed)
- 无效(Invalide)
正是通过上面的三种状态来实现行为树的行为决策的(所有节点都有返回状态)
节点选择
决策控制选择节点时需要有依据,这里的依据可以看做是选择某个节点的一个前提条件(Precondition),前提可以让我们快速判定分支避免不必要的子节点判定。控制节点相当于默认返回True,叶子节点可以作为选择节点的依据。
关于节点选择更多的优化(e.g. 1. 带优先级的选择 2. 带权值的选择……)参考:
行为树(Behavior Tree)实践(2)– 进一步的讨论
Note:
这里考虑到前置条件等价于组合节点的第一个节点设置条件节点,所以最终实战实现里没有实现前置条件(Precondition)
行为树更新
行为树更新方案:
- 每次从上次运行的节点运行(打断时从根节点重新运行)
行为树中断
前面我们已经选择了方案2,这里就要考虑一下如何实现行为打断了。
从前面可以看出行为树节点是有Running状态的,也就是说只要满足子节点的层层条件后,可以持续执行一段时间(比如寻找目标地点寻路过去这段时间都是Running)。
试想一下游戏过程中所有的条件数据都是在实时变化的,执行寻找目标地点的条件可以在任何时候出现条件不满足的情况,那么我们如何打断正在执行的Running节点,让行为树判定执行一条符合条件的行为分支了?
通过了解,BehaviorDesigner里存在4中类型的中断:
- None
- Self
- Lower Priority
- Both
详情参考:行为树 中断的理解
核心都是通过标识决策节点的打断类型以及配合设置相应条件节点来实现额外的条件判定,从而实现其他子节点运行时也可以动态判定其他节点条件来实现打断运行节点。
BehaviorDesigner节点有抽象出节点优先级概念(大概就是树形结构的层级顺序优先级)。
这里只是为了实现一个简易版的行为树,所以就不实现那么复杂的打断机制了。
方案:
行为树运行时记录运行时跑过的需要重新评估的节点的运行结果状态(现阶段主要是指条件节点),每一次从根节点运行时都判定需要重新评估的节点运行状态是否有变化,有变化表示条件有变行为树需要打断执行。
为什么使用行为树
从Programming Game AI by Example的介绍里可以看到相比状态机(FSM)行为树有以下几个好处:
- 使用行为树解决了状态机(FSM)过于复杂后维护成本高不利于扩展的问题。
- 行为树决策节点重用性高
- 行为树可视化编辑器能高效的查看行为树执行情况
行为树实战
目标:
- 实现一套可用于项目级别使用的支持Lua版编写AI的Unity行为树(深入理解行为树的原理和实现)
设计:
- 使用黑板模式作为单颗行为树的数据中心解决方案
- 使用Unity原生API实现行为树节点编辑器,支持高效方便的行为树编辑导出调试工具
- 使用Json(至于性能问题未来可选择高效的Json库,这里主要看中Json的可读性)作为行为树数据序列化反序列化方案
- 使用C#开发一套兼容CS和Lua的行为树框架(方便未来直接用于作为CS测的行为树解决方案),兼容Lua的核心思想是参数由string序列化,行为树节点反序列化构建时自行解析。
- 支持暂停,打断以及加载不同行为树Json的行为树框架
- 支持条件节点状态变化级别的行为树打断设计
数据管理
可以简单的理解成一个共享数据的地方,核心设计是一个K-V结构的Map容器(结合泛型的设计支持不同类型的数据访问),通过统一的数据访问方式实现行为树统一的数据访问。
1 | /// <summary> |
这里具体的黑板数据类型支持通过集成BlackboardData即可,这里就不放全部代码了。
黑板模式这里用于实现自定义变量并支持修改和读取判定,用于实现自定义变量控制AI逻辑。
黑板模式实现效果如下:
节点编辑器
关于节点编辑器,核心要了解Unity以下几个API:
- Event — UnityGUI事件响应编写接口类
- GUILayout.BeginArea() GUILayout.Toolbar() GUILayout.EndArea() EditorGUILayout.* — GUI自定义面板UI显示接口
- GenericMenu — Unity编写自定义菜单的类
- BeginWindows() GUI.Window() EndWindows() GUI.DragWindow() — Unity编写节点窗口的类(支持拖拽的节点)
- Handles.DrawBezier() — 绘制节点曲线(Bezier曲线)连接线的接口
- JsonUtility.ToJson() JsonUtility.FromJson
() — Unity Json数据序列化反序列化存储
结合上面几个核心类,最终实现行为树节点编辑器效果图如下:
行为树框架
这里考虑项目正在使用,所以不方便放出源码,通过给出行为树的UML类图设计,大家可以参考思路自行实现(因为UML比较大所以分了两张图):
行为树打断
这里单独谈谈行为树的打断实现,在BehaviourDesigner里行为树的打断设计相对比较复杂,详情可以参考:
Unity3D Behavior Designer 行为树4 打断机制的实现
而这里实现的行为树打断是一种较为简单打断方式,就是在每棵行为树重新从根节点开始执行时,记录执行过的节点以及需要重新判定的节点以及状态,在下一次从根节点运行时执行需要重新判定的节点是否有状态变化来判定是否需要打断行为树Running等执行状态。而需要打断的节点是从上一次记录的执行过的节点里执行结果为Running的才需要打断,其他状态不用打断。
行为树驱动执行代码大致如下:
BTGraph.cs1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60 /// <summary>
/// 更新
/// </summary>
public void OnUpdate()
{
if (RootNode != null)
{
// 根节点处于非成功和失败状态(一般来说是Running)
if (!RootNode.IsTerminated)
{
// 节点处于非运行时需要清除上一次运行数据
if(!RootNode.IsRunning)
{
// 清除上一次运行数据(比如执行过的节点和需要重新评估的节点数据)
ClearRunningDatas();
}
// 重新评估需要重新判定的节点,看状态是否有变化
if (ReevaluatedExecutedNodes())
{
// 重新判定节点运行结果有变化需要重置运行节点重新判定
DoAbortBehaviourTree();
}
RootNode.OnUpdate();
}
// 根节点处于成功或失败状态
else
{
// 清除上一次运行数据(比如执行过的节点和需要重新评估的节点数据)
ClearRunningDatas();
// 重新评估需要重新判定的节点,看状态是否有变化
if (ReevaluatedExecutedNodes())
{
// 重新判定节点运行结果有变化需要重置运行节点重新判定
DoAbortBehaviourTree();
}
// 判定行为树单次执行完后是否需要自动重启下一次执行
if (OwnerBT.RestartWhenComplete)
{
RootNode.OnUpdate();
}
}
}
}
/// <summary>
/// 执行终止行为树
/// </summary>
public void DoAbortBehaviourTree()
{
Debug.Log("DoAbortBehaviourTree()");
foreach (var executingnode in ExecutingNodesMap)
{
// 只有执行中的节点需要打断(避免执行完成的节点二次打断)
if(executingnode.Value.IsRunning)
{
executingnode.Value.OnConditionalAbort();
}
}
ClearRunningDatas();
}
而动态打断机制是通过指定特定节点(比如当前博主只支持条件节点)是否支持参与重新打断判定实现的。
BTBaseConditionNode.cs1
2
3
4
5
6
7
8/// <summary>
/// 是否可被重新评估
/// </summary>
/// <returns></returns>
protected override bool CanReevaluate()
{
return AbortType == EAbortType.Self;
}
公司项目没有继续做了,项目开源了:
Reference
行为树(Behavior Tree)实践(1)– 基本概念
行为树(Behavior Tree)实践(2)– 进一步的讨论
用800行代码做个行为树(Behavior Tree)的库(1)
用800行代码做个行为树(Behavior Tree)的库(2)
AI 行为树设计与实现-理论篇
行为树及其实现
unity 行为树 简单实现
基于Unity行为树设计与实现的尝试
游戏设计模式——黑板模式
Unity3D Behavior Designer 行为树4 打断机制的实现