行为树概述

行为树的缺点

  1. 一个简单的操作都需要定义叶子节点,继承类并实现派生方法Init, OnTickprovide port,在工厂类中注册,导致在工程初期的代码量快速增大
  2. 当行为树规模变大,XML文件逻辑分散,很难追踪执行路径,复用性并没有想象中高,
  3. 最大缺陷:黑板变量,有的人在XML文件里赋值,有的人在C++里赋值,我如果用groot2看行为树,无法看到C++里赋值的情况
  4. 黑板机制的副作用:太灵活,没有强类型约束,没有作用域控制
  5. switch默认最多6个case,不能直接修改xml文件里的Switch,C++不识别。要想实现更多case,只能自己实现新的 Control 节点,或者说行为树不倾向这种用法
  6. C++的逻辑风格是大多数人熟悉的,比如if else, while, 设计模式等等,一个人看另一个人写的代码并不难上手。但是行为树的逻辑类型很多,有的人还会自己定义控制节点和修饰节点。同样的功能,不同人实现的逻辑会很不同,互相难理解,不利于团队协作,其他人难接手以前代码。也就是说行为树编程太灵活,缺乏规范

行为树的优点

  1. 有些逻辑,如果在C++里实现或者说用状态机,会非常难理解,代码量膨胀,比如修饰节点
  2. 也就是相对状态机的优点,比如这样的逻辑

    1
    2
    3
    4
    去目标点
    ├── 直走
    ├── 绕障
    └── 重新规划路径

    如果用状态机的话,需要考虑状态转移、错误处理、retry逻辑,处理起来比较复杂或者说不符合人的思维,如果用行为树,那就是普通的Fallback顺序节点,每个情况作为一个Fallback的分支,很容易理解

  3. 某些情况下可以实现节点复用

  4. 官方提供了与ROS2的接口,适合机器人的业务开发,而且是两套接口。

补充

  1. tick的运行方式,不符合C++程序员的思维。tick一般不会因为频率而造成CPU开销,sequence with memory 等节点可以跳过已运行完的节点。除非 tick 频率设为 100hz 甚至更高,树又非常深,才会成为性能瓶颈。
  2. AsyncNode,没帮你解决线程问题,cancel机制不统一,生命周期容易错

状态机的缺点

  1. 在项目中可能增加新状态、减少状态或者改变状态之间的迁移关系,如果状态越来越多,一点小修改都会产生很大的工作量,代码中会出现大量的判断跳转,耦合性太强。代码的逻辑会变得臃肿

状态机是由事件驱动的,状态与执行内容是绑定在一起的。当执行内容需要在多个状态中执行时,各个状态下都需要放置执行内容的逻辑。当业务逻辑代码分散在各处时就不太好维护了


行为树适合做任务调度层,控制复杂流程(回充 / 避障 / 任务切换),用它做所有的决策系统,很不合适。应当是行为树和状态机混合使用。

对于割草机的 直走—->绕障 —-> 重新规划路径,用行为树规划,其中的绕障内部用状态机,

1
2
3
4
去目标点
├── MoveStraightAction
├── AvoidObstacleAction <--- 内部是FSM
└── ReplanAction

AvoidObstacleAction内部是 FSM:

1
2
3
4
5
6
7
INIT

AVOID

CHECK

SUCCESS / FAIL / RECOVER

AvoidObstacleAction这个叶子节点可以复用,比如有走直线的避障、沿边的避障等等。

行为树不适合表达连续性的流程(如避障),因为它缺乏状态锁定机制,会导致执行不稳定和结构复杂化。

在上面这个例子里,如果避障部分也用行为树,会导致:

  1. 结构膨胀
  2. 在子树之间频繁切换
  3. 调试困难
  4. 子树不可复用

同样道理,脱困机制也该用状态机,而不是行为树。

总结

选策略用行为树,连续过程(执行细节)用状态机

  • 如果一个任务满足有明确步骤(1. 2. 3.)、中途不能被打断、需要记住进度,使用 FSM

  • 如果是存在多种行为选择、需要 fallback 或 retry、可以随时切换状态,使用 BT

1
2
3
4
5
BehaviorTree(高层调度)

FSM / Planner(决策)

Controller(控制)

频率控制

1
2
3
4
5
BT tick(10Hz)

FSM update(10~20Hz)

Controller(50~100Hz)