找回密码
 点一下
查看: 8818|回复: 32

[教程]SC2 XML数据文件中的继承、追加、覆盖规则和语法(20楼补充了包外文件的特殊情况)

[复制链接]
发表于 2010-3-4 19:32:30 | 显示全部楼层 |阅读模式
本文一些术语基于我的这篇规范文:
http://bbs.islga.org/read-htm-tid-38476.html

本文基于我对SC2 XML数据文件一些普遍规律的总结,希望可以帮助大家更好地创作单位和技能还有其他杂七杂八的什么东西的模板。


今天这篇教程呢,讲的是SC2 XML数据文件中各个模板之间的继承规则,以及在此基础上的属性追加和覆盖规则。


不过在这之前,我们先定义一些新的术语:
首先定义SC2模板的继承概念:

注意SC2模板的继承区别于编程概念的继承。具体在于,编程概念的继承意思是直接使用父类定义的属性和方法。但严格来说,SC2数据模板的继承的与其说是属性的"定义"不如说是这些属性的“值”。

单位模板、技能模板、效果模板以及其他所有类型的模板,他们各自有哪些属性基本上是由该模板的基础类所决定的,虽然我们也可以自定义一些属性,但也要遵从很严格的规范。一个单位肯定有生命值这个属性,这是无需通过模板来定义的,模板指定的只是它的值(即这类型的单位有多少HP)。

因此我们说模板A继承模板B的时候,意即模板A继承了模板B所有属性的值。模板B称为模板A的父模板。模板A称为模板B的子模板。就我目前的观察来看,SC2模板的继承是单继承,即一个模板只能有一个父模板。但父模板也可以有父模板,继承层数不限。假如模板A中给一个属性进行了赋值,模板B继承了模板A,但没有改写该属性的值,模板C又继承了模板B,也没有改写该属性的值,那么模板C中该属性的值就是模板A中该属性的值。
然后是属性的覆盖:

要注意追加和覆盖这两个概念跟上面继承的概念并不同级,继承是就模板之间来说的,我们可以说模板A继承了模板B。但是SC2的XML语法中并没有写法可以让模板A"覆盖"模板B(至少我还没见过)。因此覆盖和追加都是属性级别的。另外,覆盖是对模板自身来说的假如模板A继承了模板,而且模板B已经对其中一个属性X进行过赋值。那么首先模板A中的属性X的值就默认已经是模板B中属性X的值了。然后如果模板A对属性X重新赋值,那么称模板A覆盖(也可以叫改写)了属性X,而不是覆盖了模板B。为什么要说到这么详细呢?

因为在下面的语法部分中你会发现一个真意义上的"模板对自己进行覆盖"的例子。


注意这样的情况不称为覆盖(改写):

在模板A中对一个属性进行了赋值,但其任何一级父模板都没有对该属性赋过值。那么这种写法不称为覆盖,仅仅称为对该属性进行了赋值。注意赋值包括覆盖。但两者并不等同。
再就是属性的追加:

我上面不是说继承只能继承属性的值么?怎么属性又可以追加了呢?

实际上我上面并没有说属性完全不能自定义,只是有很多限制条件而已。

一个最显然的例子是不限尺寸的属性数组。比如单位模板中的AbilArray。如果所有模板的所有属性个数全部都是限制死的,那我们首先就无法给单位添加更多技能了。

因此至少属性数组是可以扩展的,我们把扩展属性数组的行为称作追加。我不知道是否有办法对属性数组以外的属性进行追加,迄今为止的尝试都失败了。但也许可以做到。因此如果以后能做到的话,那么这个定义就要改一改,不再限于属性数组了。

注意追加和对已有的属性赋值有时候可能会混淆。我这样定义区别他们的方法。即该属性此前是否存在于当前模板中。那么如何区分一个属性是否存在于当前模板中呢?这可不是通过观察XML就能简单断定的了。就像我上面说的,就算某个单位模板(以及其所有的祖先模板)中没有显式地写出生命值这个属性,单位肯定还是有生命值这个属性。最简单的方案是用CatalogFieldValueGet()这个函数来判断,返回null就是根本没这个属性,返回""或者具体值则代表有。因此id、default、parent并不是模板的属性。因为CatalogFieldValueGet()取不到它们。它们只能是传统XML定义中的"属性"。

