4. 决策树与行为树(BT)
好,咱们今天聊聊行为树。说实话,我刚开始做自动驾驶决策时,用的还是传统的有限状态机(FSM)。那时候项目赶得紧,状态一多,代码就乱成一锅粥。后来接触到行为树,才感觉打开了新世界的大门。
行为树这东西,说白了就是一棵树。树的每个节点代表一个行为或一个判断。机器人从根节点开始,按照树的逻辑往下走,最终执行某个动作。听起来简单,但用起来是真香。
4.1 行为树节点类型
行为树的节点类型不多,但每个都有它的脾气。我一个个说。
4.1.1 Sequence(序列节点)
Sequence 节点,顾名思义,就是按顺序执行子节点。它从左到右依次执行,如果某个子节点返回 Success,就继续下一个;如果返回 Failure,整个 Sequence 就失败,不再继续。
我在项目中遇到过这样一个场景:车辆要完成一个“靠边停车”的动作。这个动作需要先打右转向灯,然后减速,再向右打方向盘,最后回正。这四个步骤必须按顺序来,缺一不可。用 Sequence 节点就特别合适。
关键点:Sequence 节点要求所有子节点都成功,才算成功。任何一个失败,整个序列就中断。
4.1.2 Selector(选择节点)
Selector 节点,也叫 Fallback 节点。它也是从左到右执行子节点,但逻辑正好相反:只要有一个子节点返回 Success,整个 Selector 就成功,不再继续执行后面的节点。
你想想看,这像不像一个“备选方案”?比如车辆要避障,它先尝试向左变道。如果左边有车,那就尝试向右变道。如果右边也有车,那就减速。Selector 节点就是干这个的——从多个方案中选一个能用的。
我的经验:我曾经在写避障逻辑时,把 Selector 和 Sequence 搞混了,结果车辆总是同时执行多个动作,差点撞上护栏。后来我养成了一个习惯:在写代码前,先在纸上画出行为树的结构,标清楚每个节点的类型。
4.1.3 Decorator(装饰节点)
Decorator 节点,说白了就是给子节点加一层“包装”。它可以改变子节点的行为,比如限制执行次数、设置超时时间、反转结果等。
举个例子,车辆在等待红绿灯时,不能无限等下去。如果红灯超过 30 秒,就应该考虑绕行。这时候就可以用 Decorator 节点,给“等待”这个行为加一个超时限制。
常用的 Decorator 有:
- Inverter:反转子节点的结果(Success 变 Failure,Failure 变 Success)
- Repeater:重复执行子节点 N 次
- Timeout:给子节点设置超时时间
- Force Success:不管子节点返回什么,都返回 Success
注意:Decorator 节点虽然好用,但别滥用。我见过有人把整个行为树都包在 Decorator 里,结果调试时根本看不出问题出在哪。Decorator 应该用在刀刃上,而不是当万能胶。
4.1.4 Condition(条件节点)
Condition 节点,就是做判断的。它不执行任何动作,只检查某个条件是否成立。比如“前方是否有障碍物”、“当前速度是否超过限速”、“电池电量是否充足”等。
Condition 节点通常放在 Sequence 或 Selector 的最前面,用来决定是否继续执行后续节点。嗯,这里要注意:Condition 节点不应该有副作用,它只是“检查”,不应该“改变”任何状态。
4.2 行为树与 FSM 的对比
说到 FSM(有限状态机),很多老工程师对它感情复杂。FSM 简单直观,但状态一多,就变成了“面条式代码”。我当年维护过一个有 50 多个状态的 FSM,每次改一个状态,都要担心会不会影响其他状态。
行为树和 FSM 的区别,我用一个表格来说明:
| 对比维度 | FSM(有限状态机) | BT(行为树) |
|---|---|---|
| 状态管理 | 状态之间需要显式定义转移 | 节点之间通过树结构隐式管理 |
| 可扩展性 | 状态增多时,转移关系呈指数级增长 | 新增节点不影响已有节点 |
| 可复用性 | 状态逻辑通常耦合在一起 | 节点可以独立复用 |
| 调试难度 | 需要追踪状态转移路径 | 可以通过树结构直观定位问题 |
| 适用场景 | 状态少、逻辑简单的系统 | 状态多、逻辑复杂的系统 |
我个人习惯是:如果系统状态不超过 5 个,用 FSM 就够了。但如果状态超过 10 个,或者需要频繁增删状态,我会毫不犹豫地选择行为树。
4.3 使用 PyTrees 库构建一个避障行为树
PyTrees 是一个轻量级的 Python 行为树库。它简单易用,特别适合做原型验证。下面我带你手写一个避障行为树。
先安装 PyTrees:
pip install py_trees
然后,我们定义几个行为节点:
import py_trees
class CheckObstacle(py_trees.behaviour.Behaviour):
"""检查前方是否有障碍物"""
def __init__(self, name="CheckObstacle"):
super(CheckObstacle, self).__init__(name)
self.has_obstacle = False
def update(self):
# 这里模拟传感器检测
# 实际项目中会调用激光雷达或视觉模块
if self.has_obstacle:
return py_trees.common.Status.FAILURE
else:
return py_trees.common.Status.SUCCESS
class MoveForward(py_trees.behaviour.Behaviour):
"""直行"""
def update(self):
print("正在直行...")
return py_trees.common.Status.SUCCESS
class TurnLeft(py_trees.behaviour.Behaviour):
"""左转避障"""
def update(self):
print("左转避障...")
return py_trees.common.Status.SUCCESS
class TurnRight(py_trees.behaviour.Behaviour):
"""右转避障"""
def update(self):
print("右转避障...")
return py_trees.common.Status.SUCCESS
接下来,构建行为树:
def create_obstacle_avoidance_tree():
# 根节点:Selector,优先选择左转,其次右转,最后直行
root = py_trees.composites.Selector("避障策略")
# 左转分支:先检查障碍物,再左转
left_turn = py_trees.composites.Sequence("左转避障")
check_left = CheckObstacle("检查左侧")
turn_left = TurnLeft("左转")
left_turn.add_children([check_left, turn_left])
# 右转分支:先检查障碍物,再右转
right_turn = py_trees.composites.Sequence("右转避障")
check_right = CheckObstacle("检查右侧")
turn_right = TurnRight("右转")
right_turn.add_children([check_right, turn_right])
# 直行分支:直接直行
move_forward = MoveForward("直行")
# 组装树
root.add_children([left_turn, right_turn, move_forward])
return root
最后,运行行为树:
if __name__ == "__main__":
tree = create_obstacle_avoidance_tree()
for i in range(10):
tree.tick_once()
print(f"第 {i+1} 次执行完成")
# 模拟障碍物变化
if i == 3:
tree.children[0].children[0].has_obstacle = True
避坑指南:我曾经在写行为树时,忘记给 Condition 节点设置初始状态,结果第一次执行时,Condition 节点返回了 Undefined,导致整个树卡住了。后来我养成了习惯:所有 Condition 节点在初始化时,都要显式设置一个默认状态。
这个例子虽然简单,但已经涵盖了行为树的核心思想:用树结构组织行为,用节点类型控制执行逻辑。实际项目中,你可能会需要更复杂的节点,比如并行节点、随机节点等,但万变不离其宗。
好了,行为树的内容就讲到这里。下一章我们会聊聊更高级的决策方法——蒙特卡洛树搜索(MCTS)。那个东西更有意思,咱们到时候见。