|
本文一些术语基于我的这篇规范文:
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数据文件读取、覆盖、追加、继承的通用规则。 |
|