抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

简介

我们在设计 AI 的时候,可以通过状态机来实现,也可以通过行为树来实现。行为树更加灵活,适合逻辑更为复杂的 AI。

最基本的行为树每次都需要从根节点轮询,如果树深度比较大的情况下,每次都从头开始运行效率就没有那么高了。因此本项目存储和管理行为树最后运行的节点,如果行为树在中间某一节点停止运行,那下次就从这一节点开始运行,省去了重新查找的过程。

对于行为树的运行,本项目既可以选择轮询,也可以通过其他方法更新,例如事件来驱动更新。

行为树构建的工具源自于 Untiy-节点编辑器开发优化框架 GraphViewExtension

使用方法见 BehaviourTree 文档

原理

行为树节点

所有行为树的节点都继承自节点基础类 BhBaseNode

节点基础类包含以下字段:

  • 父节点。
  • 子节点列表。
  • 节点状态。
  • 最近运行节点索引。
  • 是否允许打断。

以及字段操作相关的方法和以下可重写的核心方法:

  • Init 初始化。
  • Run 节点运行逻辑。
  • CheckState 判断节点状态是否能够继续运行。
  • CheckStop 判断节点是否运行结束。
  • Reset 重置节点数据。
1
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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
using System.Collections.Generic;

namespace BhTree
{
public class BhBaseNode
{
public int id;

protected BhResult result;

private BhBaseNode _parent;

protected List<BhBaseNode> children = new List<BhBaseNode>();

protected int currentChildIndex;

public void SetResult(BhResult res)
{
result = res;
}

public BhResult GetResult()
{
return result;
}

/// <summary>
/// 添加子节点
/// </summary>
/// <param name="node"></param>
public void AddChild(BhBaseNode node)
{
children.Add(node);
node._parent = this;
}

/// <summary>
/// 获取所有子节点
/// </summary>
/// <returns></returns>
public List<BhBaseNode> GetChildren()
{
return children;
}

/// <summary>
/// 获取当前子节点索引
/// </summary>
/// <returns></returns>
public int GetCurIndex()
{
return currentChildIndex;
}

public void SetCurIndex(int index)
{
currentChildIndex = index;
}

/// <summary>
/// 获取父节点
/// </summary>
/// <returns></returns>
public BhBaseNode GetParent()
{
return _parent;
}

public virtual void Init(dynamic data)
{

}

public virtual void Run()
{
}

/// <summary>
/// 检测运行结果是否会终止执行状态
/// </summary>
/// <param name="res"></param>
/// <returns></returns>
public virtual bool CheckState(BhResult res)
{
return true;
}

public virtual bool CheckStop()
{
return true;
}

public virtual void Reset()
{
currentChildIndex = 0;
result = BhResult.Running;
}
}
}

行为树管理类

TreeManager 负责管理行为树的创建、运行。

BlackboardManager 负责黑板数据的管理

行为树创建

通过配置初始化得到的行为树数据信息创建完整的行为树,并且返回根节点。

利用行为树数据信息中的节点类型,我们通过反射创建对应类型的节点。

由于行为树节点的数据是 ExpandoObject 类型,即 dynamic 运行时解析的,因此在初始化数据 node?.Init(data) 的时候要注意判断好数据中是否存在对应的字段,并且 node 字段是一定要存在的,这是创建节点的根据。

创建好节点后我们还会根据数据递归创建其子节点。

typeName 中的 BhTree. 是命名空间的前缀,如果你拓展的节点不在该命名空间,则修改为对应的空间。并且 typeName 必须提前拼接好,否则 GetType 会报错

1
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
/// <summary>
/// 创建行为树
/// </summary>
/// <param name="json"></param>
/// <returns></returns>
public BhBaseNode InitBHNode(SaveJson json)
{
var data = json.data;

string typeName = "BhTree." + data.node;

Type type = Type.GetType(typeName);

BhBaseNode node = Activator.CreateInstance(type) as BhBaseNode;

//初始化数据
node?.Init(data);

node.id = _id++;

foreach (var child in json.children)
{
node?.AddChild(InitBHNode(child));
}

return node;
}

行为树运行

