环境搭建:仿真工具链安装、OpenSCENARIO解析器配置、首个场景文件运行

说实话,很多初学者一上来就急着写场景,结果卡在环境搭建这一步好几天。我当年也踩过这个坑——工具链版本不匹配、解析器报错找不到符号、第一个场景跑出来全是乱码。嗯,这节课咱们就把这些坑一个个填平。

1. 仿真工具链的选择与安装

先说说工具链。目前主流的自动驾驶仿真平台就那么几个:CARLA、SUMO、VTD、51Sim-One。我个人习惯用 CARLA + esmini 的组合,原因很简单——开源、社区活跃、对OpenSCENARIO的支持比较完整。

安装步骤其实不复杂,但有几个细节要注意:

  • CARLA 版本:建议用 0.9.13 或更高版本。我之前用 0.9.11 折腾了两天,发现它对 OpenSCENARIO 1.0 的支持有 bug,升级后就好了。
  • Python 环境:CARLA 依赖 Python 3.7~3.9,别用 3.10+,否则编译会报错。
  • esmini:这是一个轻量级的 OpenSCENARIO 播放器,用来验证场景文件非常方便。下载预编译包就行,不需要自己编译。

我的安装清单(Ubuntu 20.04 为例):

# 1. 安装 CARLA
wget https://carla-releases.s3.eu-west-3.amazonaws.com/Linux/CARLA_0.9.13.tar.gz
tar -xzf CARLA_0.9.13.tar.gz
cd CARLA_0.9.13
./CarlaUE4.sh  # 启动测试

# 2. 安装 esmini
wget https://github.com/esmini/esmini/releases/download/v2.35.0/esmini_linux.zip
unzip esmini_linux.zip -d esmini
cd esmini/bin
./esmini --window 60 60 800 600  # 启动测试

# 3. 安装 Python 依赖
pip install carla
pip install pyproj  # 坐标转换用

注意:CARLA 和 esmini 的安装路径不要有中文或空格。我曾经因为路径里带了个空格,折腾了一下午才发现是这个问题。

2. OpenSCENARIO 解析器配置

解析器是什么?说白了,它就是负责把 .xosc 文件翻译成仿真引擎能理解的指令。没有它,你写的场景就是一堆废纸。

目前主流的解析器有两个:

解析器 适用场景 特点
esmini 内置解析器 快速验证、单场景测试 轻量、启动快、支持 OpenSCENARIO 1.0
CARLA ScenarioRunner 复杂场景、多车协同 功能全、支持 OpenSCENARIO 1.1、可扩展

我个人建议:先用 esmini 做快速验证,再用 ScenarioRunner 做深度测试。为什么?因为 esmini 启动只要 2 秒,而 ScenarioRunner 要加载整个 CARLA 世界,至少 30 秒。你想想看,每次改个参数都要等半分钟,效率多低。

配置 esmini 解析器其实很简单:

# 在 esmini 目录下创建配置文件 config.txt
# 内容如下:
--window 60 60 800 600
--osc ../scenarios/my_scenario.xosc
--path ../models

# 然后运行:
./esmini --cfg config.txt

小技巧:如果你用 ScenarioRunner,记得先检查 scenario_runner/config.py 里的路径配置。我遇到过好几次因为路径写错导致解析器找不到场景文件的情况。

3. 首个场景文件运行

好了,工具链装好了,解析器也配好了,咱们来跑第一个场景。别整太复杂,就一个最简单的场景:主车直线行驶,前方 50 米处有一辆静止车辆

先写 .xosc 文件:

<?xml version="1.0" encoding="UTF-8"?>
<OpenSCENARIO>
  <FileHeader author="demo" description="first scenario" />
  <ParameterDeclarations/>
  <Catalogs/>
  <RoadNetwork>
    <LogicFile filepath="xodr/straight_road.xodr"/>
  </RoadNetwork>
  <Entities>
    <ScenarioObject name="Ego">
      <VehicleCategory car/>
    </ScenarioObject>
    <ScenarioObject name="Target">
      <VehicleCategory car/>
    </ScenarioObject>
  </Entities>
  <Storyboard>
    <Init>
      <Actions>
        <Private entityRef="Ego">
          <PrivateAction>
            <LongitudinalAction>
              <SpeedAction>
                <SpeedActionDynamics dynamicsShape="step"/>
                <SpeedActionTarget>
                  <AbsoluteTargetSpeed value="10"/>
                </SpeedActionTarget>
              </SpeedAction>
            </LongitudinalAction>
          </PrivateAction>
          <PrivateAction>
            <TeleportAction>
              <Position>
                <LanePosition roadId="1" laneId="-1" offset="0" s="0"/>
              </Position>
            </TeleportAction>
          </PrivateAction>
        </Private>
        <Private entityRef="Target">
          <PrivateAction>
            <TeleportAction>
              <Position>
                <LanePosition roadId="1" laneId="-1" offset="0" s="50"/>
              </Position>
            </TeleportAction>
          </PrivateAction>
        </Private>
      </Actions>
    </Init>
    <Story name="my_story">
      <Act name="my_act">
        <ManeuverGroup name="my_group" maximumExecutionCount="1">
          <Actors selectTriggeringEntities="false">
            <EntityRef entityRef="Ego"/>
          </Actors>
          <Maneuver name="my_maneuver">
            <Event name="my_event" priority="overwrite">
              <Action name="keep_speed">
                <PrivateAction>
                  <LongitudinalAction>
                    <SpeedAction>
                      <SpeedActionDynamics dynamicsShape="step"/>
                      <SpeedActionTarget>
                        <AbsoluteTargetSpeed value="10"/>
                      </SpeedActionTarget>
                    </SpeedAction>
                  </LongitudinalAction>
                </PrivateAction>
              </Action>
            </Event>
          </Maneuver>
        </ManeuverGroup>
        <StartTrigger>
          <ConditionGroup>
            <Condition name="start_condition" delay="0">
              <ByValueCondition>
                <SimulationTimeCondition value="0" rule="greaterThan"/>
              </ByValueCondition>
            </Condition>
          </ConditionGroup>
        </StartTrigger>
      </Act>
    </Story>
    <StopTrigger/>
  </Storyboard>
</OpenSCENARIO>

保存为 my_first_scenario.xosc,然后运行:

cd esmini/bin
./esmini --window 60 60 800 600 --osc ../scenarios/my_first_scenario.xosc

如果一切顺利,你会看到一辆车在直道上匀速行驶,前方停着一辆车。嗯,就这么简单。

常见问题排查:

  • 场景加载后黑屏:检查 .xodr 文件路径是否正确。esmini 默认在 esmini/resources/xodr 下找路网文件。
  • 车辆不动:检查 SpeedAction 的 dynamicsShape 是不是 "step",如果是 "sinusoidal" 需要配合时间参数。
  • 解析器报错 "Entity not found":检查 Entities 里的 name 和 Storyboard 里引用的 entityRef 是否一致。

避坑指南:我曾经犯过一个低级错误——把 LanePositionlaneId 写成了正数。在 OpenDRIVE 标准里,车道编号正负代表方向,负号才是前进方向。这个坑我至少见过 5 个新手踩过。

4. 验证场景文件

场景跑起来了,但怎么知道它对不对?我一般用三个方法验证:

  1. 肉眼观察:看车辆运动是否符合预期。比如主车是不是真的在 10 m/s 匀速行驶。
  2. 日志输出:esmini 启动时加 --logfile log.txt 参数,会记录每一帧的车辆状态。
  3. 回放对比:用 --record recording.dat 记录场景,然后用 --play recording.dat 回放,看是否一致。

我个人最常用的是第二种。打开日志文件,搜索 "Ego" 关键字,看速度值是不是稳定在 10 左右。如果是,说明场景文件没问题。

好了,环境搭建这块就讲到这里。下一节课咱们会深入 OpenSCENARIO 的核心语法,包括条件触发、事件管理这些高级功能。到时候你会发现,今天写的这个场景虽然简单,但它的结构是所有复杂场景的基础。