4、场景文件结构:根元素、Storyboard、Entities、Story、Act、ManeuverGroup详解

好,咱们今天来聊聊OpenSCENARIO的场景文件结构。说实话,我刚接触这个标准的时候,第一反应是——这玩意儿怎么这么多层?跟套娃似的。但用久了你会发现,这种分层设计其实非常科学,每个层级各司其职。

一个标准的OpenSCENARIO文件,本质上就是一个XML文档。它的根元素是<OpenSCENARIO>,所有内容都包在里面。根元素下面,直接挂着两个最重要的孩子:<FileHeader><OpenSCENE>。前者是文件头,记录版本号、作者、描述这些元信息;后者才是真正的场景本体。

核心要点:OpenSCENARIO的场景结构可以概括为「一个根,两大块,层层嵌套」。两大块就是Entities(实体定义)和Storyboard(故事板),它们是场景的骨架和血肉。

4.1 根元素与FileHeader

根元素<OpenSCENARIO>没什么好说的,就是个容器。但<FileHeader>我建议你认真对待。它有三个属性:revMajorrevMinordate。版本号决定了你用的标准是1.0还是1.1还是1.2,不同版本之间有些细微差异。

我个人习惯在description字段里写清楚这个场景的测试目的。比如「测试自车在交叉口面对闯红灯行人时的紧急制动响应」。别小看这个习惯,项目大了以后,几十上百个场景文件,光靠文件名根本记不住。

<OpenSCENARIO>
  <FileHeader
    revMajor="1"
    revMinor="0"
    date="2024-01-15T10:00:00"
    description="交叉口行人闯红灯场景 - AEB功能测试"
    author="ZhangSan"/>
  <OpenSCENE>
    ...
  </OpenSCENE>
</OpenSCENARIO>

4.2 Entities:场景里的「演员表」

Entities,说白了就是场景里所有会动的东西。车辆、行人、自行车、甚至宠物狗,都算实体。每个实体都有一个唯一的name,这个名字会在后面的Storyboard里被反复引用。

实体分两种:一种是ScenarioObject,代表具体的交通参与者;另一种是EntitySelection,用于选择一组实体。实际项目中,90%的情况你只需要ScenarioObject。

每个ScenarioObject可以绑定一个VehiclePedestrianMiscObject。这里面有个坑——车辆的category属性。我记得有一次,我定义了一辆「car」,但忘了设category,结果仿真器把它当成了静态障碍物,怎么都不动。排查了半天才发现是这个问题。

<Entities>
  <ScenarioObject name="Ego">
    <Vehicle name="Sedan" category="car">
      <BoundingBox>
        <Dimensions width="1.8" length="4.5" height="1.5"/>
      </BoundingBox>
      <Properties>
        <Property name="maxSpeed" value="30.0"/>
      </Properties>
    </Vehicle>
  </ScenarioObject>
  <ScenarioObject name="Pedestrian1">
    <Pedestrian name="Adult" category="pedestrian">
      <BoundingBox>
        <Dimensions width="0.5" length="0.6" height="1.75"/>
      </BoundingBox>
    </Pedestrian>
  </ScenarioObject>
</Entities>

小技巧:实体的命名最好遵循统一规范。比如自车统一叫「Ego」,目标车叫「Target1」「Target2」,行人叫「Ped1」「Ped2」。这样在写Story的时候,一眼就能看出谁是谁。

4.3 Storyboard:场景的「导演剧本」

Storyboard是整个场景文件里最核心的部分。它定义了场景从开始到结束的完整流程。一个Storyboard包含三部分:Init(初始化)、Story(故事)和End(结束条件)。

Init阶段负责把所有实体放到它们初始的位置上,设置初始速度、朝向等。End阶段定义场景什么时候结束——通常是某个条件满足后,比如自车停止、或者碰撞发生、或者超时。

嗯,这里要注意:Init里的动作是一次性执行的,而Story里的动作是按时间线触发的。这个区别很重要,我见过有人把初始化动作写到Story里,结果每次循环都重新执行一遍,乱套了。

<Storyboard>
  <Init>
    <Actions>
      <PrivateAction entityRef="Ego">
        <TeleportAction>
          <Position>
            <WorldPosition x="0.0" y="0.0" z="0.0" h="0.0"/>
          </Position>
        </TeleportAction>
        <LongitudinalAction>
          <SpeedAction>
            <SpeedActionDynamics dynamicsShape="step" value="0.0"/>
            <SpeedActionTarget>
              <AbsoluteTargetSpeed value="10.0"/>
            </SpeedActionTarget>
          </SpeedAction>
        </LongitudinalAction>
      </PrivateAction>
    </Actions>
  </Init>
  <Story name="MainStory">
    ...
  </Story>
  <End>
    <ConditionGroup>
      <Condition name="EndCondition" delay="0">
        <ByEntityCondition>
          <TriggeringEntities rule="any">
            <EntityRef entityRef="Ego"/>
          </TriggeringEntities>
          <EntityCondition>
            <EndOfRoadCondition duration="3.0"/>
          </EntityCondition>
        </ByEntityCondition>
      </Condition>
    </ConditionGroup>
  </End>
</Storyboard>

4.4 Story、Act、ManeuverGroup:三层嵌套的执行逻辑