行为树的运行分为两个步骤:

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
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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
public void Run(BhBaseNode node)
{
if (node == null) return;

BhBaseNode root = node;

BhBaseNode curNode;

_curNode.TryGetValue(node, out curNode);

if (curNode != null)
{
node = curNode;
}

RunNode(node, root);
}

private void RunNode(BhBaseNode node, BhBaseNode root, BhBaseNode parent = null)
{
if (node == null)
{
Debug.Log("一次执行结束");
_curNode[root] = null;
root.Reset();
return;
}

if (node == parent)
{
return;
}

var children = node.GetChildren();

if (children.Count != 0)
{
bool interrupt = node.GetInterruptCheck();
int index = interrupt && node.GetResult() == BhResult.Running ? 0 : node.GetCurIndex();
BhResult res;

for (int i = index; i < children.Count; i++)
{
var child = children[i];
RunNode(child, root, node);

child.Run();
res = child.GetResult();

if (!node.CheckState(res) || i == children.Count - 1)
{
node.SetCurIndex(i);
node.SetResult(res);
break;
}
}

if (node.CheckStop())
{
RunNode(node.GetParent(), root, parent);
}
else
{
if (!interrupt)
{
_curNode[root] = node;
}
else
{
//如果当前节点可打断,向上查找到最后一个能打断的节点并存入
BhBaseNode baseNode = node;
BhBaseNode tempNode = null;
while (baseNode.GetInterruptCheck())
{
tempNode = baseNode;
baseNode = baseNode.GetParent();
}

_curNode[root] = tempNode;
}
}
}
}

黑板数据管理

黑板数据管理就是利用字典来存储,原理很简单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class BlackboardManager<T> : Singleton<BlackboardManager<T>>
{
private Dictionary<string, T> _blackboard = new Dictionary<string, T>();

public void SetBlackboard(string key, T data)
{
_blackboard[key] = data;
}

public T GetBlackboard(string key)
{
_blackboard.TryGetValue(key, out var data);
return data;
}
}

配置初始化工具类

本项目对行为树数据的存储结构为:

1
2
3
4
5
6
public class SaveJson
{
public List<SaveJson> children = new List<SaveJson>();

public dynamic data;
}

包括行为树的数据 data ,以及子节点 children

本项目使用 Newtonsoft.Json 来进行 Json 数据的反序列化。

  1. 首先我们从本地读取 Json 文件。
  2. 然后我们反序列化 Json。
  3. 最后返回 SaveJson 数据。
1
2
3
4
5
6
7
8
9
10
public static SaveJson Load(string path)
{
string jsonData = FileUtils.ReadFile(path);

SaveJson json = JsonConvert.DeserializeObject<List<SaveJson>>(jsonData)[0];

Deserialize(json);

return json;
}

反序列化的流程如下:

1
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
/// <summary>
/// 反序列化
/// </summary>
/// <param name="parent"></param>
private static void Deserialize(SaveJson parent)
{
dynamic obj = new ExpandoObject();
foreach (var res in (parent.data as JObject).Properties())
{
JTokenType jType = res.Value.Type;

// ConsoleUtils.Log(dataObj);
switch (jType)
{
case JTokenType.String:
((IDictionary<string, object>)obj)[res.Name] = res.Value.Value<string>();
break;
case JTokenType.Float:
((IDictionary<string, object>)obj)[res.Name] = res.Value.Value<float>();
break;
case JTokenType.Integer:
((IDictionary<string, object>)obj)[res.Name] = res.Value.Value<int>();
break;
case JTokenType.Boolean:
((IDictionary<string, object>)obj)[res.Name] = res.Value.Value<bool>();
break;
}
}

parent.data = obj;

foreach (var child in parent.children)
{
Deserialize(child);
}
}

由于数据是以 dynamic 保存的,因此要把数据转为对应类型再保存。

从本地加载数据的方法可以按照自己的需求实现。

代码

行为树节点
1
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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
using System.Collections.Generic;