关于该函数的教程:
http://bbs.islga.org/read-htm-tid-38662.html

注意,某些不符合规范的属性就算写进了模板里也不能算有,只要CatalogFieldValueGet()返回null就不算。


以下才是正文:SC2数据文件中模板继承、属性覆盖、属性追加的语法和规则部分。


显式继承



注意严格来说,default并不是一个模板属性,因为无法由CatalogFieldValueGet()获得它的值,它的值本身也无法被继承到子模板里去。我暂且将它和id、parent一道分类到具有特殊含义的模板关键字中去。(我已经修改了之前那个帖子,将id和default从属性这一类中去除)

如果一个模板A要继承另一个模板B,那么模板A需要将自身的parent关键字设为B的id.

如果要覆盖父模板的某个属性,直接重新赋值即可。

以下是一个例子


[codes=xml]
    <CAbilBuild default="1" id="TerranAddOns">
        <EditorCategories value="Race:Terran,AbilityorEffectType:Units"/>
        <Alert value="AddOnComplete"/>
        <Type value="AddOn"/>
        <FlagArray index="InstantPlacement" value="1"/>
        <FlagArray index="PeonDisableAbils" value="1"/>
        <FlagArray index="ShowProgress" value="1"/>
        <InfoArray index="Build1" Unit="TechLab" Time="25">
            <Resource index="Minerals" value="50"/>
            <Resource index="Vespene" value="50"/>
            <Button DefaultButtonFace="TechLabBarracks" State="Suppressed"/>
        </InfoArray>
        <InfoArray index="Build2" Unit="NuclearReactor" Time="25">
            <Resource index="Minerals" value="50"/>
            <Resource index="Vespene" value="50"/>
            <Button DefaultButtonFace="NuclearReactor" State="Restricted"/>
        </InfoArray>
    </CAbilBuild>
[/codes]


[codes=xml]
    <CAbilBuild id="BarracksAddOns" parent="TerranAddOns">
        <EditorCategories value="Race:Terran,AbilityorEffectType:Structures"/>
        <BuildMorphAbil value="BarracksLiftOff"/>
        <InfoArray index="Build1" Unit="BarracksTechLab">
            <Button DefaultButtonFace="BuildTechLabBarracks"/>
        </InfoArray>
        <InfoArray index="Build2" Unit="BarracksNuclearReactor"/>
    </CAbilBuild>
[/codes]

这两个模板之间,模板BarracksAddOns继承了模板TerranAddOns的所有属性。并将EditorCategories属性的值改写为"Race:Terran,AbilityorEffectType:Structures"。并对BuildMorphAbil属性进行了赋值。注意,这里BuildMorphAbil属性并非是由BarracksAddOns追加的,TerranAddOns本身也有该属性,它也不是从TerranAddOns的父模板继承而来(对,TerranAddOns也有父模板,下文会进行说明。)。而是CAbilBuild这个基础类本身就有BuildMorphAbil这一属性。实际上,如果尝试用CatalogFieldValueGet()来对TerranAddOns的BuildMorphAbil属性取值,得到的是""而不是null。这代表属性存在但未赋值。

如果你看过CatalogFieldValueGet()那篇文章的例子并且自己试过的话,会发现BarracksAddOns这个模板有Name这个属性,而且值为Abil/Name/BarracksAddOns。无论是BarracksAddOns和TerranAddOns都没有对这个属性进行过赋值,那这个值是怎么来的呢?游戏代码里写死的么?不是。下面会进一步展开。


默认模板:


我们将default=1的模板(即可被其余模板继承的模板)称为默认模板。默认模板对属性赋值时可以有一些特殊的语法。

比如用##id##来代表继承它的子模板的id。

如果一个技能模板中有这么一行:

<Name value="Abil/Name/##id##" />

那么继承它的所有模板的Name属性都会等于Abil/Name/模板自身的id,除非那些模板对Name的值进行了改写。如果BarracksAddOns继承了这样一个模板,它的Name属性默认就是Abil/Name/BarracksAddOns

这样就可以解释它Name属性的来历了,那么BarracksAddOns是否真的继承了这样一个技能模板呢,答案是,是的。


