行为树的缺点
- 一个简单的操作都需要定义叶子节点,继承类并实现派生方法
Init,OnTick,provide port,在工厂类中注册,导致在工程初期的代码量快速增大 - 当行为树规模变大,XML文件逻辑分散,很难追踪执行路径,复用性并没有想象中高,
- 最大缺陷:黑板变量,有的人在XML文件里赋值,有的人在C++里赋值,我如果用groot2看行为树,无法看到C++里赋值的情况
- 黑板机制的副作用:太灵活,没有强类型约束,没有作用域控制
- switch默认最多6个case,不能直接修改xml文件里的Switch,C++不识别。要想实现更多case,只能自己实现新的 Control 节点,或者说行为树不倾向这种用法
- C++的逻辑风格是大多数人熟悉的,比如if else, while, 设计模式等等,一个人看另一个人写的代码并不难上手。但是行为树的逻辑类型很多,有的人还会自己定义控制节点和修饰节点。同样的功能,不同人实现的逻辑会很不同,互相难理解,不利于团队协作,其他人难接手以前代码。也就是说行为树编程太灵活,缺乏规范
行为树的优点
- 有些逻辑,如果在C++里实现或者说用状态机,会非常难理解,代码量膨胀,比如修饰节点
也就是相对状态机的优点,比如这样的逻辑
1
2
3
4去目标点
├── 直走
├── 绕障
└── 重新规划路径如果用状态机的话,需要考虑状态转移、错误处理、retry逻辑,处理起来比较复杂或者说不符合人的思维,如果用行为树,那就是普通的Fallback顺序节点,每个情况作为一个Fallback的分支,很容易理解
某些情况下可以实现节点复用
- 官方提供了与ROS2的接口,适合机器人的业务开发,而且是两套接口。
补充
- tick的运行方式,不符合C++程序员的思维。tick一般不会因为频率而造成CPU开销,
sequence with memory等节点可以跳过已运行完的节点。除非 tick 频率设为 100hz 甚至更高,树又非常深,才会成为性能瓶颈。 - AsyncNode,没帮你解决线程问题,cancel机制不统一,生命周期容易错
状态机的缺点
- 在项目中可能增加新状态、减少状态或者改变状态之间的迁移关系,如果状态越来越多,一点小修改都会产生很大的工作量,代码中会出现大量的判断跳转,耦合性太强。代码的逻辑会变得臃肿
状态机是由事件驱动的,状态与执行内容是绑定在一起的。当执行内容需要在多个状态中执行时,各个状态下都需要放置执行内容的逻辑。当业务逻辑代码分散在各处时就不太好维护了
行为树适合做任务调度层,控制复杂流程(回充 / 避障 / 任务切换),用它做所有的决策系统,很不合适。应当是行为树和状态机混合使用。
对于割草机的 直走—->绕障 —-> 重新规划路径,用行为树规划,其中的绕障内部用状态机,
1 | 去目标点 |
在AvoidObstacleAction内部是 FSM:1
2
3
4
5
6
7INIT
↓
AVOID
↓
CHECK
↓
SUCCESS / FAIL / RECOVERAvoidObstacleAction这个叶子节点可以复用,比如有走直线的避障、沿边的避障等等。
行为树不适合表达连续性的流程(如避障),因为它缺乏状态锁定机制,会导致执行不稳定和结构复杂化。
在上面这个例子里,如果避障部分也用行为树,会导致:
- 结构膨胀
- 在子树之间频繁切换
- 调试困难
- 子树不可复用
同样道理,脱困机制也该用状态机,而不是行为树。
总结
选策略用行为树,连续过程(执行细节)用状态机
如果一个任务满足有明确步骤(1. 2. 3.)、中途不能被打断、需要记住进度,使用 FSM
如果是存在多种行为选择、需要 fallback 或 retry、可以随时切换状态,使用 BT
1 | BehaviorTree(高层调度) |
频率控制1
2
3
4
5BT tick(10Hz)
↓
FSM update(10~20Hz)
↓
Controller(50~100Hz)