namespace BhTree
{
public class BhBaseNode
{
public int id;

protected BhResult result;

private BhBaseNode _parent;

protected List<BhBaseNode> children = new List<BhBaseNode>();

protected int currentChildIndex;

public virtual void SetResult(BhResult res)
{
result = res;
}

public BhResult GetResult()
{
return result;
}

/// <summary>
/// 添加子节点
/// </summary>
/// <param name="node"></param>
public void AddChild(BhBaseNode node)
{
children.Add(node);
node._parent = this;
}

/// <summary>
/// 获取所有子节点
/// </summary>
/// <returns></returns>
public List<BhBaseNode> GetChildren()
{
return children;
}

/// <summary>
/// 获取当前子节点索引
/// </summary>
/// <returns></returns>
public int GetCurIndex()
{
return currentChildIndex;
}

public void SetCurIndex(int index)
{
currentChildIndex = index;
}

/// <summary>
/// 获取父节点
/// </summary>
/// <returns></returns>
public BhBaseNode GetParent()
{
return _parent;
}

public virtual void Init(dynamic data)
{

}

public virtual void Run()
{
}

/// <summary>
/// 检测运行结果是否会终止执行状态
/// </summary>
/// <param name="res"></param>
/// <returns></returns>
public virtual bool CheckState(BhResult res)
{
return true;
}

public virtual bool CheckStop()
{
return true;
}

public virtual void Reset()
{
currentChildIndex = 0;
result = BhResult.Running;
}
}
}
行为树管理
行为树管理类
1
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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
using System;
using System.Collections.Generic;
using UnityEngine;

namespace BhTree
{
public class TreeManager : Singleton<TreeManager>
{
/// <summary>
/// 节点编号
/// </summary>
private int _id = 0;

private Dictionary<BhBaseNode, BhBaseNode> _curNode = new Dictionary<BhBaseNode, BhBaseNode>();

/// <summary>
/// 创建行为树
/// </summary>
/// <param name="json"></param>
/// <returns></returns>
public BhBaseNode InitBHNode(SaveJson json)
{
var data = json.data;

string typeName = "BhTree." + data.node;

Type type = Type.GetType(typeName);

BhBaseNode node = Activator.CreateInstance(type) as BhBaseNode;

//初始化数据
node?.Init(data);

node.id = _id++;

foreach (var child in json.children)
{
node?.AddChild(InitBHNode(child));
}

return node;
}

public void Run(BhBaseNode node)
{
if (node == null) return;

BhBaseNode root = node;

BhBaseNode curNode;

_curNode.TryGetValue(node, out curNode);

if (curNode != null)
{
node = curNode;
}

RunNode(node, root);
}

private void RunNode(BhBaseNode node, BhBaseNode root,BhBaseNode parent = null)
{
if (node == null)
{
Debug.Log("一次执行结束");
_curNode[root] = null;
root.Reset();
return;
}

if (node == parent)
{
return;
}

var children = node.GetChildren();

if (children.Count != 0)
{
bool interrupt = node.GetInterruptCheck();
int index = interrupt && node.GetResult() == BhResult.Running ? 0 : node.GetCurIndex();
BhResult res;

for (int i = index; i < children.Count; i++)
{
var child = children[i];
RunNode(child,root,node);

child.Run();
res = child.GetResult();

if (!node.CheckState(res) || i == children.Count - 1)
{
node.SetCurIndex(i);
node.SetResult(res);
break;
}
}

if (node.CheckStop())
{
RunNode(node.GetParent(), root,parent);
}
else
{
if (!interrupt)
{
_curNode[root] = node;
}
else
{
//如果当前节点可打断,向上查找到最后一个能打断的节点并存入
BhBaseNode baseNode = node;
BhBaseNode tempNode = null;
while (baseNode.GetInterruptCheck())
{
tempNode = baseNode;
baseNode = baseNode.GetParent();
}

_curNode[root] = tempNode;
}
}
}
}

}
}
黑板类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using System.Collections.Generic;

namespace BhTree
{
public class BlackboardManager<T>: Singleton<BlackboardManager<T>>
{
private Dictionary<string, T> _blackboard = new Dictionary<string, T>();

public void SetBlackboard(string key, T data)
{
_blackboard[key] = data;
}

public T GetBlackboard(string key)
{
_blackboard.TryGetValue(key, out var data);
return data;
}
}
}
数据初始化
行为树节点数据结构
1
2
3
4
5
6
7
8
9
10
11
using System.Collections.Generic;

