unity-doc-ai

传统逻辑判断

在状态机的理念没有提出来的时候,传统的判断逻辑都是如果A条件满足,就执行a行为。如果A条件不满足但是B条件满足,就执行b行为。随着条件和行为的增多,这种if else判断会非常长,可读性非常低。

1
2
3
4
5
6
7
8
// 通过简单的if-else 来判断状态
// switcah-case 来执行各种状态对应的行为
switch(状态) {
case 状态1:....break;
case 状态2:....break;
case 状态3:....break;
...
}

有限状态机

将一系列操作划分成各个状态,然后控制状态之间的相互切换。

优点

  • 逻辑复杂度不高、状态数量不多的时候代码较为清晰,符合人类思维逻辑。
  • 自由度高,可以在不同状态之间进行跳转。

缺点

  • 当状态机的模型复杂到一定的程度后,状态过渡将会变得复杂,导致维护困难。
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
using System.Collections.Generic;


abstract class FMSTransition
{
/// <summary>
/// 判断转换条件是否满足
/// </summary>
public abstract bool isValid();

/// <summary>
/// 获取将要过度的新状态
/// </summary>
/// <returns></returns>
public abstract FSMState getNextState();

/// <summary>
/// 触发过渡时的行为
/// </summary>
public abstract void onTransition();
}

abstract class FSMState {
/// <summary>
/// 状态进入行为
/// </summary>
public virtual void onEnter(){}

/// <summary>
/// 状态更新行为
/// </summary>
public virtual void onUpdate()
{
}

/// <summary>
/// 状态退出行为
/// </summary>
public virtual void onExist()
{

}

internal List<FMSTransition> transitions; // 状态过渡列表,代表能从当前状态可以转到的状态
}

abstract class FiniteStateMachine{

List<FSMState> states; // 所有状态列表
FSMState initialState; // 初始化状态
FSMState activeState; // 当前状态

void Update() {
foreach(var t in activeState.transitions) {
if (t.isValid()) {
var ns = t.getNextState();
activeState.onExist();
t.onTransition();
activeState = ns;
activeState.onEnter();
break;
}
}

activeState.onUpdate();
}
}

分层有限状态机

分层有限状态机(Hierarchical Finite State Machine,HFSM)简单概括就是将状态机安装功能模块划分,每个模块管理自己的状态机,分而治之。

多层状态机其实是对简单状态机的封装。大状态机的核心思想是将若干小状态封装成一个大转改,内部小状态的转换使用小状态及维护,大状态机之间的转换在大状态机下面维护。


优点

  • 从某种程度上规范了状态及的状态转移
  • 状态内的子状态不需要关心外部状态的跳转
  • 做到无关状态的间隔

缺点

  • 没有彻底解决FSM同样存在的问题,高层状态特别的多时候还是会出现状态切换的混乱。
  • 当状态机的模型复杂到一定的程度之后,还是会带来实现和维护上的困难。

行为树

行为树类似于一颗树,它有树根、树枝、树叶。树根就是树的根节点,后面的树枝(组合节点、装饰节点、条件节点等)和树叶(行为节点)都是在后面延伸。因为它是树状状态结构,对一些通用树叶(行为节点)可以直接服用,非常方便。

行为树和状态机最大的差别就是行为树通过条件触发各个行为,达到什么条件就执行什么行为。这样的好处是可以将每个行为单独提取出来,单独配置,后面只需要添加相应的条件就可以定制不同的行为,通过添加不同的条件去驱动行为。

优点

  • 易于理解并且可以使用可时候编辑器进行创建
  • 很大的灵活性,非常强大,并且非常容易对其进行更改
  • 能够创建由简单任务组成的非常复杂的任务,而不必担心简单任务是如何实现的
  • 每个行为逻辑互不影响,行为模块间的耦合度相对较低

缺点

  • 行为树做的选择并不一定是最优的,结果也不定是我们想要的。而去决策每次都要从根部往下判断选择行为节点,比状态机要耗费时间。每次决策都要经过大量的判断语句,会变得非常慢。
  • 如果ai对象只有比较少的状态,用行为树设计的话,计算量反而更大,更耗费性能。

在行为树结构里,父节点需要根据当前子节点的执行结构来决定后面应该要执行哪条分支。子节点的状态有初始状态、成功状态、失败状态。行为树中还有一个特别的执行结果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

