第三章:行人导航系统

行人导航,说白了就是让虚拟行人知道「我要去哪儿」以及「怎么走过去」。

我刚开始做Carla仿真时,觉得行人走路嘛,随便给个目标点不就行了?结果第一次跑测试,行人直接穿墙而过,还跟车辆来了个「亲密接触」……嗯,从那以后我才明白,导航系统才是行人行为建模的基石。

3.1 行人导航网格(NavMesh)

导航网格,就是行人的「地图」。它不是我们肉眼看到的路面,而是一张抽象的可行走区域图。

在Carla里,行人只能在NavMesh覆盖的区域活动。你想想看,如果NavMesh没覆盖到人行道,那行人打死也不会走上去——哪怕你给他设了目标点。

核心概念:NavMesh将连续的地形离散化成一个个三角形网格。每个三角形代表一块可通行的区域,行人只能在三角形之间移动。

我在项目中遇到过一个问题:地图更新后,行人突然开始「原地踏步」。排查了半天,发现是NavMesh没重新生成。所以我的习惯是——每次修改地图,第一件事就是检查NavMesh。

NavMesh的生成方式

  • 自动生成:Carla自带的Recast工具,一键生成。适合快速验证。
  • 手动编辑:用Blender或Unity编辑器微调。适合复杂场景。
  • 动态更新:运行时修改NavMesh。比如施工区域临时封闭,需要实时更新可通行区域。

我的小技巧:生成NavMesh时,把「行人半径」设成0.3米。太小了容易卡住,太大了走不过窄路。0.3米是我试出来的黄金值。

3.2 路径规划算法

有了地图,接下来就是规划路径。路径规划说白了就是回答一个问题:「从A点到B点,怎么走最划算?」

这里的「划算」可以是距离最短、时间最少、或者最安全。不同的场景,侧重点不一样。

常见的路径规划算法

算法 特点 适用场景
Dijkstra 保证最短路径,但慢 静态地图,小范围
A* 启发式搜索,快 大部分场景,首选
RRT 随机采样,适合高维 复杂障碍物环境
D* Lite 动态环境,增量更新 地图频繁变化

我个人习惯用A*。为什么?因为它够快,而且实现起来不复杂。你想想看,行人导航需要实时计算,如果每帧都跑Dijkstra,帧率直接崩了。

3.3 A*寻路算法

A*的核心公式很简单:F = G + H

  • G:从起点到当前点的实际代价
  • H:从当前点到终点的预估代价(启发式函数)
  • F:总代价,A*每次选F最小的节点扩展

说白了,A*就是「摸着石头过河」——每一步都选看起来最有可能到终点的方向走。

关键点:启发式函数H的选择直接影响A*的效率。用曼哈顿距离还是欧几里得距离?我建议用欧几里得距离,因为行人可以任意方向移动,不是只能走横平竖直的格子。

我曾经踩过一个坑:H值设得太大,导致A*变成了「贪心算法」,路径经常撞墙。后来把H的权重调成0.8,效果就好多了。

// A*寻路核心代码片段(Carla Python API)
def a_star_search(nav_mesh, start, goal):
    open_set = {start}
    came_from = {}
    g_score = {start: 0}
    f_score = {start: heuristic(start, goal)}
    
    while open_set:
        current = min(open_set, key=lambda p: f_score[p])
        if current == goal:
            return reconstruct_path(came_from, current)
        
        open_set.remove(current)
        for neighbor in nav_mesh.get_neighbors(current):
            tentative_g = g_score[current] + distance(current, neighbor)
            if tentative_g < g_score.get(neighbor, float('inf')):
                came_from[neighbor] = current
                g_score[neighbor] = tentative_g
                f_score[neighbor] = tentative_g + heuristic(neighbor, goal)
                open_set.add(neighbor)
    return None  # 无路可走

注意:NavMesh的邻居关系不是简单的上下左右。每个三角形可能有2-3个邻居,A*需要遍历这些邻居节点,而不是像素点。

3.4 动态避障

路径规划好了,但路上突然出现一辆车、一个垃圾桶、或者另一个行人怎么办?这就是动态避障要解决的问题。

我刚开始做动态避障时,想得太简单了——让行人看到障碍物就停下来。结果呢?行人集体「堵车」,一个都走不了。

常用的避障策略

  • 速度障碍法(VO):计算相对速度,判断是否会发生碰撞。如果会,就调整速度方向。
  • 互惠速度障碍法(RVO):VO的升级版,双方各让一步,避免「你往左我也往左」的尴尬。
  • 社会力模型:把行人之间的相互作用力模拟成物理力。推推搡搡,自然就分开了。

我个人推荐RVO。为什么?因为VO容易导致震荡——两个行人面对面,你往左我往左,永远躲不开。RVO让双方各承担一半的避让责任,效果就好多了。

避坑指南:我曾经把RVO的「反应时间」设成0.5秒,结果行人总是快撞上了才躲。后来改成0.2秒,配合提前预测,效果才正常。

3.5 行人群体流动

单个行人好办,但一群行人怎么走?这就涉及到群体流动了。

你想想看,早高峰的地铁站,几百号人怎么做到不撞在一起的?其实每个人都在做局部决策——看前面的人往哪走,自己跟着调整。

群体流动的核心机制

  1. 局部导航:每个行人独立运行A* + RVO,只感知周围几米内的障碍物。
  2. 密度控制:当某个区域行人密度过高,自动降低进入该区域的概率。
  3. 跟随行为:如果前面的人走得顺畅,就跟着走,减少重新规划的计算量。
  4. 紧急疏散:模拟恐慌状态,行人会忽略部分避障规则,直奔出口。

我在项目中做过一个测试:让100个行人在十字路口随机穿行。一开始用的是纯A*,结果行人全挤在路口,谁也动不了。后来加了RVO和密度控制,流动就顺畅多了。

性能优化建议:群体流动的计算量跟行人数量成正比。100个行人以内,用CPU实时计算没问题。超过500个,建议用GPU加速,或者预计算部分路径。

群体流动的典型场景

场景 行为特征 实现要点
人行横道过街 等待绿灯,集体通过 信号灯状态触发群体决策
商场入口 进出人流交叉 分层导航,避免对冲
突发事件 恐慌,向出口涌 降低避障权重,提高速度

嗯,这里要注意一点:群体流动不是简单的「人多力量大」。如果所有行人都用同样的算法、同样的参数,反而会出现「共振」——大家一起左转,一起右转,效率更低。我的做法是给每个行人加一点随机偏移量,让行为更自然。

好了,这一章的内容就这些。导航系统是行人行为建模的骨架,骨架搭好了,后面的行为逻辑才能跑得起来。下一章我们会聊行人的感知系统——他们是怎么「看」到周围世界的。