4、场景文件结构:根元素、Storyboard、Entities、Story、Act、ManeuverGroup详解
好,咱们今天来聊聊OpenSCENARIO的场景文件结构。说实话,我刚接触这个标准的时候,第一反应是——这玩意儿怎么这么多层?跟套娃似的。但用久了你会发现,这种分层设计其实非常科学,每个层级各司其职。
一个标准的OpenSCENARIO文件,本质上就是一个XML文档。它的根元素是<OpenSCENARIO>,所有内容都包在里面。根元素下面,直接挂着两个最重要的孩子:<FileHeader>和<OpenSCENE>。前者是文件头,记录版本号、作者、描述这些元信息;后者才是真正的场景本体。
核心要点:OpenSCENARIO的场景结构可以概括为「一个根,两大块,层层嵌套」。两大块就是Entities(实体定义)和Storyboard(故事板),它们是场景的骨架和血肉。
4.1 根元素与FileHeader
根元素<OpenSCENARIO>没什么好说的,就是个容器。但<FileHeader>我建议你认真对待。它有三个属性:revMajor、revMinor和date。版本号决定了你用的标准是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可以绑定一个Vehicle、Pedestrian或MiscObject。这里面有个坑——车辆的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 | 到达指定位置 | 自车到达路口中心时触发 |
我个人最常用的是RelativeDistanceCondition和SimulationTimeCondition。前者适合做交互式场景,后者适合做时序固定的场景。两者结合,能覆盖90%以上的测试需求。
4.7 总结:一张图看懂场景结构
说了这么多,其实OpenSCENARIO的场景结构可以用一句话概括:Entities定义谁参与,Storyboard定义怎么演,条件系统定义何时触发。这三者缺一不可。
刚开始写场景的时候,我建议你从最简单的模板开始:一个Entities块定义自车和目标车,一个Storyboard里放一个Story,Story里放一个Act,Act里放一个ManeuverGroup。跑通了再慢慢加复杂度。别一上来就搞多Story多Act,容易把自己绕进去。
好了,这一章的内容就到这里。下一章我们会深入讲解如何用OpenSCENARIO编写一个完整的测试场景,从零开始,一步步搭建。到时候我会拿一个实际项目中的案例来演示,保证干货满满。