于是这里出现了一个新的语法:

基础类模板、元祖模板和隐式继承:



如果大家看过Core里的那些技能模板的话,会发现他们大多没有id....

没有id的模板有什么用呢,不能实例化啊,那么唯一的用途就是拿来被别人继承。

我们把default=1,但没有id的模板称为基础类模板。所有与该模板的基础类相同的模板(甚至包括那些基础类与它相同,default=1,但是有id的模板)都隐式地继承于它。

比如Core中存在以下模板。

[codes=xml]
    <CAbilBuild default="1">
        <HaltCmdButton DefaultButtonFace="Cancel"/>
        <Alert value="BuildComplete"/>
        <ErrorAlert value="Error"/>
        <FlagArray index="Cancelable" value="1"/>
        <FlagArray index="PeonMaintained" value="1"/>
        <FlagArray index="RangeIncludesBuilding" value="1"/>
        <RefundFraction>
            <Resource index="Minerals" value="-0.75"/>
            <Resource index="Vespene" value="-0.75"/>
            <Resource index="Terrazine" value="-0.75"/>
            <Resource index="Custom" value="-0.75"/>
            <Charge value="-1"/>
            <Cooldown value="-1"/>
        </RefundFraction>
    </CAbilBuild>
[/codes]

那么其余所有用<CAbilBuild>做基础类的模板,一律隐式地继承于这个模板。也就是是说BarracksAddOns和TerranAddOns的HaltCmdButton.DefaultButtonFace属性默认都为"Cancel",无需显式地进行继承声明。

嗳,等一下, <CAbilBuild default="1">这个模板里同样也没有定义Name这个属性啊,那么Name到底是哪里来的?难道 <CAbilBuild default="1">还继承了什么模板?

是的。
[codes=xml]
    <CAbil default="1">
        <Name value="Abil/Name/##id##"/>
        <EditorCategories value="Race:Neutral,AbilityorEffectType:Units"/>
        <TargetMessage value="Abil/TargetMessage/DefaultTargetMessage"/>
        <OrderArray>
            <Color value="255,0,255,0"/>
            <Model value="Assets\UI\Feedback\WayPointConfirm\WayPointConfirm.m3"/>
            <Scale value="0.75"/>
            <LineTexture value="Assets\Textures\WayPointLine.dds"/>
        </OrderArray>
    </CAbil>
[/codes]

Name属性终于出现了。它在<CAbil default="1">里。如果<CAbilBuild default="1">这种技能基础类模板是所有使用该基础类的模板的祖先模板的话,那么<CAbil default="1">就是所有技能基础类模板的祖先。

所有的技能模板默认地继承于<CAbil default="1">的模板,同理,所有的效果模板默认继承于<CEffect default="1">的模板,所有的行为基础类模板默认继承于<CBahovior default="1">的模板,以此类推……

我们把这些基础类模板的祖先模板称为元祖模板

元祖模板定义了所有技能、单位、行为、效果、物品、光、验证器等等的默认属性。


属性与属性数组中元素的追加和覆盖:


本节内容很重要!

其实刚才在讲继承的时候已经提到了属性的覆盖方式,但是本节特别要提的地方并非是继承后的属性覆盖,而是对自身已经赋值过的属性的覆盖,以及追加。

这里退回去普及一个非常重要的知识:

SC2的地图中XML数据文件读取的规则是以属性(对属性数组来说,细化到数组中的每个单独的元素)为覆盖粒度的,而不是以整个文件或是整个模板。

举例来说,如果我们想要重新定义BarracksAddOns这个技能,所有属性的值完全继承自TerranAddOns,不再自行修改,仅仅将Name属性设为"ABC",那么这样写是不行的:

[codes=xml]
    <CAbilBuild id="BarracksAddOns" parent="TerranAddOns">
        <Name value="ABC"/>
    </CAbilBuild>
[/codes]

如果我们直接将这个AbilData.xml按照MPQ里的路径压缩到地图里去,我们最后得到BarracksAddOns并不能完全如我们所愿。

虽然Name属性确实变成了ABC,但是尝试输出它的BuildMorphAbil属性的话,我们会发现:还是BarracksLiftOff...