namespace BhTree
{
public class SaveJson
{
public List<SaveJson> children = new List<SaveJson>();

public dynamic data;
}
}
数据初始化工具
1
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
using System.Collections.Generic;
using System.Dynamic;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UnityEngine.Device;

namespace BhTree
{
public class ConfigLoader
{
public static SaveJson Load(string path)
{
string jsonData = FileUtils.ReadFile(path);

SaveJson json = JsonConvert.DeserializeObject<List<SaveJson>>(jsonData)[0];

Deserialize(json);

return json;
}

/// <summary>
/// 反序列化
/// </summary>
/// <param name="parent"></param>
private static void Deserialize(SaveJson parent)
{
dynamic obj = new ExpandoObject();
foreach (var res in (parent.data as JObject).Properties())
{
JTokenType jType = res.Value.Type;

// ConsoleUtils.Log(dataObj);
switch (jType)
{
case JTokenType.String:
((IDictionary<string, object>)obj)[res.Name] = res.Value.Value<string>();
break;
case JTokenType.Float:
((IDictionary<string, object>)obj)[res.Name] = res.Value.Value<float>();
break;
case JTokenType.Integer:
((IDictionary<string, object>)obj)[res.Name] = res.Value.Value<int>();
break;
case JTokenType.Boolean:
((IDictionary<string, object>)obj)[res.Name] = res.Value.Value<bool>();
break;
}
}

parent.data = obj;

foreach (var child in parent.children)
{
Deserialize(child);
}
}
}
}
行为树 Json 配置参考
1
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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
[
{
"children": [
{
"children": [
{
"children": [],
"data": {
"guid": "55ecdbc145e92094383246a89072a03a",
"pos": "(599.00, 394.00)",
"size": "(200.00, 142.00)",
"node": "ANodeTest",
"isSuc": false,
"desc": "测试节点成功"
},
"type": "GraphViewExtension.ANodeTest"
},
{
"children": [
{
"children": [],
"data": {
"guid": "15fb6edb82c92324e9e185f4c39fa4ba",
"pos": "(868.00, 293.00)",
"size": "(200.00, 142.00)",
"node": "ANodeTest",
"isSuc": false,
"desc": "测试节点失败"
},
"type": "GraphViewExtension.ANodeTest"
},
{
"children": [],
"data": {
"guid": "6e35a56ed5170804dbc6930776c2109d",
"pos": "(868.00, 116.00)",
"size": "(200.00, 138.00)",
"node": "ANodeWait",
"desc": "延时节点",
"time": 1538
},
"type": "GraphViewExtension.ANodeWait"
},
{
"children": [],
"data": {
"guid": "ffa50a43832af9c4eb543199149a9949",
"pos": "(868.00, -74.00)",
"size": "(200.00, 142.00)",
"node": "ANodeTest",
"isSuc": true,
"desc": "测试节点"
},
"type": "GraphViewExtension.ANodeTest"
}
],
"data": {
"guid": "3a8e290b08f711f46b99a7fa8baa7e28",
"pos": "(599.00, 149.00)",
"size": "(200.00, 158.00)",
"node": "CNodeSelect",
"interrupt": true
},
"type": "GraphViewExtension.CNodeSelect"
}
],
"data": {
"guid": "925a1f117afb5214e94ebe1bca57212a",
"pos": "(293.00, 234.00)",
"size": "(202.00, 160.00)",
"node": "CNodeSelect",
"interrupt": true
},
"type": "GraphViewExtension.CNodeSelect"
},
{
"children": [],
"data": {
"guid": "207185d6ee6a55f4292b01162d42614a",
"pos": "(306.00, 49.00)",
"size": "(200.00, 100.00)",
"node": "DNodeFail"
},
"type": "GraphViewExtension.DNodeFail"
}
],
"data": {
"guid": "ab1dc5fe007748c4e83865ddf11a61a8",
"pos": "(21.00, 149.00)",
"size": "(190.00, 161.00)",
"node": "CNodeSelect",
"interrupt": false
},
"type": "GraphViewExtension.CNodeSelect"
}
]

项目

更新日志

2024-06-25

  1. 修复行为树逻辑 Bug。

2024-06-18

  1. 更新基础内容。

评论