传统逻辑判断
在状态机的理念没有提出来的时候,传统的判断逻辑都是如果A条件满足,就执行a行为。如果A条件不满足但是B条件满足,就执行b行为。随着条件和行为的增多,这种if else
判断会非常长,可读性非常低。
1 2 3 4 5 6 7 8
|
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 { public abstract bool isValid();
public abstract FSMState getNextState();
public abstract void onTransition(); }
abstract class FSMState { public virtual void onEnter(){}
public virtual void onUpdate() { }
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
|
enum BTResult { NONE, SUCCESSFUL, FAIL, RUNNING, }
abstract class BTNode { 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
|
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
|
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
|
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
|
abstract class ParallelNode : CompositeNode { }
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; } }
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
|
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
|
abstract class ConditionNode : BTNode { public override BTResult doAction() { return BTResult.FAIL; } }
|
行为节点
是最底层的节点,也就是树的叶子,它是行为树最终的执行者。
1 2 3 4 5 6 7
|
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
|
public class Move : Action { public override TaskStatus OnUpdate() { return TaskStatus.Failure; } }
public class CanMove : Conditional { public override TaskStatus OnUpdate() { return TaskStatus.Failure; } }
|