/// <summary>
/// 行为树的执行结果
/// </summary>
enum BTResult
{
NONE,
SUCCESSFUL, // 成功
FAIL, // 失败
RUNNING, // 进行中
}

/// <summary>
/// 树节点基类
/// </summary>
abstract class BTNode
{
/// <summary>
/// 执行节点行为
/// </summary>
/// <returns></returns>
public virtual BTResult doAction()
{
return BTResult.NONE;
}
}

组合节点

将选择、顺序、并行等多个节点组合在一起的一个跟节点。既然是一棵树,就肯定需要一个决策者,如果要在一个十字路口做一个选择,这个组合节点就是用来决定后面要走哪条路,这时需要选择节点,对于选择节点,一真则真,全假则假;如果要做顺序行为,先执行行为a,再执行行为b,等等,这时就需要顺序节点,对于顺序节点,一假则假,全真则真;如果需要并行执行行为,需要同时执行行为a和行为b等更多行为,就需要并行节点,对于并行节点,同时执行全部。组合节点比较简单,它仅仅是一些节点的容器,只需要用列表保存即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/// <summary>
/// 组合节点基类
/// </summary>
abstract class CompositeNode : BTNode
{
protected List<BTNode> children;

public CompositeNode()
{
this.children = new List<BTNode>();
}

public void addChild(BTNode node)
{
this.children.Add(node);
}
}

选择节点

当存在多条分支的时候选择节点(Select Node)多用于选择某条分支,在同一时刻必须选择一个行为,也就是说,选择节点是专门用来进行互斥选择的,选择节点的特效就决定了它的返回结果的形式。选择节点的子节点只能执行一个,只要有一个返回成功了,后面的就不需要执行了,执行向父亲节点返回成功的结果就行。一句话概括就是一真则真,全假则假。

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

/// <summary>
/// 选择节点
/// </summary>
class SelectNode : CompositeNode
{
private int index;

public SelectNode()
{
this.reset();
}

public override BTResult doAction()
{
if (this.children == null || this.children.Count < 1)
{
return BTResult.FAIL;
}

if (this.index >= this.children.Count)
{
this.reset();
}

BTResult result = BTResult.NONE;
for (int length = this.children.Count; this.index < length; this.index++)
{
result = this.children[this.index].doAction();
if (result == BTResult.SUCCESSFUL)
{
this.reset();
return result;
}

if (result == BTResult.RUNNING)
{
return result;
}
}

this.reset();
return BTResult.FAIL;
}

private void reset()
{
this.index = 0;
}
}

顺序节点

按照顺序执行后面的分支,当分支返回false时,就不在执行,只要有一个分支执行失败,这个顺序节点的执行结果就是失败,只有当所有的节点都执行成功了,这个分支才是执行成功的。一句话概括就是一假则假,全真则真。

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

/// <summary>
/// 顺序节点
/// </summary>
class SequenceNode : CompositeNode
{
private int index;

public SequenceNode()
{
this.reset();
}

public override BTResult doAction()
{
if (this.children == null || this.children.Count < 1)
{
return BTResult.FAIL;
}

if (this.index >= this.children.Count)
{
this.reset();
}


BTResult result = BTResult.NONE;
for (int length = this.children.Count; this.index < length; this.index++)
{
result = this.children[this.index].doAction();
if (result == BTResult.FAIL)
{
this.reset();
return result;
}

if (result == BTResult.RUNNING)
{
return result;
}
}

this.reset();
return BTResult.SUCCESSFUL;
}

private void reset()
{
this.index = 0;
}
}

并行节点

同时执行多个分支。并行节点大致分为并行选择节点和并行顺序节点。并行选择节点的执行结果是一假全假,全真则真;并行顺序节点的执行结果是一真全真,全假则假。

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
130
131
132
133
134
135
136
137
138
139
140
141
142

/// <summary>
/// 并行节点基类
/// </summary>
abstract class ParallelNode : CompositeNode
{

}