这是因为SC2的数据文件读取优先级虽然仍然是:地图中的文件>游戏包外的文件>MPQ中的文件。但是地图中的游戏数据相对于MPQ文件的覆盖粒度都是属性级别而不是模板级别或文件级别。虽然我们地图中的BarracksAddOns技能的Name属性覆盖掉了MPQ中该技能的Name属性,但MPQ中该技能的其余属性依然会照样被读取。

所以如果我们想把其余的属性的值都去掉,只有将它们设为""才行。即用""来覆盖原有值。


这里尤其要注意属性数组中元素的覆盖和追加方式:



一般的模板的属性数组的每个元素都会标出自身的index,用以互相区分。但也有存在一些模板中,并没有显式的表示它们一些属性数组的index。最常见的例子就是单位模板中的AbilArray属性数组,代表了该单位有哪些技能。

[codes=xml]
    <CUnit id="PointDefenseDrone">
        <Race value="Terr"/>
        <Mob value="Multiplayer"/>
        <PlaneArray index="Air" value="1"/>
        <Collide index="Flying" value="1"/>
        <Attributes index="Light" value="1"/>
        <Attributes index="Mechanical" value="1"/>
        <FlagArray index="NoPortraitTalk" value="1"/>
        <FlagArray index="ArmorDisabledWhileConstructing" value="1"/>
        <LifeStart value="50"/>
        <LifeMax value="50"/>
        <LifeArmorName value="Unit/LifeArmorName/TerranShipPlating"/>
        <EnergyStart value="200"/>
        <EnergyMax value="200"/>
        <EnergyRegenRate value="1"/>
        <Mover value="Fly"/>
        <Sight value="7"/>
        <Height value="3"/>
        <VisionHeight value="4"/>
        <CostResource index="Minerals" value="100"/>
        <RepairTime value="33.3332"/>
        <AttackTargetPriority value="20"/>
        <AbilArray Link="attack"/>
        <AbilArray Link="stop"/>
        <CardLayouts>
            <LayoutButtons Face="PointDefense" Type="Passive" AbilCmd="255,255" Row="1" Column="0"/>
        </CardLayouts>
        <Radius value="0.625"/>
        <SeparationRadius value="0.6"/>
        <ScoreMake value="50"/>
        <ScoreKill value="100"/>
        <SubgroupPriority value="6"/>
        <MinimapRadius value="0.6"/>
        <EditorCategories value="ObjectType:Unit,ObjectFamily:Melee"/>
        <GlossaryPriority value="250"/>
        <GlossaryCategory value="Unit/Category/TerranUnits"/>
        <WeaponArray Link="PointDefenseLaser" Turret="PointDefenseDrone"/>
        <KillDisplay value="Never"/>
    </CUnit>
[/codes]

观察其AbilArray属性可以得知它有2个技能,攻击和停止。对于这个单位我想进行一些修改,不打算更改其他任何属性,单纯地把两个技能换掉(注意在实际操作中替换或是添加技能并不是改了AbilArray就了事的,还得修改按钮,但是这里目的只是修改AbilArray属性,就简化掉了)。

[codes=xml]
    <CUnit id="PointDefenseDrone">
        <AbilArray Link="Explode"/>
        <AbilArray Link="FleetBeaconResearch"/>
    </CUnit>
[/codes]

这里随便写了2个技能,但是指望用这个写法来替换掉单位所拥有的技能是行不通的。进入游戏后用CatalogFieldValueGet()来检查的话,会发现AbilArray[0].Link和AbilArray[1].Link还是攻击和停止……AbilArray[3].Link和AbilArray[4].Link才是我们写的技能。

因此如果在地图中不写属性数组的索引,得到的结果就是属性数组这些元素被追加到原有数组元素的后面去了。因此必须显式地写出索引。

正确的写法如下

[codes=xml]
    <CUnit id="PointDefenseDrone">
        <AbilArray index="0" Link="Explode"/>
        <AbilArray index="1" Link="FleetBeaconResearch"/>
    </CUnit>
[/codes]

道理和我在那个CatalogFieldValueGet()用法帖里写的一样,虽然MPQ中PointDefenseDrone的模板的AbilArray元素没有写索引,但是它们默认是以0 1  2 3 4 5....的顺序被赋予索引的。因此我们要覆盖它们时,只需要写出索引即可。

