CARLA Sync Mode & Scenarios: Traffic, Weather & Closed Loop
| 模式 | Server 行为 | 时间步 | Client 等待 | 用途 |
|---|---|---|---|---|
| 异步 (Asynchronous) | 全速渲染,按渲染帧率自走 | delta_seconds 跟随渲染 | 否(随时读最新帧) | 快速数据采集、调试可视化 |
| 同步 (Synchronous) | 停在当前帧,等 tick() 才走 | fixed_delta_seconds 固定 | 是(每帧等 Server) | 闭环评测、可复现实验 |
异步模式下,Server 的推进由渲染帧率决定(受 GPU 负载、垂直同步、场景复杂度影响)。Client 调 get_world().snapshot 拿到的是”此刻最新的世界状态”,但这个状态对应哪个仿真时刻是模糊的——两次运行时 GPU 负载不同,相同时间点查到的世界状态可能完全不同。
更糟的是采样不均匀:如果 Client 推理慢,它会错过几帧;如果快,会重复采同一帧。对一个 20 Hz 训练的数据集来说,采样时刻漂移会导致轨迹标注错位。
同步模式把仿真和算法绑成一个严格串行的循环:
Client 发 tick() → Server 推进 fixed_delta → 传感器采一帧 → 返回 frame ID → Client 处理
tick() 之前完全冻结,物理不推进、传感器不采。tick() 是阻塞调用,等 Server 完成这一帧的物理和渲染后才返回。frame ID,可以精确对齐。这保证了:只要输入序列相同,输出完全可复现——这是闭环评测的前提(Bench2Drive 要求可复现性)。
很多人以为”异步 = 不等”,其实 CARLA 提供一个折中:异步模式 + world.wait_for_tick()。Client 调 wait_for_tick() 阻塞到 Server 走完下一帧,但时间步仍由渲染决定(不固定)。这只解决了”我等下一帧”的问题,没解决”时间步固定”的问题,所以不能用于可复现评测,只能用于调试。
切换模式本身是免费的,但要注意:
settings.synchronous_mode = False; world.apply_settings(settings)。否则 Server 还在等 tick,下次连接会卡死(详见 EP01 B08)。闭环评测要求每一帧的”传感器输入 → 模型输出 → 物理执行”是原子单元:
apply_control——异步模式下 control 可能跑到下一帧才生效,时序错乱。所以整个 SparseDriveV2 系列都默认 synchronous_mode=True。
settings = world.get_settings()
settings.synchronous_mode = True
settings.fixed_delta_seconds = 0.05 # 20 Hz
world.apply_settings(settings)
fixed_delta_seconds 是仿真时间步(单位秒),不是渲染帧间隔。同步模式下 Server 收到 tick() 后固定推进这个时间。
| 频率 | 典型用途 | |
|---|---|---|
| 0.05 s | 20 Hz | Bench2Drive / SparseDriveV2 标准 |
| 0.02 s | 50 Hz | 高频控制研究 |
| ~0.033 s | 30 Hz | 部分密集采集 |
SparseDriveV2 用 20 Hz 是因为模型推理节奏、传感器 sensor_tick、控制响应时间都按这个频率标定。
物理引擎(PhysX)用显式或半隐式积分推进刚体。步长 越大,积分误差越大,碰撞穿透、车辆弹跳、抖动都会出现。典型表现:
20 Hz 是物理稳定性和推理预算的平衡点。
fixed_delta_seconds 是仿真层的步长,物理引擎内部还会进一步细分成子步——这就是 Sub-Stepping。原因:
CARLA 通过 UE 的 MaxSubstepDeltaTime 和 MaxSubsteps 控制:
max_substep_delta_time = 0.01 # 单子步最大 0.01 s → 100 Hz
max_substeps = 10 # 一个 tick 最多 10 个子步
设 fixed_delta=0.05 时,物理引擎会把它拆成 5 个 0.01 s 子步。子步频率不低于 60 Hz 是经验下限——低于这个阈值,高速碰撞会有可见穿透。
fixed_delta_seconds 必须能被子步上限整除,否则物理会跳步:
如果 ,,正好 5 步。但如果 ,会算出 7 步、最后一步只用 0.004 s,导致最后一步物理略快——长期累积会让仿真时间和墙钟时间不同步(同步模式下两者本来应该一致)。
no_rendering_mode 的影响调试时可以开 settings.no_rendering_mode = True:跳过所有渲染,只跑物理。这能让 Server 每帧快 5–10 倍,适合大批量轨迹回放评测。代价是 RGB / Depth 相机返回空图像——只对不需要视觉的评测有用。
CARLA 里”NPC 怎么动”分两个层次,对应两套不同的工具:
| 层 | 工具 | 运行位置 | 目的 | 控制粒度 |
|---|---|---|---|---|
| 背景层 | Traffic Manager (TM) | Client 端(C++ 后端) | 撑起常态背景车流 | 全局统计行为(跟车/超车概率) |
| 考题层 | ScenarioRunner (SR) | Client 端 + py_trees 行为树 | 精确编排考试场景 | 单车单步精确控制 |
Bench2Drive 的 44 种交互场景由 SR 驱动,背景车流由 TM 填充。两者协作:TM 提供有车流的”路面”,SR 在其中插入精确的”考题”。
TM 是个运行在 Client 端 C++ 后端的批处理流水线。每个 tick,TM 对所有”挂给它”的车批量算下一步动作,分五个阶段:
apply_control 的油门/刹车/转向。注意 CARLA 官方文档把 Stage 2 和 5 之间的边界写法略有出入,但核心是”预测 → 规划 → 碰撞 → 路权 → 控制”五步。
关键点:碰撞检测用的是预测路径上的 bbox,不是当前 bbox。TM 看的是”如果继续这样走会不会撞”,这是前瞻性的避撞,不是被动反应。
ScenarioRunner 的核心是 py_trees(Python 行为树库)。行为树是一种分层的状态机:
DriveTo、ChangeLane、WaitUntil。例如 Cut-In 场景的行为树大致是:
Sequence
├── Selector
│ ├── WaitUntil(distance_to_ego < threshold) # NPC 追上 Ego
├── ChangeLane(target_lane=ego_lane) # 切入
├── MaintainSpeed(slow_factor=...) # 切完后稳速
行为树的好处是可组合:把”追上”和”切入”拆成原子行为,组合出 44 种不同场景。Bench2Drive 的 cut_in_right、object_crossing、junction_left_turn 都是这种组合。
TM 的设计目标是统计意义上的真实车流——它追求”看起来像真交通”,不保证”NPC 一定在某帧做某个动作”。但考试场景要求精确:cut_in_right 必须在 Ego 进入指定路段、达到指定速度时让 NPC 切入。这种确定性的事件序列必须靠行为树,不能交给 TM 的概率模型。
这也是 EP07 T1 故障的根源:Bench2Drive 的 cut_in_right 用 SR 编排,但底层切道动作依赖 CARLA 在该路段生成 LaneChange waypoint——弯道处这个拓扑不存在,行为树里 ChangeLane 节点永远 FAILURE,NPC 只能平行前进(详见 EP07)。
tm = client.get_trafficmanager(port) # port 默认 8000
tm.set_synchronous_mode(True) # 跟 Server 同步模式对齐
tm.set_hybrid_physics_mode(True) # 远处的车用简化物理
tm.set_hybrid_physics_radius(50.0) # 半径 50m 内用完整物理
for npc in npc_vehicles:
npc.set_autopilot(True) # 把车交给 TM
tm.port() # 确认接管
set_autopilot(True) 是把车的控制权交给 TM——TM 的 PID Control 阶段会替它发 apply_control。set_synchronous_mode(True) 让 TM 的流水线和 Server 的 tick() 同步,否则 TM 自己按另一个节奏跑会和 Server 错位。
TM 的参数分两个粒度:
tm.set_global_distance_to_leading_vehicle(5.0) # 跟车距离 5 米
tm.set_global_speed_limit(40) # 全局速度上限 40 km/h
tm.set_respawn_dormant_vehicles(True) # 死掉的车自动重生
tm.set_boundaries_respawn_dormant_vehicles(25, 700) # 重生的距离边界
tm.distance_to_leading_vehicle(npc, 5.0) # 这辆车的跟车距离
tm.set_desired_speed(npc, 0.9) # 目标速度 = 限速 × 0.9
tm.set_vehicle_lane_offset(npc, 0.0) # 横向偏移(米)
tm.set_lane_change_policy(npc,
collision_detection=True, lane_change=True) # 允许变道
tm.set_collision_detection(npc, ego, False) # 让这辆 NPC 忽略和 Ego 的碰撞检测
| 参数 | 含义 | 典型用途 |
|---|---|---|
desired_speed | 相对限速的倍率(0–1) | 0.5 = 温和,1.2 = 激进超速 |
distance_to_leading_vehicle | 跟车距离(米) | 模拟拥堵 vs 高速 |
global_percentage_speed_difference | 与限速的差值百分比 | 负值 = 超速 |
set_collision_detection(npc, target, False) | 关闭 NPC 与 target 的碰撞预测 | 编排场景时让 NPC 故意逼近 Ego |
最后一个参数很重要:默认 TM 会主动避撞。要编排”NPC 切入 Ego 前方”这类场景,必须关掉 NPC 与 Ego 的碰撞检测,否则 TM 检测到接近就提前减速,永远切不进来。
tm.set_hybrid_physics_mode(True)
tm.set_hybrid_physics_radius(50.0)
远处(半径 50m 外)的 NPC 用运动学模型(简化、快),近处(半径内)用完整车辆物理(PhysX、慢但真实)。这是性能优化:
代价是”远车变近车”那一帧物理切换可能不平滑——极端场景下会有可见跳变。
通过组合参数可以构造从温和到激进的车流:
# 温和车流
tm.set_global_distance_to_leading_vehicle(8.0)
for npc in npc_vehicles:
tm.set_desired_speed(npc, 0.7)
tm.set_lane_change_policy(npc, lane_change=False)
# 激进车流
tm.set_global_distance_to_leading_vehicle(3.0)
for npc in npc_vehicles:
tm.set_desired_speed(npc, 1.1)
tm.set_lane_change_policy(npc, lane_change=True)
测试算法在不同交通压力下的表现,是 SparseDriveV2 闭环评测的一部分。
set_autopilot 必须每辆 NPC 都调:不是全局开关,是单车属性。carla.WeatherParameters 的参数集合CARLA 的天气不是一个”晴/阴/雨”枚举,而是一组连续的物理参数。每个参数独立可调:
weather = carla.WeatherParameters(
cloudiness=0.0, # 云量 [0,100]
precipitation=0.0, # 降水强度 [0,100]
precipitation_deposits=0.0, # 路面水/雪沉积 [0,100]
wind_intensity=0.0, # 风强度(影响植被摆动)[0,100]
fog_density=0.0, # 雾密度 [0,100]
fog_distance=0.0, # 雾能见度距离(米)
wetness=0.0, # 湿度/路面湿润 [0,100]
sun_azimuth_angle=0.0, # 太阳方位角(度)
sun_altitude_angle=70.0,# 太阳高度角(度,决定白天/黄昏/夜)
)
world.set_weather(weather)
关键参数的物理含义:
sun_altitude_angle:太阳在地平线上方角度。70° 是正午,0° 是日出/日落(橙红光),负值是夜间。这是光照变化的主因。fog_density / fog_distance:雾由体积散射近似,fog_density 越高散射越强、fog_distance 越小能见度越近。两者共同决定雾的厚度。precipitation_deposits:和 precipitation(下不下雨)不同,这是路面已经被淋湿的程度——决定路面上有没有水渍反光。CARLA 提供一组预设组合:
presets = [
carla.WeatherParameters.ClearNoon,
carla.WeatherParameters.CloudyNoon,
carla.WeatherParameters.WetNoon,
carla.WeatherParameters.WetSunset,
carla.WeatherParameters.MidRainSunset,
carla.WeatherParameters.HardRainNoon,
carla.WeatherParameters.SoftRainSunset,
]
world.set_weather(random.choice(presets))
这些预设是参数组合的快捷方式,本质就是上面那组参数的具体取值。
这是最容易踩坑的地方:
precipitation 增大只让画面更暗、相机噪声增加,车辆的轮胎摩擦系数不变。fog_density 高了能见度低,但 NPC 的 TM 不会因此减速(它用的是 OpenDRIVE 拓扑和 bbox 预测,不是视觉)。wind_intensity 只让树叶和旗帜摆动,车不受风阻。为什么这么设计?因为 CARLA 的天气是渲染层的概念,物理引擎(PhysX)独立运行。要把天气影响加到物理上,需要自己改车辆的 tire_friction_scale、drag_coefficient 等参数。
雾对 RGB 相机的影响是真实的——大气散射让远处物体对比度下降。这给纯视觉算法带来挑战:
所以 Bench2Drive 在多种天气下评测,本质是在测算法的域鲁棒性——能不能在没见过的天气里还正常开车。
天气可以每帧调,做”日落动画”:
for alt in np.linspace(70, -10, 100): # 太阳从 70° 落到 -10°
weather = world.get_weather()
weather.sun_altitude_angle = alt
world.set_weather(weather)
world.tick()
这模拟了一天内的光照变化。但要注意:天气变化是渲染时的,需要 tick 才生效——同步模式下 set 完要 tick 一次才能看到。
这是整个 SparseDriveV2 系统的”心跳”。每一帧严格按这个环运转:
world.tick() # ① Server 推进 0.05s,渲染 6 相机
│
▼
传感器回调采集 6 张图 ──┐
│ │(同 frame ID 对齐入队)
▼ │
SparseDriveV2 推理 ◄───┘ # ② 感知 → 建图 → 规划
│
▼
输出一条规划轨迹 τ = {(x_t, y_t)}
│
▼
apply_control(throttle, steer, brake) # ③ 转成控制指令
│
▼
(下一帧 tick 时物理执行) # ④
s 意味着整个环必须在 50 ms 内跑完:
如果超时,会出现两种问题:
tick() 还没回来,仿真”卡”在原帧——不是真的卡,是 Client 在等模型。fixed_delta_seconds 调大(牺牲物理精度),要么跳帧(牺牲时序一致性)。这是 SparseDriveV2 追求效率的根本原因——26 万候选的 Scoring 必须用 Coarse-to-Fine 才能在预算内完成(详见 EP05)。
模型输出的是轨迹 ——一系列未来时刻的目标位置和速度。但 CARLA 的 apply_control 只接受油门/转向/刹车。中间需要一个**轨迹跟踪控制器(trajectory tracker)**把轨迹转成控制指令。
最常见的是 Pure Pursuit(纯跟踪):
其中 是车的前后轴距(wheelbase), 是前瞻距离。
油门/刹车用 PID 控制当前速度逼近轨迹上指定速度:
一个容易被忽略的细节:apply_control 在本帧 tick 后、下一帧 tick 时才物理生效。因为:
tick() 已经推进过物理,本帧的 apply_control 是设给”下一帧物理步”的输入。这个延迟在 20 Hz 下意味着模型输出的轨迹其实是”为 50 ms 后的世界规划的”。好的轨迹规划器会做模型预测,把这一帧延迟考虑进去。Bench2Drive 评测时所有方案都面对同样的延迟,所以是公平的。
实际系统里,“推理”这步很慢,会让整个环阻塞。常见做法是把推理放到单独的 worker 线程:
import queue, threading
infer_q = queue.Queue()
result_q = queue.Queue()
threading.Thread(target=inference_worker, args=(infer_q, result_q), daemon=True).start()
while True:
world.tick()
images = collect_frame() # 主线程:IO + 数据准备
infer_q.put(images) # 丢给 worker
try:
control = result_q.get(timeout=0.05) # 试着拿上一次推理的结果
ego.apply_control(control)
except queue.Empty:
pass # 推理没好,沿用上一帧控制
注意这种”用上一次推理结果”会引入额外一帧延迟,对控制稳定性有影响——所以推理必须够快,否则要降级。
apply_control:模型出了轨迹但没转成控制,Ego 车不动。要确保 trajectory tracker 一定跑。tick → 采数据 → 推理 → apply_control,不能 apply_control → tick,否则用的是上一帧推理结果去控制本帧物理。