第三章:行人导航系统
行人导航,说白了就是让虚拟行人知道「我要去哪儿」以及「怎么走过去」。
我刚开始做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 行人群体流动
单个行人好办,但一群行人怎么走?这就涉及到群体流动了。
你想想看,早高峰的地铁站,几百号人怎么做到不撞在一起的?其实每个人都在做局部决策——看前面的人往哪走,自己跟着调整。
群体流动的核心机制
- 局部导航:每个行人独立运行A* + RVO,只感知周围几米内的障碍物。
- 密度控制:当某个区域行人密度过高,自动降低进入该区域的概率。
- 跟随行为:如果前面的人走得顺畅,就跟着走,减少重新规划的计算量。
- 紧急疏散:模拟恐慌状态,行人会忽略部分避障规则,直奔出口。
我在项目中做过一个测试:让100个行人在十字路口随机穿行。一开始用的是纯A*,结果行人全挤在路口,谁也动不了。后来加了RVO和密度控制,流动就顺畅多了。
性能优化建议:群体流动的计算量跟行人数量成正比。100个行人以内,用CPU实时计算没问题。超过500个,建议用GPU加速,或者预计算部分路径。
群体流动的典型场景
| 场景 | 行为特征 | 实现要点 |
|---|---|---|
| 人行横道过街 | 等待绿灯,集体通过 | 信号灯状态触发群体决策 |
| 商场入口 | 进出人流交叉 | 分层导航,避免对冲 |
| 突发事件 | 恐慌,向出口涌 | 降低避障权重,提高速度 |
嗯,这里要注意一点:群体流动不是简单的「人多力量大」。如果所有行人都用同样的算法、同样的参数,反而会出现「共振」——大家一起左转,一起右转,效率更低。我的做法是给每个行人加一点随机偏移量,让行为更自然。
好了,这一章的内容就这些。导航系统是行人行为建模的骨架,骨架搭好了,后面的行为逻辑才能跑得起来。下一章我们会聊行人的感知系统——他们是怎么「看」到周围世界的。