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;
    }
};

你想想看,如果动作节点只返回SuccessFailure,那过马路这种持续动作就没法处理了。这就是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()              // 实在不行就乱穿马路
}

行人会优先选择最安全的方式,如果不可行,就退而求其次。

我的建议:刚开始用行为树时,先用SequenceSelector就够了。并行节点容易引入竞态条件,等熟练了再碰它。

4.7 实战:搭建一个简单的行人行为树

好了,理论讲完了。我们动手搭一个最简单的行人行为树。

场景:行人在路边,决定是否过马路。

// 行人过马路行为树
Selector {
    // 方案一:走人行横道
    Sequence {
        IsCrosswalkNearby()          // 条件:附近有人行横道吗?
        IsTrafficLightGreen()        // 条件:绿灯亮了吗?
        WalkToCrosswalk()            // 动作:走到人行横道
        WaitForGreenLight()          // 动作:等待绿灯
        CrossStreet()                // 动作:过马路
    }
    
    // 方案二:直接过马路(注意安全)
    Sequence {
        IsRoadClear()                // 条件:路上没车吗?
        LookBothWays()               // 动作:左右观察
        CrossStreet()                // 动作:快速通过
    }
    
    // 方案三:等一会儿再试
    Sequence {
        WaitForSeconds(5)            // 动作:等5秒
        CrossStreet()                // 动作:强行通过(风险高)
    }
}

这个行为树表达了三种策略:优先走人行横道,其次安全直穿,最后无奈等待。你看,逻辑非常清晰,而且容易扩展。

核心要点:行为树的魅力在于,你可以把复杂的决策逻辑拆解成一个个小节点。每个节点只做一件事,组合起来就能完成非常复杂的行为。

4.8 本章小结

今天我们搞清楚了行为树的四大节点类型:

  • 条件节点:问问题,不做事
  • 动作节点:干实事,能持续
  • 装饰节点:包装器,改行为
  • 复合节点:指挥官,控流程

下一章,我们会把这些节点真正用到 Carla 里,让行人在仿真环境中跑起来。到时候你会看到,行为树配合 Carla 的行人 API,能做出非常逼真的行人行为。

嗯,今天就到这里。有问题随时问我。