4. OpenSCENARIO标准入门:XML结构、核心元素(Storyboard、Maneuver、Action)
好,咱们今天聊聊OpenSCENARIO。说实话,我第一次接触这个标准的时候,心里是有点抵触的——又是个XML?但真正用起来之后,我发现它其实挺优雅的。说白了,它就是一套用来描述“场景怎么演”的剧本语言。
你想想看,我们做自动驾驶仿真测试,光有静态地图和动态交通流是不够的。我们需要精确控制每一辆车在什么时间、什么地点、做什么动作。OpenSCENARIO就是干这个的。
4.1 XML结构:从根节点说起
OpenSCENARIO文件本质上是一个XML文档。它的根节点是<OpenSCENARIO>,里面包含了两个主要部分:<FileHeader>和<ScenarioDefinition>。
<?xml version="1.0" encoding="UTF-8"?>
<OpenSCENARIO>
<FileHeader author="ZhangSan" description="Cut-in场景" revMinor="0" revMajor="1"/>
<ScenarioDefinition>
<!-- 核心内容都在这里 -->
</ScenarioDefinition>
</OpenSCENARIO>
嗯,这里要注意一点:FileHeader里的revMajor和revMinor是版本号。我个人习惯用1.0版本,因为目前大部分工具链都支持得比较好。
ScenarioDefinition下面又分三个子节点:
- ParameterDeclarations:参数声明,相当于变量定义
- CatalogLocations:目录位置,引用外部资源
- Storyboard:故事板,整个场景的核心
说白了,前两个是准备工作,第三个才是真正干活的地方。
4.2 Storyboard:场景的总导演
Storyboard是OpenSCENARIO里最重要的概念。它定义了整个场景的“剧本”。我把它理解成一个舞台剧的导演——它告诉所有演员(车辆、行人)什么时候上场、什么时候退场、做什么动作。
Storyboard包含两个子元素:
- Init:初始化状态。所有实体(车辆、行人、交通灯)的初始位置、速度、朝向都在这里定义。
- Story:故事本身。一个场景可以有多个Story,每个Story包含多个Act。
核心概念:Storyboard的层级结构是 Story → Act → ManeuverGroup → Maneuver → Event → Action。每一层都是“包含”关系,从上到下越来越具体。
我在项目中遇到过一个问题:两个Act同时触发,导致车辆行为冲突。后来我学乖了,每个Act都加上明确的触发条件,用<Condition>来控制执行时机。
4.3 Maneuver:动作的编排者
Maneuver,说白了就是一组动作的集合。它本身不执行任何动作,而是把多个Event组织在一起。每个Event里可以包含多个Action。
<Maneuver name="LaneChangeManeuver">
<Event name="StartLaneChange" priority="overwrite">
<Action name="ChangeToLeftLane">
<PrivateAction>
<LaneChangeAction>
<LaneChangeTarget>
<RelativeTargetLane entityRef="Ego" value="-1"/>
</LaneChangeTarget>
<LaneChangeActionDynamics dynamicsDimension="time" value="3.0" dynamicsShape="linear"/>
</LaneChangeAction>
</PrivateAction>
</Action>
<StartTrigger>
<ConditionGroup>
<Condition name="TriggerCondition" delay="0">
<ByEntityCondition>
<TriggeringEntities rule="any" entityRef="Ego"/>
<EntityCondition>
<DistanceCondition value="20.0" freespace="false" alongRoute="true" rule="lessThan"/>
</EntityCondition>
</ByEntityCondition>
</Condition>
</ConditionGroup>
</StartTrigger>
</Event>
</Maneuver>
你看这个例子,Maneuver里定义了一个变道事件。当自车距离前车小于20米时,触发向左变道的动作。这里有个小技巧:priority="overwrite"表示如果这个事件和另一个事件冲突,它优先执行。
我的经验:写Maneuver时,尽量把相关的Event放在同一个Maneuver里。比如“超车”这个行为,包含“加速”、“变道”、“再变回来”三个Event,放在一个Maneuver里管理起来更清晰。
4.4 Action:最小的执行单元
Action是OpenSCENARIO里最底层的执行单元。它定义了“做什么”。Action分为两类:
| 类型 | 说明 | 常见子类型 |
|---|---|---|
| GlobalAction | 影响整个场景的动作 | EnvironmentAction(天气)、TrafficSignalAction(交通灯) |
| PrivateAction | 影响单个实体的动作 | LongitudinalAction(纵向)、LateralAction(横向)、VisibilityAction(可见性) |
| UserDefinedAction | 自定义动作 | 通过CustomCommand实现 |
PrivateAction是最常用的。它又细分为:
- LongitudinalAction:控制速度、加速度。比如
SpeedAction、LongitudinalDistanceAction - LateralAction:控制横向行为。比如
LaneChangeAction、LaneOffsetAction - RoutingAction:控制路径。比如
AcquirePositionAction、FollowTrajectoryAction - VisibilityAction:控制可见性。比如让某辆车突然“消失”
我曾经踩过一个坑:用SpeedAction让车辆减速到0,但忘记设置dynamicsShape参数,结果车辆瞬间刹停,完全不真实。后来我养成了习惯,所有动力学参数都明确指定。
注意:Action的执行时间由Event的StartTrigger和StopTrigger控制。如果不设置StopTrigger,Action会一直执行直到被其他Action覆盖。这很容易导致意外的行为。
4.5 一个完整的例子:Cut-in场景
咱们把前面讲的知识点串起来,写一个完整的Cut-in场景。这个场景很简单:自车在中间车道行驶,右侧车道有一辆车加速后切入自车前方。
<OpenSCENARIO>
<FileHeader author="LiSi" description="Cut-in场景示例" revMinor="0" revMajor="1"/>
<ScenarioDefinition>
<ParameterDeclarations>
<ParameterDeclaration name="$CutInSpeed" parameterType="double" value="15.0"/>
<ParameterDeclaration name="$EgoSpeed" parameterType="double" value="10.0"/>
</ParameterDeclarations>
<Storyboard>
<Init>
<Actions>
<Private name="Ego">
<LongitudinalAction>
<SpeedAction>
<SpeedActionTarget>
<AbsoluteTargetSpeed value="$EgoSpeed"/>
</SpeedActionTarget>
<SpeedActionDynamics dynamicsShape="step" dynamicsDimension="time" value="0"/>
</SpeedAction>
</LongitudinalAction>
</Private>
<Private name="CutInVehicle">
<LongitudinalAction>
<SpeedAction>
<SpeedActionTarget>
<AbsoluteTargetSpeed value="$CutInSpeed"/>
</SpeedActionTarget>
<SpeedActionDynamics dynamicsShape="step" dynamicsDimension="time" value="0"/>
</SpeedAction>
</LongitudinalAction>
</Private>
</Actions>
</Init>
<Story name="CutInStory">
<Act name="CutInAct">
<ManeuverGroup name="CutInManeuverGroup" maximumExecutionCount="1">
<Actors selectTriggeringEntities="false">
<EntityRef entityRef="CutInVehicle"/>
</Actors>
<Maneuver name="CutInManeuver">
<Event name="ChangeLaneEvent" priority="overwrite">
<Action name="ChangeToLeftLane">
<PrivateAction>
<LateralAction>
<LaneChangeAction>
<LaneChangeTarget>
<RelativeTargetLane entityRef="Ego" value="0"/>
</LaneChangeTarget>
<LaneChangeActionDynamics dynamicsShape="linear" dynamicsDimension="time" value="3.0"/>
</LaneChangeAction>
</LateralAction>
</PrivateAction>
</Action>
<StartTrigger>
<ConditionGroup>
<Condition name="DistanceCondition" delay="0">
<ByEntityCondition>
<TriggeringEntities rule="any" entityRef="CutInVehicle"/>
<EntityCondition>
<DistanceCondition value="5.0" freespace="false" alongRoute="true" rule="lessThan"/>
</EntityCondition>
</ByEntityCondition>
</Condition>
</ConditionGroup>
</StartTrigger>
</Event>
</Maneuver>
</ManeuverGroup>
<StopTrigger>
<ConditionGroup>
<Condition name="StopCondition" delay="0">
<ByStateCondition>
<AtStartCondition/>
</ByStateCondition>
</Condition>
</ConditionGroup>
</StopTrigger>
</Act>
</Story>
</Storyboard>
</ScenarioDefinition>
</OpenSCENARIO>
这个例子虽然简单,但包含了OpenSCENARIO的核心要素:参数声明、初始化、Story、Act、Maneuver、Event、Action、触发条件。你想想看,如果把这个场景跑在仿真器里,CutInVehicle会在距离自车5米时开始变道,整个过程持续3秒。
避坑指南:我曾经在写触发条件时,把rule="lessThan"写成了rule="greaterThan",结果车辆在距离很远的时候就变道了,完全不符合预期。所以每次写完触发条件,我都会在脑子里过一遍逻辑:这个条件什么时候为真?
好了,关于OpenSCENARIO的XML结构和核心元素,咱们就聊到这里。下一章我会讲怎么用这些元素组合出更复杂的场景,比如多车交互、天气变化这些。记住,写场景文件就像写剧本,Storyboard是导演,Maneuver是编剧,Action是演员——三者配合好了,才能演出一场好戏。