第三章:XML基础与语法——OpenSCENARIO的XML结构、命名空间、元素与属性规则
好,咱们进入正题。这一章讲的是OpenSCENARIO的骨架——XML语法。说白了,你写的每一个场景文件,本质上就是一个XML文档。如果你不懂XML的结构规则,写出来的场景文件要么解析报错,要么跑起来行为诡异。我见过太多人在这上面栽跟头了。
3.1 OpenSCENARIO的XML结构长什么样?
先看一个最简单的例子。一个完整的OpenSCENARIO文件,最外层一定是一个<OpenSCENARIO>根元素。它就像房子的地基,所有东西都长在这个根上。
<?xml version="1.0" encoding="UTF-8"?>
<OpenSCENARIO>
<FileHeader author="ZhangSan" description="Simple cut-in scenario" />
<CatalogLocations>
<VehicleCatalog>
<Directory path="Catalogs/Vehicles" />
</VehicleCatalog>
</CatalogLocations>
<RoadNetwork>
<LogicFile filepath="maps/highway.xodr" />
</RoadNetwork>
<Entities>
<ScenarioObject name="Ego" />
<ScenarioObject name="Target" />
</Entities>
<Storyboard>
<Init>
<Actions>
<PrivateAction>
<LongitudinalAction>
<SpeedAction>
<SpeedActionDynamics dynamicsShape="step" />
<SpeedActionTarget>
<AbsoluteTargetSpeed value="30.0" />
</SpeedActionTarget>
</SpeedAction>
</LongitudinalAction>
</PrivateAction>
</Actions>
</Init>
<Story name="CutInStory">
<Act name="CutInAct">
<ManeuverGroup name="CutInManeuverGroup">
<Actors selectTriggeringEntities="false">
<EntityRef entityRef="Target" />
</Actors>
<Maneuver name="CutInManeuver">
<Event name="CutInEvent">
<Action name="LaneChangeAction">
<PrivateAction>
<LateralAction>
<LaneChangeAction>
<LaneChangeActionDynamics dynamicsShape="sinusoidal" />
<LaneTarget>
<AbsoluteTargetLane value="-1" />
</LaneTarget>
</LaneChangeAction>
</LateralAction>
</PrivateAction>
</Action>
</Event>
</Maneuver>
</ManeuverGroup>
</Act>
</Story>
</Storyboard>
</OpenSCENARIO>
看到没?结构非常清晰。根元素下面,依次是FileHeader、CatalogLocations、RoadNetwork、Entities和Storyboard。这五个是必须的,缺一不可。我个人习惯先把这五个骨架搭好,再往里面填肉。
3.2 命名空间——别小看这个声明
很多新手会忽略命名空间。嗯,这里要注意。OpenSCENARIO的命名空间是强制要求的。你必须在根元素上声明它,否则解析器根本不认。
标准命名空间声明:
<OpenSCENARIO
xmlns="http://www.asam.net/xml/osc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.asam.net/xml/osc
https://www.asam.net/xml/osc/1.2/OpenSCENARIO.xsd">
为什么要有命名空间?说白了就是为了避免元素名字冲突。你想想看,万一你的场景文件里有个<Speed>元素,而OpenSCENARIO标准里也有个<Speed>,解析器怎么区分?命名空间就是给元素贴了个标签,告诉解析器「这个<Speed>是OpenSCENARIO标准里的那个」。
我在项目中遇到过一件事:有个同事从网上复制了一段场景代码,没注意命名空间版本,结果跑了一整天都报错。最后发现他用的schema是1.0的,但代码里写的元素是1.2才支持的。所以,命名空间的版本一定要和你的OpenSCENARIO版本匹配。
3.3 元素与属性——OpenSCENARIO的积木块
XML里的元素,你可以理解成积木块。每个积木块有自己的名字,比如<SpeedAction>、<LaneChangeAction>。积木块里面可以嵌套其他积木块,也可以有自己的属性。
属性是写在元素标签里的键值对。比如:
<AbsoluteTargetSpeed value="30.0" />
这里的value就是属性,"30.0"是属性的值。属性用来描述元素的特征。比如速度的目标值、车道变化的动态形状等等。
我的小技巧:写属性值时,尽量用双引号。虽然XML也支持单引号,但OpenSCENARIO的官方示例全部用双引号。保持一致,能少很多麻烦。
3.4 元素嵌套规则——树形结构你得懂
OpenSCENARIO的元素嵌套是有严格层级关系的。你不能把<SpeedAction>直接放在<Storyboard>下面,它必须放在<PrivateAction>里面。这个层级关系,在官方Schema文件里定义得清清楚楚。
我给大家总结一下常见的嵌套规则:
| 父元素 | 子元素 | 说明 |
|---|---|---|
| OpenSCENARIO | FileHeader, CatalogLocations, RoadNetwork, Entities, Storyboard | 五个必须按顺序出现 |
| Storyboard | Init, Story | Init在前,Story在后 |
| Story | Act | 一个Story可以包含多个Act |
| Act | ManeuverGroup, Condition (StartTrigger, StopTrigger) | 触发条件必须定义 |
| ManeuverGroup | Actors, Maneuver | 先定义参与者,再定义动作 |
| Maneuver | Event | 一个Maneuver可以有多个Event |
| Event | Action, Condition (StartTrigger) | 每个Event必须有触发条件 |
这张表你最好记在心里。我曾经因为把Init和Story的顺序搞反了,排查了整整一个下午。后来才发现,OpenSCENARIO要求Init必须在Story之前。顺序错了,解析器直接报错。
3.5 自闭合元素与空元素
你注意到没有,有些元素是自闭合的,比如<AbsoluteTargetSpeed value="30.0" />。这种元素没有子元素,也没有文本内容,所以用一个标签搞定。而有些元素有子元素,比如<SpeedAction>,它里面还包着<SpeedActionDynamics>和<SpeedActionTarget>。
什么时候用自闭合?很简单:这个元素不需要包含任何子元素或文本时,就用自闭合。比如<EntityRef entityRef="Target" />,它只有一个属性,没有子元素,自闭合最合适。
注意:自闭合元素不能有子元素。如果你写成了<EntityRef entityRef="Target"></EntityRef>,虽然语法上没错,但不符合OpenSCENARIO的规范。解析器可能会忽略里面的内容。保持简洁,用自闭合。
3.6 属性类型——字符串、数字、枚举,一个都不能错
OpenSCENARIO的属性是有类型的。常见的类型有:
- 字符串(string):比如
name="Ego",直接写文本。 - 浮点数(double):比如
value="30.0",注意带小数点。 - 整数(int):比如
value="-1",不带小数点。 - 布尔值(boolean):比如
selectTriggeringEntities="false",只能是true或false。 - 枚举(enum):比如
dynamicsShape="sinusoidal",值必须是预定义的几个之一。
这里有个坑。布尔值在XML里是true和false,不是True和False。大小写敏感!我见过有人写True,结果解析器报错,还以为是工具的问题。其实,就是大小写写错了。
3.7 注释——给你的场景文件加点说明
XML支持注释,用<!-- 注释内容 -->。我个人习惯在复杂的场景文件里加注释,比如某个Condition的逻辑是什么,某个Action的触发时机。这样过几个月回头看,还能想起来当初的设计意图。
<!-- 这个事件在目标车辆进入本车道后触发 -->
<Event name="CutInEvent">
<Action name="LaneChangeAction">
...
</Action>
<StartTrigger>
<ConditionGroup>
<Condition name="TargetInLane">
<ByEntityCondition>
<TriggeringEntities rule="any">
<EntityRef entityRef="Target" />
</TriggeringEntities>
<EntityCondition>
<RelativeLaneCondition entityRef="Ego" rule="equalTo" value="-1" />
</EntityCondition>
</ByEntityCondition>
</Condition>
</ConditionGroup>
</StartTrigger>
</Event>
注释不会影响解析,但能帮你自己和同事理解场景逻辑。尤其是团队协作时,注释就是最好的文档。
3.8 避坑指南——我踩过的那些XML坑
最后,分享几个我实际踩过的坑,希望能帮你省点时间。
坑1:属性值里的特殊字符
如果你在属性值里用了<、>、&这些字符,必须用转义序列。比如&要写成&。我遇到过有人想在description属性里写A & B,结果解析器把&当成实体引用的开始,直接报错。
坑2:空元素和自闭合混用
有些元素既可以自闭合,也可以写成非自闭合。但OpenSCENARIO的Schema对某些元素有严格要求。比如<Directory path="Catalogs/Vehicles" />必须自闭合,不能写成<Directory path="Catalogs/Vehicles"></Directory>。虽然XML语法上没问题,但OpenSCENARIO解析器会报错。所以,严格按照官方示例来写。
坑3:编码声明
文件第一行的<?xml version="1.0" encoding="UTF-8"?>不能省略。我见过有人删掉这行,结果场景文件在Linux和Windows之间来回拷贝时,中文注释全变成乱码。加上编码声明,能避免很多跨平台问题。
好了,这一章的内容就这些。XML基础虽然看起来简单,但它是OpenSCENARIO的基石。你写场景文件时,80%的报错都跟XML语法有关。把这一章吃透,后面的章节你会轻松很多。