我在19楼补上了图,用的例子是机枪兵。

另一点要注意的是:某些模板的属性数组虽然有写出索引,但可能不是以整数而是以字符串来做index的,比如<Resource index="Minerals" value="-1" /> 这种但是实际上按照它们的读取顺序,用index=0,1,2,3,4为索引来将它们覆盖也同样行得通。但是实际操作中建议还是用它们的字符串索引来覆盖它们的值比较方便和安全。





以上这些就是SC2数据文件读取、覆盖、追加、继承的通用规则。
发表于 2010-3-4 19:43:29 | 显示全部楼层
绝对第一膜拜!
回复

使用道具 举报

发表于 2010-3-4 19:50:17 | 显示全部楼层
感觉讲的比实际的复杂
前面的悬念铺垫太长...

下面这段要不要加粗?
“这是因为SC2的数据文件读取优先顺序虽然仍然是:地图中的文件>游戏包外的文件>补丁MPQ中的文件>Liberty.SC2Mod的MPQ中的文件>Core.SC2Mod的MPQ中的文件。但是每一级的覆盖粒度都是属性级别而不是模板级别或文件级别。”
回复

使用道具 举报

 楼主| 发表于 2010-3-4 20:00:30 | 显示全部楼层
hmmm我讲得比实际简单多了。

我这些只是基本规则,具体到每个模板还有很多不同。
回复

使用道具 举报

发表于 2010-3-4 20:01:07 | 显示全部楼层
好长,终于等到了~
回复

使用道具 举报

发表于 2010-3-4 20:06:14 | 显示全部楼层
摸摸头目
回复

使用道具 举报

发表于 2010-3-4 20:20:09 | 显示全部楼层
头目总是处于领先地位,膜拜……
回复

使用道具 举报

发表于 2010-3-4 20:20:30 | 显示全部楼层
感謝之情無法言表。
回复

使用道具 举报

发表于 2010-3-4 21:07:56 | 显示全部楼层
SC2的数据文件读取优先顺序虽然仍然是:地图中的文件>游戏包外的文件>补丁MPQ中的文件>Liberty.SC2Mod的MPQ中的文件>Core.SC2Mod的MPQ中的文件。
这一点我同意
但是每一级的覆盖粒度都是属性级别而不是模板级别或文件级别
如果这句话是承接在上面那个优先级后面,我有点不同的意见。

我认为在“游戏包外的文件>补丁MPQ中的文件>Liberty.SC2Mod的MPQ中的文件>Core.SC2Mod的MPQ中的文件”这个优先级中,数据文件是不存在属性的覆盖的,只要找到了优先级高的数据文件,就不再载入低优先级的数据文件。例如在游戏包外找到了 UnitData.xml,就不再载入补丁MPQ中的文件。否则按照楼主在覆盖和追加一节中所说的,如果载入了多个 UnitData.xml,就会造成属性数组追加的问题。

如果我上面的理解没错的话,“地图中的文件->游戏包外的文件->...”这个优先级中,“地图中的文件”也应该是完整的 xml,因为它处于优先级的顶端,载入了“地图中的文件”就不再载入低优先级的文件了。

但是换个角度来想,例如以后地图编辑器要实现重写(override)某个对象的属性,总不能把整个 xml 都包含到地图文件中,而是应该只覆盖某些属性。而这一点又与上面提到的数据文件载入优先级冲突了,这正是我想不通的地方。
回复

使用道具 举报

发表于 2010-3-4 21:10:02 | 显示全部楼层
目前UnitData.xml放外面 重新开始游戏 会出错的吧(仅测试了破解版)
回复

使用道具 举报

 楼主| 发表于 2010-3-4 21:17:15 | 显示全部楼层
引用第8楼et2010于2010-03-04 21:07发表的  :

如果这句话是承接在上面那个优先级后面,我有点不同的意见。

