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里的revMajorrevMinor是版本号。我个人习惯用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:控制速度、加速度。比如SpeedActionLongitudinalDistanceAction
  • LateralAction:控制横向行为。比如LaneChangeActionLaneOffsetAction
  • RoutingAction:控制路径。比如AcquirePositionActionFollowTrajectoryAction
  • 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是演员——三者配合好了,才能演出一场好戏。