第三章: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>

看到没?结构非常清晰。根元素下面,依次是FileHeaderCatalogLocationsRoadNetworkEntitiesStoryboard。这五个是必须的,缺一不可。我个人习惯先把这五个骨架搭好,再往里面填肉。

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必须有触发条件

这张表你最好记在心里。我曾经因为把InitStory的顺序搞反了,排查了整整一个下午。后来才发现,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",只能是truefalse
  • 枚举(enum):比如dynamicsShape="sinusoidal",值必须是预定义的几个之一。

这里有个坑。布尔值在XML里是truefalse,不是TrueFalse。大小写敏感!我见过有人写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:属性值里的特殊字符

如果你在属性值里用了<>&这些字符,必须用转义序列。比如&要写成&amp;。我遇到过有人想在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语法有关。把这一章吃透,后面的章节你会轻松很多。