Story、Act、ManeuverGroup这三层,是OpenSCENARIO里最容易让人晕的地方。我当初学的时候,画了好几张图才理清楚。说白了,它们的关系是这样的:

  • Story:一个场景可以有多个Story,它们可以并行执行。每个Story代表一条独立的故事线。
  • Act:一个Story里可以有多个Act,但同一时间只能有一个Act处于激活状态。Act之间是顺序执行的。
  • ManeuverGroup:一个Act里可以有多个ManeuverGroup,它们可以并行执行。每个ManeuverGroup绑定一组实体,执行一组动作。

你想想看,这种设计是不是很像拍电影?Story是主线剧情,Act是每一幕,ManeuverGroup是每一幕里不同演员的表演。每个层级都有自己的开始条件和结束条件,灵活度非常高。

实战经验:我建议一个场景只用一个Story。多Story虽然理论上可行,但调试起来非常痛苦。你很难判断两个Story之间的时序关系是否如你所愿。一个Story,多个Act,每个Act里放几个ManeuverGroup,这样结构最清晰。

4.5 ManeuverGroup与Maneuver:真正的「动作执行者」

ManeuverGroup是动作的容器,它本身不执行任何动作。真正干活的是它里面的Maneuver,而Maneuver里又包含Event,Event里才是具体的Action

这个嵌套层级确实有点深。我给大家一个记忆口诀:「组里有演,演里有场,场里有动」。ManeuverGroup是组,Maneuver是演(表演),Event是场(场景),Action是动(动作)。

每个Event可以设置触发条件,比如「当自车距离目标车小于10米时,触发行人横穿动作」。这种条件触发机制,正是OpenSCENARIO最强大的地方——它让场景不再是死板的脚本,而是能根据实时状态动态响应。

<ManeuverGroup name="PedestrianCrossing" maximumExecutionCount="1">
  <Actors>
    <EntityRef entityRef="Pedestrian1"/>
  </Actors>
  <Maneuver name="CrossRoad">
    <Event name="StartCrossing" priority="overwrite">
      <Action name="WalkAction">
        <PrivateAction>
          <LongitudinalAction>
            <SpeedAction>
              <SpeedActionDynamics dynamicsShape="step" value="0.0"/>
              <SpeedActionTarget>
                <AbsoluteTargetSpeed value="1.4"/>
              </SpeedActionTarget>
            </SpeedAction>
          </LongitudinalAction>
        </PrivateAction>
      </Action>
      <StartTrigger>
        <ConditionGroup>
          <Condition name="DistanceCondition" delay="0">
            <ByEntityCondition>
              <TriggeringEntities rule="any">
                <EntityRef entityRef="Ego"/>
              </TriggeringEntities>
              <EntityCondition>
                <RelativeDistanceCondition
                  value="10.0"
                  freespace="false"
                  rule="lessThan"
                  relativeDistanceType="longitudinal"/>
              </EntityCondition>
            </ByEntityCondition>
          </Condition>
        </ConditionGroup>
      </StartTrigger>
    </Event>
  </Maneuver>
</ManeuverGroup>

避坑指南:我曾经在ManeuverGroup里忘了设置maximumExecutionCount,结果这个动作组被无限循环执行,仿真器直接卡死了。这个属性控制动作组最多执行几次,默认是无限次。如果你只想让它执行一次,记得设成1。

4.6 条件系统:场景的「大脑」

条件(Condition)是OpenSCENARIO里最灵活也最复杂的部分。它分为两大类:ByEntityCondition(基于实体的条件)和ByValueCondition(基于数值的条件)。

基于实体的条件包括距离条件、速度条件、碰撞条件、到达道路终点条件等。基于数值的条件包括模拟时间条件、参数条件、用户自定义条件等。

每个条件都有一个delay属性和一个rule属性。delay表示条件满足后延迟多少秒触发,rule表示比较规则(大于、小于、等于)。这两个属性用好了,能实现非常精细的时序控制。

条件类型 常用场景 示例
RelativeDistanceCondition 自车接近目标车时触发 距离小于10米时行人横穿
RelativeSpeedCondition 两车速度差过大时触发 相对速度大于20km/h时报警
CollisionCondition 碰撞检测 任何实体发生碰撞时结束场景
SimulationTimeCondition 定时触发 仿真开始后5秒触发变道
ReachPositionCondition 到达指定位置 自车到达路口中心时触发

我个人最常用的是RelativeDistanceConditionSimulationTimeCondition。前者适合做交互式场景,后者适合做时序固定的场景。两者结合,能覆盖90%以上的测试需求。

4.7 总结:一张图看懂场景结构

说了这么多,其实OpenSCENARIO的场景结构可以用一句话概括:Entities定义谁参与,Storyboard定义怎么演,条件系统定义何时触发。这三者缺一不可。

刚开始写场景的时候,我建议你从最简单的模板开始:一个Entities块定义自车和目标车,一个Storyboard里放一个Story,Story里放一个Act,Act里放一个ManeuverGroup。跑通了再慢慢加复杂度。别一上来就搞多Story多Act,容易把自己绕进去。

好了,这一章的内容就到这里。下一章我们会深入讲解如何用OpenSCENARIO编写一个完整的测试场景,从零开始,一步步搭建。到时候我会拿一个实际项目中的案例来演示,保证干货满满。