|
由于下周就要发布1.2.0正式版了,所以我就在这里写一下这个帖子吧。
其实我上个月就在BN论坛上发过了,不过由于这几个月一直没时间竟然忘了在老巢里也发一下。
简单地说,在ptr1.2.0 patch3里,blz加入了几个mod的文件碎片。其中就有玻璃渣倒塌的MOD。注意这个MOD是指模组,也就是像战役mod一样供人引用的资源,本身并非一张地图。这个mod里做好了blizzard dota里一些最基础的系统,比如暴击,击晕,属性加成系统等等。还包括几个触发技能。
当然,由于ptr只是测试版,地图本身也没有出现,所以里头的mod是不完整的,其实只包括了一个不完整触发器库文件,里头没有TriggerString.txt也没有Identifier。不过我花了时间补完了重建了这个触发库。通过分析这个库文件我们能大致了解Blizzard Dota的基础系统。
Blizzard Dota一个不错的地方是它完全用触发器来控制攻击力防御力武器伤害移动速度攻击速度还有致命一击几率。因此可以极为自由地由触发器全面操纵这些数值。由于它使用的加成方式是CatalogFieldValueSet(),所以完全没有绿字加成的要素,全部都是黄字加成,这样触发器就可以直接设置攻击力了。而不需要通过添加buff的方式来增加绿字攻击力。
但是这个基本系统里也存在不少的问题。下面随便列举两个,是希望大家能引以为戒的。而我之前把这些发到BN论坛也是为了让BLZ注意一下,好在以后的版本中改进。毕竟这些问题作为地图来说根本算不上问题,但是关键是这些是做在官方的公用MOD里的,是和战役mod相同等级的东西,显然会有大量地图制作者引用这个库,要是因此造成别的地图制作者在使用这个库制作地图时的问题那可就相当不妥。如果大家想要做公用mod,就首先要记住非常重要的一个前提就是泛用性问题。
以下进入正题
注意:由于这个触发是我重建过来的,所以变量名和触发名可能和以后发布的正式版有所不同,不过不同之处仅限于名字。触发本身是不会变的。当然除非暴雪看到了我发在BN的那个帖子然后修正了里面的问题,hmmm。
糟糕的例子1:致命一击的实现
[trigger]
AttackEffects
Events
Unit - Any Unit is attacked
Local Variables
HeroIndex = 0 <Integer>
i = 0 <Integer>
Count = 0 <Integer>
Conditions
Actions
General - For each integer HeroIndex from 1 to MaxHero with increment 1, do (Actions)
Actions
General - If (Conditions) then do (Actions) else do (Actions)
If
And
Conditions
(Unit type of (Attacking Unit)) == Heros[HeroIndex]
CurrentCrits[HeroIndex] > 0
Then
Variable - Set i = (Random integer between 1 and 100)
General - If (Conditions) then do (Actions) else do (Actions)
If
And
Conditions
i <= CurrentCrits[HeroIndex]
((Triggering unit) Life (Current)) >= 1.0
Then
------- Crit! Double damage, run the same damage effect again.
Actor - Attach CritImpact (Unknown) to Overhead on (Triggering unit)
Environment - Execute CurrentEffects[HeroIndex] on (Triggering unit) from (Attacking Unit)
Else
Else
------- 以下部分和致命一击无关,就省略了。
[/trigger]
好了,很多同学看了这个触发器以后应该就能明白其原理,不过为了让大家都能理解,我还是详细说明下的好。
Heros[]:长度为30的数组,保存的是单位类型。在初始化时它会把玻璃渣倒塌的所有英雄的单位类型保存在里面。由于所有的英雄在地图上都是唯一的,所以这个数组其实同时可以用来给出英雄的编号。
CurrentCrits[]:长度为30的实数数组。它的下标和Heros[]的下标对应。保存的是对应英雄的当前致命一击几率。
CurrentEffects[]:长度为30的数组,保存的是效果类型。它的下标也和Heros[]的下标对应。记录对应英雄的武器伤害效果。
所以整个触发器的流程就是:
一个单位被攻击->枚举Heros[]里的所有英雄类型,找到攻击者的编号->获取攻击者的致命几率->然后Roll 1-100 看看是否致命一击->如果致命一击,而且目标大于1点血,那么就再对目标发动一次攻击者的武器伤害效果。
好了,这个流程里存在多个问题,其中最显然的一个就是:他们实现致命一击的方法其实就是再发动一次武器伤害。这是致命一击么?玩过wow的战士的人大概知道,这是wow里的剑专天赋,一定几率出1一次额外的武器伤害。wow是暴雪自己做的,他们应该比任何人都清楚两者的区别才对。暴击是一次伤害,而剑专是两次。
这毕竟是个官方mod。假设有人引用了这个mod来做地图,然后在自己的地图里做了个能抵消一次伤害的buff会怎样?显然,如果发动暴击,不管那个buff何时加上,最多只能抵消掉一半伤害。要么抵消掉附加的部分,要么抵消掉原伤害部分。反正怎么都会被伤害。
况且两次伤害分开算,那么目标的护甲也会被分开计算。假设攻击者攻击力为10,目标护甲2,那么致命一击应该造成18点伤害。但是用伤害2次的方式,总共的伤害就变成16点,护甲被多扣了一次。
其实还有另外一个问题,如果是用过WE的同学应该可以敏锐地发现:用的事件是“单位被攻击”。这个事件在war3编辑器里可谓臭名昭著,因为在war3里实际上单位被攻击事件发动后,你依然有一小段时间来狂按S停止这次攻击,但是触发却会执行下去,这样相当于你狂按S就能不断骗到攻击效果。
但是我说的问题却不是这个,在sc2编辑器里,其实单位被攻击事件和war3的单位被攻击事件并不等同,它其实是在单位的武器效果发动后抛出的。这时候你已经无法通过按S来停止这次攻击了。SC2另有一个“单位开始攻击”事件,这个事件才是开始攻击时抛出的。可以通过按S来取消。所以SC2的攻击检测系统相当于在war3的“开始攻击”和“受到伤害”之间插了一个阶段“武器效果发动”。用这个事件的话就不用担心攻击取消的bug。
所以问题不在于狂按S,而是在于这个事件抛出的时机本身。就像我上面说的,这个事件是在武器效果发动的瞬间抛出的,而不是“武器的伤害效果”发动的瞬间。对近战单位来说其实没啥区别,但是对远程单位来说却是相当麻烦的事情。以导弹塔为例的话,那就是导弹刚一射出就抛出事件。显然这时候你连导弹中没中都不知道,直接就给了目标一个伤害。
所以对远程单位来说,被“暴击”的那部分伤害竟然远在被攻击到之前……
举个极端的例子说的话,有个英雄残血了,不过它有一招瞬移技能,后面有个英雄在追他,对着他射了一箭。只见被追的英雄发动闪烁,完美地避开了那支箭——然后他挂掉了,恭喜呢。
还真是太糟糕了呢。
例子2:缝线的钩子
玩过倒塌的同学都记得憎恶钩子吧?简单地说这个技能就是,把目标拖拽到自己身边,然后造成伤害。
[trigger]
HookLv1
Events
Unit - Any Unit uses StitchesAbility2_1 at Effect3 - Cast stage (Ignore shared abilities)
Local Variables
Conditions
Actions
Variable - Set HookCaster = (Triggering unit)
Variable - Set HookTarget = (Triggering ability target unit)
Variable - Set HookBackwardLoc = ((Position of (Triggering unit)) offset by -2.0 towards (Position of (Triggering ability target unit)))
Variable - Set HookDamageEffect = StitchesAbility2_1Damage (Unknown)
Unit - Add 1 Hooked (Unknown) to (Triggering ability target unit) from (Triggering unit)
Unit - Make HookTarget Uncommandable
Unit - Change HookTarget height to 2.0 over 0.125 seconds
Variable - Set HookStep = 0
------- Remove burrow/levitate behaviors here
Trigger - Turn HookProgress On
[/trigger]
上面这个触发是在1级钩子使用时触发的动作,同理还有2级、3级、4级、5级,内容基本都一样,所以就只贴1级的好了。它们都同样会启用HookProgress这个触发,而HookProgress初始化时是关闭的:
[trigger]
HookProgress
Events
Timer - Every 0.0625 seconds of Game Time
Local Variables
Conditions
Actions
General - If (Conditions) then do (Actions) else do (Actions)
If
And
Conditions
HookTarget != No Unit
(HookTarget is alive) == true
Then
Unit - Make HookTarget face HookBackwardLoc over 0.0625 seconds
Unit - Move HookTarget instantly to ((Position of HookTarget) offset by 0.5 towards HookBackwardLoc) (Blend)
Variable - Modify HookStep: + 1
General - If (Conditions) then do (Actions) else do (Actions)
If
Or
Conditions
(Distance between (Position of HookTarget) and HookBackwardLoc) < 0.5
HookStep > 15
Then
Unit - Make HookCaster Commandable
Unit - Make HookTarget Commandable
Unit - Remove 1 Hooked (Unknown) from HookTarget
Unit - Change HookTarget height to 0.0 over 0.125 seconds
Environment - Execute HookDamageEffect on HookTarget from HookCaster
Trigger - Turn (Current trigger) Off
Else
Else
Unit - Make HookCaster Commandable
Unit - Make HookTarget Commandable
Unit - Remove 1 Hooked (Unknown) from HookTarget
Unit - Change HookTarget height to 0.0 over 0.125 seconds
Trigger - Turn (Current trigger) Off
[/trigger]
HookCaster:单位变量,保存钩子技能的施法者。
HookTarget:单位变量,保存钩子技能的目标。
HookBackwardLoc点变量,保存你想要把目标拽向哪里,基本上就是施法者施法时的位置了,但是为了有个缓冲带,所以加了2的距离。
HookDamageEffect效果类型变量,保存你把目标拽过来后想要对它造成的伤害效果。
HookStep整型变量,保存目前已经拖拽了多少进度,这个变量同时用来控制最大拖拽距离(15 x 0.5)。
注意:以上变量都是全局变量。
所以整个过程的第一部分是:
任意单位使用钩子技能->把施法者、施法目标、拖拽目标点、伤害效果保存到全局变量里->给目标添加一个buff“被钩住了”->令目标无法被控制->在0.125秒内将目标高度调整为2->将拖拽进度变量重置为0->启用HookProgress这个触发。
这些都只能算是准备工作,实际的拖拽是在HookProgress里实现的。
这个触发每0.0625秒做以下操作:
如果目标还活着,而且目标变量不是null,那么使用混合渲染(Blend)式移动法,将目标往目标点处移动0.5的距离。然后将Hookstep+1。
如果目标死了,那么直接取消以下所有操作,然后关闭本触发。
如果目标已经被拖到施法者身边(距离<0.5),或者目前已经拖到最大拖拽距离(15 x 0.5),那么灵施法者和目标都变回可控制状态,删除“被钩住”的debuff,然后用触发造成伤害,重设目标的高度,关闭本触发。
注:这里特别提一下0.0625秒和混合渲染移动的共同作用方式。就像我之前说过的,sc2的一个周期就是0.0625游戏秒,在0.0625秒的非整数倍时间段内没有任何会实际影响游戏进程的事件和动作发生。所以在触发器里,0,0625也是最短的时间间隔,当然其实通过循环等待0.0秒可以将最短间隔缩小到0.03125,不过其实这实际上是个bug。
而触发器里还有一个动作:移动单位。这个动作可以决定是使用立即移动还是使用混合渲染移动,当你使用混合渲染移动的话,目标的实际坐标虽然已经立刻被移动,但是它的视觉位置会在下一个游戏周期(0.0625秒)内从原位置平滑移动到目标点。所以好好利用这个动作,每0.0625秒移动一次单位的话,就算无法达到更短的间隔,也能做出非常平滑的移动来。玩弹幕的同学要特别注意一下这点。
题外话说够多来了,那么来说说这个钩子触发里存在的一些问题:
首先一点,实际上我完全没有发现用触发器来实现这个技能的必要性,里头究竟哪一部分是不能用数据编辑器和XML实现的呢?用施力效果来做拖拽明显简单美观许多,而且撞上障碍物还会自动停止,如果不想自动停止也可以用移动单位的效果。无论如何都比用触发器高效的多,而且更具移植性。这毕竟是一个MOD,我们需要尽量多的可移植性。
何况,就算我们必须用触发来制作这个技能,实际上还存在另一个问题:他们在一个需要等待\周期性事件的过程里使用全局变量。用过we的同学应该已经明白这个问题的严重性。
使用全局变量来制作这样的技能,其结果就是,有人在使用这个技能的时候,其他人都不可以使用,如果两个人一起用这个技能,那么触发就会互相冲突,使得触发技能产生完全错误的结果。
其实在这个倒塌库里的大部分系统都默认每一个玩家最多每种英雄拥有一个。比如说每个玩家最多能有一个大法师和一个山丘和一个圣骑士和一个XXXX……这是可以理解的。因为有些系统是用CatalogFieldValueSet()实现的,这个函数修改的是整个单位类型的属性。而某些修改是单纯用buff无法实现的,因此我们只能用CatalogFieldValueSet()。
但是限定每种英雄你只能有一个这个条件的苛刻程度和限定整个地图上都只有一个钩子是没法相比的。这和CatalogFieldValueSet()的情况不一样,我们并不是只能用全局变量。
我们只需要稍微修改下HookProgress这个触发器,把它从周期性触发的结构换成循环等待结构的函数,然后把那些需要的变量作为参数发送过去就可以了。SC2里等待和Timer的精度是一样的,所以不需要专门去用Timer结构而放弃Wait结构。
以上便是玻璃渣倒塌触发库里犯下的两个经典错误。实际上如果这些触发是写在地图里的,那一点问题都没有,关键是它们偏偏作为官方MOD来发布。这就显然造成了大问题。希望大家能从中吸取教训,并学到一些东西。 |
|