4. 行人行为树:行为树概念、行为节点类型
各位同学,欢迎来到第四章。
前面几章我们搞定了行人的基础感知和运动控制。但说实话,要让行人在仿真里真正「活」起来,光有这些还不够。你想想看,一个行人走到路口,是停下来等车,还是直接冲过去?这背后需要一个决策机制。
我个人习惯用行为树来做这件事。它比有限状态机更灵活,比决策树更直观。今天我们就来彻底搞懂它。
4.1 行为树到底是什么?
行为树,说白了就是一个「决策流程图」。它用树状结构来描述一个智能体(比如行人)在不同情况下该做什么。
我记得第一次接触行为树是在做自动驾驶决策模块的时候。当时团队还在用有限状态机,状态多了以后,代码变得一团乱麻。后来换成行为树,整个逻辑清晰多了。
行为树有几个核心特点:
- 模块化:每个行为都是一个独立节点,可以复用
- 层次化:复杂行为可以拆解成简单行为的组合
- 可读性强:图形化展示,一眼就能看懂决策逻辑
- 易于调试:每个节点都有执行状态,方便定位问题
核心概念:行为树的执行是从根节点开始,按照深度优先策略遍历。每个节点执行后会返回三种状态之一:Success(成功)、Failure(失败)、Running(运行中)。
嗯,这里要注意:Running状态是行为树独有的。它表示这个动作还没做完,下一帧继续执行。这个设计让行为树特别适合处理持续性的动作,比如走路、等待。
4.2 行为节点的四种类型
行为树的节点类型,我把它分成四大类。每一类都有自己的职责。
| 节点类型 | 英文名称 | 核心作用 | 返回状态 |
|---|---|---|---|
| 复合节点 | Composite | 控制子节点的执行逻辑 | 取决于子节点 |
| 条件节点 | Condition | 检查某个条件是否成立 | Success / Failure |
| 动作节点 | Action | 执行具体的动作 | Success / Failure / Running |
| 装饰节点 | Decorator | 修饰子节点的行为 | 取决于修饰逻辑 |
4.3 条件节点:决策的「开关」
条件节点是行为树的「眼睛」。它只做一件事:检查条件是否满足。如果满足,返回Success;不满足,返回Failure。
它不会改变任何状态,也不会执行任何动作。纯粹是「问问题」。
我在项目中常用的条件节点:
- IsTrafficLightGreen:检查人行道绿灯是否亮起
- IsVehicleApproaching:检测是否有车辆靠近
- IsCrosswalkClear:检查人行横道是否安全
- HasReachedDestination:判断是否到达目标点
我的经验:条件节点尽量保持「纯函数」风格——同样的输入永远返回同样的输出。这样调试起来特别方便。我曾经因为条件节点里混了随机数,排查了整整一个下午。
4.4 动作节点:真正干活的「手」
动作节点是行为树的「手脚」。它负责执行具体的动作,比如走路、转身、等待。
动作节点可以返回三种状态:
- Success:动作执行完毕
- Failure:动作执行失败
- Running:动作还在执行中,下一帧继续
举个例子,行人过马路的动作:
// 伪代码示例:行人过马路动作
class CrossStreetAction : public ActionNode {
Status Tick() override {
if (pedestrian.is_at_crosswalk()) {
// 开始过马路
pedestrian.walk_to(destination);
return Status::Running;
}
if (pedestrian.has_crossed()) {
return Status::Success;
}
return Status::Running;
}
};
你想想看,如果动作节点只返回Success和Failure,那过马路这种持续动作就没法处理了。这就是Running状态的价值所在。
避坑指南:我曾经在动作节点里忘记返回Running状态,结果行人走到一半就「瞬移」到终点了。嗯,那画面挺搞笑的,但调试起来一点都不好笑。
4.5 装饰节点:行为的「滤镜」
装饰节点,我习惯叫它「包装器」。它只有一个子节点,用来修改子节点的行为。
常见的装饰节点:
- Inverter:反转子节点的结果(成功变失败,失败变成功)
- Repeater:重复执行子节点N次
- UntilSuccess:一直执行直到成功
- Timeout:给子节点设置超时时间
- ForceSuccess:不管子节点返回什么,都返回成功
举个例子,行人等待过马路时,可以用Timeout装饰器:
// 等待过马路,最多等30秒
Timeout(30.0) {
WaitAtCrosswalk()
}
如果30秒内还没等到绿灯,就放弃等待,走另一条路。这个逻辑用装饰节点实现,代码非常简洁。
个人心得:装饰节点是行为树的「瑞士军刀」。我经常用ForceSuccess来处理「可选的」行为——比如行人看到熟人打个招呼,打不成也没关系,不影响主线任务。
4.6 复合节点:决策的「大脑」
复合节点是行为树的「指挥官」。它控制子节点的执行顺序和逻辑。
最常用的三种复合节点:
| 节点类型 | 执行逻辑 | 类比 |
|---|---|---|
| Sequence(序列) | 按顺序执行子节点,全部成功才算成功 | 「与」逻辑 |
| Selector(选择器) | 依次尝试子节点,有一个成功就停止 | 「或」逻辑 |
| Parallel(并行) | 同时执行所有子节点 | 「多任务」 |
Sequence 的典型用法:
Sequence {
CheckTrafficLight() // 先检查红绿灯
LookBothWays() // 再左右看
CrossStreet() // 最后过马路
}
任何一个步骤失败,整个序列就失败。行人不会在红灯时过马路。
Selector 的典型用法:
Selector {
CrossAtCrosswalk() // 优先走人行横道
CrossAtIntersection() // 不行就走路口
Jaywalk() // 实在不行就乱穿马路
}
行人会优先选择最安全的方式,如果不可行,就退而求其次。
我的建议:刚开始用行为树时,先用Sequence和Selector就够了。并行节点容易引入竞态条件,等熟练了再碰它。
4.7 实战:搭建一个简单的行人行为树
好了,理论讲完了。我们动手搭一个最简单的行人行为树。
场景:行人在路边,决定是否过马路。
// 行人过马路行为树
Selector {
// 方案一:走人行横道
Sequence {
IsCrosswalkNearby() // 条件:附近有人行横道吗?
IsTrafficLightGreen() // 条件:绿灯亮了吗?
WalkToCrosswalk() // 动作:走到人行横道
WaitForGreenLight() // 动作:等待绿灯
CrossStreet() // 动作:过马路
}
// 方案二:直接过马路(注意安全)
Sequence {
IsRoadClear() // 条件:路上没车吗?
LookBothWays() // 动作:左右观察
CrossStreet() // 动作:快速通过
}
// 方案三:等一会儿再试
Sequence {
WaitForSeconds(5) // 动作:等5秒
CrossStreet() // 动作:强行通过(风险高)
}
}
这个行为树表达了三种策略:优先走人行横道,其次安全直穿,最后无奈等待。你看,逻辑非常清晰,而且容易扩展。
核心要点:行为树的魅力在于,你可以把复杂的决策逻辑拆解成一个个小节点。每个节点只做一件事,组合起来就能完成非常复杂的行为。
4.8 本章小结
今天我们搞清楚了行为树的四大节点类型:
- 条件节点:问问题,不做事
- 动作节点:干实事,能持续
- 装饰节点:包装器,改行为
- 复合节点:指挥官,控流程
下一章,我们会把这些节点真正用到 Carla 里,让行人在仿真环境中跑起来。到时候你会看到,行为树配合 Carla 的行人 API,能做出非常逼真的行人行为。
嗯,今天就到这里。有问题随时问我。