/// <summary>
/// 并行选择节点
/// </summary>
class ParallelSelectNode : ParallelNode
{
private List<BTNode> waitNodes;
private bool isFail;

public ParallelSelectNode()
{
this.waitNodes = new List<BTNode>();
this.isFail = false;
}

public override BTResult doAction()
{
if (this.children == null || this.children.Count < 1)
{
return BTResult.FAIL;
}

BTResult result = BTResult.NONE;
List<BTNode> _waitNodes = new List<BTNode>();
List<BTNode> _mainNodes = new List<BTNode>();
_mainNodes = this.waitNodes.Count > 0 ? this.waitNodes : this.children;

for (int i = 0, length = _mainNodes.Count; i < length; i++)
{
result = _mainNodes[i].doAction();
switch (result)
{
case BTResult.SUCCESSFUL:
break;
case BTResult.RUNNING:
_waitNodes.Add(_mainNodes[i]);
break;
default:
this.isFail = true;
break;
}
}

// 存在等待节点就返回等待节点
if (_waitNodes.Count > 0)
{
this.waitNodes = _waitNodes;
return BTResult.RUNNING;
}

result = this.checkResult();
this.reset();

return result;
}

private BTResult checkResult()
{
return this.isFail ? BTResult.FAIL : BTResult.SUCCESSFUL;
}

private void reset()
{
this.waitNodes.Clear();
this.isFail = false;
}
}

/// <summary>
/// 并行顺序节点
/// </summary>
class ParallelSequenceNode : ParallelNode
{
private List<BTNode> waitNodes;
private bool isSuccess;

public ParallelSequenceNode()
{
this.waitNodes = new List<BTNode>();
this.isSuccess = false;
}

public override BTResult doAction()
{
if (this.children == null || this.children.Count < 1)
{
return BTResult.FAIL;
}

BTResult result = BTResult.NONE;
List<BTNode> _waitNodes = new List<BTNode>();
List<BTNode> _mainNodes = new List<BTNode>();
_mainNodes = this.waitNodes.Count > 0 ? this.waitNodes : this.children;


for (int i = 0, length = _mainNodes.Count; i < length; i++)
{
result = _mainNodes[i].doAction();
switch (result)
{
case BTResult.SUCCESSFUL:
this.isSuccess = true;
break;
case BTResult.RUNNING:
_waitNodes.Add(_mainNodes[i]);
break;
default:
break;
}
}

// 存在等待节点就返回等待节点
if (_waitNodes.Count > 0)
{
this.waitNodes = _waitNodes;
return BTResult.RUNNING;
}

result = this.checkResult();
this.reset();
return result;
}

private BTResult checkResult()
{
return this.isSuccess ? BTResult.SUCCESSFUL : BTResult.FAIL;
}

private void reset()
{
this.waitNodes.Clear();
this.isSuccess = false;
}
}

装饰节点

一般用来修饰判断,这个修饰可以是“直到….成功”或者“直到…失败”。装饰节点可以可以用做定时器或者持续标识。装饰节点没有具体的返回True和False的结果,而是空出留给后面继承装饰节点的具体实例去做。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

/// <summary>
/// 装饰节点基础类
/// </summary>
abstract class DecoratorNode : BTNode
{
private BTNode child;

public DecoratorNode()
{
child = null;
}

protected void setChild(BTNode node)
{
this.child = node;
}
}

条件节点

表示在满足某个条件时就可以继续执行,它只需要一个条件判断。

1
2
3
4
5
6
7
8
9
10
/// <summary>
/// 条件节点基础类
/// </summary>
abstract class ConditionNode : BTNode
{
public override BTResult doAction()
{
return BTResult.FAIL;
}
}

行为节点

是最底层的节点,也就是树的叶子,它是行为树最终的执行者。

1
2
3
4
5
6
7
/// <summary>
/// 行为节点基础类
/// </summary>
abstract class ActionNode : BTNode
{

}

插件-Behavior Designer

在做Unity开发的时候,我们正常不会自己写一个行为树,而是使用第三方插件来实现,因为针对于行为树,如果没有可视化的编辑窗口,逻辑比较复杂的时候,我们需要在代码里面设置那么多的节点逻辑,会变得比状态机还要低阅读性。Behavior Designer插件提供了非常好的可视化界面,我们只需要针对基础插件的各种节点基类扩展出自己需要的实现类,就可以在窗口拖拉出我们需要的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

/// <summary>
/// 行为节点扩展
/// </summary>
public class Move : Action
{
public override TaskStatus OnUpdate()
{
return TaskStatus.Failure;
}
}

/// <summary>
/// 条件节点扩展
/// </summary>
public class CanMove : Conditional
{
public override TaskStatus OnUpdate()
{
return TaskStatus.Failure;
}
}