我认为在“游戏包外的文件>补丁MPQ中的文件>Liberty.SC2Mod的MPQ中的文件>Core.SC2Mod的MPQ中的文件”这个优先级中,数据文件是不存在属性的覆盖的,只要找到了优先级高的数据文件,就不再载入低优先级的数据文件。例如在游戏包外找到了 UnitData.xml,就不再载入补丁MPQ中的文件。否则按照楼主在覆盖和追加一节中所说的,如果载入了多个 UnitData.xml,就会造成属性数组追加的问题。

.......

你理解错了。

这里的优先级是指覆盖时的优先级而不是读取时的顺序。优先级最低的文件反而是最先读取,这样后面的xml文件中的属性值才能覆盖前面文件中的,这就是它们的高优先级。
回复

使用道具 举报

 楼主| 发表于 2010-3-4 21:19:08 | 显示全部楼层
实际上你试试看就好了,把GameData和它文件夹里的文件都放到根目录下,进游戏造兵肯定是一造3个,然后你注意一下,技能的提示文本,会发现一个很神奇的变化。
回复

使用道具 举报

发表于 2010-3-4 21:25:28 | 显示全部楼层
引用第10楼麦德三世于2010-03-04 21:17发表的  :


你理解错了。

这里的优先级是指覆盖时的优先级而不是读取时的顺序。优先级最低的文件反而是最先读取,这样后面的xml文件中的属性值才能覆盖前面文件中的,这就是它们的高优先级。

嗯,这样就说得通了,我是被“高优先级”和那个箭头误导了。
回复

使用道具 举报

 楼主| 发表于 2010-3-4 21:46:33 | 显示全部楼层
e。等一下,放在地图里是属性覆盖,但放在包外的情况不同,我重新试一下。
回复

使用道具 举报

发表于 2010-3-4 21:52:48 | 显示全部楼层
頭目辛苦。
回复

使用道具 举报

发表于 2010-3-4 21:54:40 | 显示全部楼层
引用第13楼麦德三世于2010-03-04 21:46发表的  :
e。等一下,放在地图里是属性覆盖,但放在包外的情况不同,我重新试一下。

我也正在测试,保外好像的确不是属性覆盖。
回复

使用道具 举报

 楼主| 发表于 2010-3-4 22:51:24 | 显示全部楼层
包外的情况似乎有些复杂,怀疑部分原因是因为bug……

于是先把原文改成仅限地图中。
回复

使用道具 举报

发表于 2010-3-4 22:57:23 | 显示全部楼层
这个确实很恼火,顺带一提,目前的很多AI,与电脑对战的录像无法查看,有什么办法可以让这些AI同步呢?
回复

使用道具 举报

发表于 2010-3-4 23:16:51 | 显示全部楼层

回 17楼(keakon) 的帖子

看录像的办法很简单。方法如下:
先定位到Cache文件夹,应该知道的吧。
路径为C:\\Documents and Settings\\USERNAME\\Local Settings\\Application Data\\Blizzard Entertainment\\Battle.net\\Cache
里面有乱七八糟的很多文件夹,新建一个名为00文件夹,再在00里面新建一个名为11文件夹
把你自己的地图名字改为0011开头,比如0011test.s2ma,放在11文件夹里面。
然后用Lazy Loader启动地图,打完后就可以看录像了。

主要是保证前缀跟2个文件夹名一致就可以了。
回复

使用道具 举报

 楼主| 发表于 2010-3-4 23:44:29 | 显示全部楼层
这里顺便附个图片。

在地图里写
[codes=xml]
    <CUnit id="Marine">
        <AbilArray Link="Explode"/>
        <AbilArray Link="FleetBeaconResearch"/>
    </CUnit>
[/codes]

的话,用脚本输出Hello World,然后输出它的第一到第六个技能结果是

1.jpg

显然,这两个元素被追加到属性数组里去了。

而写了index的话

[codes=xml]
    <CUnit id="Marine">
        <AbilArray index="0" Link="Explode"/>
        <AbilArray index="1" Link="FleetBeaconResearch"/>
    </CUnit>
[/codes]

结果就变成这样,头两个技能被覆盖掉了。顺便,由于第五第六个技能由于不再存在,会有错误提示。

2.jpg
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 点一下

本版积分规则

Archiver|移动端|小黑屋|地精研究院

GMT+8, 2024-12-22 22:03 , Processed in 0.088028 second(s), 21 queries .

Powered by Discuz! X3.5

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表