解包分析《艾尔登法环》中的BOSS AI设计(快慢刀篇)

分享到社交媒体

魂系作品中的“快慢刀”一直以来都是系列BOSS的特色之一,有些人觉得刺激过瘾,有些人则大呼老贼阴险。然而不论如何,这种体验都是Soul-Like游戏中不可分割的一部分。

在《艾尔登法环》中,有些系列老玩家发现“快慢刀”好像有些不一样了。在【读指令篇】的评论中,也有一些同学提到了对法环中“快慢刀”的困惑:

(读指令篇:对面会更惨:解包分析《艾尔登法环》中的BOSS AI设计(读指令篇)

解包一下快慢刀吧,这个慢刀为啥总是能砍中我,竖劈的判定时间应该是非常短的,但我无论是滚快还是滚慢都经常吃到
希望可以把噩兆妖鬼的慢刀逻辑解包一下,看看他究竟是怎么做到慢得很或者干脆不砍了的

因此,本文将从“快慢刀”原理设计、解包动作数据、BOSS AI等方面,结合实际例子,看看《艾尔登法环》中的“快慢刀”是不是真的存在着一些问题。

(PS:解包文件是工程逆向的结果,不代表FS社员工真的在用这种逆天脚本写AI)


在讨论这个问题之前,我们需要先明确一下定义,即:

什么是快慢刀?

一般来讲,正常人的反应速度介于200ms~400ms之间,职业选手自然更快,视天赋不同会在100ms左右。年龄、精神状态、注意力集中程度等因素都可能会让这个数值产生波动。

反应速度分布

有兴趣也可以自己测一下:

humanbenchmark.com/test


所谓“快刀”,其实就是最速攻击,往往要求玩家在“第一时间”做出反应。但在实际设计上,即使是非常硬核的动作游戏,也很少会有把最速攻击的时间放在400ms以内的情况。这是因为游戏设计从来不应该以考察玩家生理能力水平为目标,如果一个最速攻击的攻击判定在200ms,可能有人每次都能躲掉,有人很难躲掉,有人永远躲不掉。这就已经不是玩游戏了,而是筛选新人类了。

因此,一般游戏的最速攻击判定时间往往会在500ms以上,一些网络游戏在考虑到延迟的情况下更是会远大于这个值。

《艾尔登法环》在这一点上做得还是非常不错的,大部分“快刀”的攻击判定生成都在700ms左右,极少数招式会在500ms+生成。

(当然了,王室幽魂、卢恩熊之类的东西自然是要排除在外的)

而所谓“慢刀”,则是远远慢于玩家反应速度的攻击,其考察的更多是对游戏的理解对情况的判断以及对BOSS的了解程度。在魂系游戏中,“慢刀”自起始动作到攻击判定生成往往需要1000ms~3000ms,极少数情况下会大于这一时长。这段时间可以支持玩家做出大部分的动作,甚至额外1~2次的翻滚,然而一旦判断失误,提前输入了某些动作,就会被“慢刀”直接命中。

《艾尔登法环》中的“慢刀”在玩家群里中产生了较大的争议,而在我实际看过一些解包文件后,也确实发现了一些问题,下面将结合一些具体例子和玩家的一些怨念点简单分析一下。


为什么慢刀总是很难躲?

这其实是一个复合型问题,结果由多个原因共同导致。首先我们需要了解的是:

《艾尔登法环》中的慢刀出招时间是不是都是定值?

绝大部分是的。

例如噩兆妖鬼的这一招,总是会在起手后81帧生成攻击判定。

并且,这一类慢速攻击往往不会掺杂额外的中断条件。简单来说就是一旦出手,除了进入处决状态、转阶段这种通用情况,都会在固定时间内把招式打完。

既然时长都是定值,那理论上不是应该很好躲吗?为什么实战中却不是这样呢?

这主要是由于以下3个原因:

  1. 每个BOSS都拥有多个不同时长的快、慢刀动作
  2. 某些不同出招的前十几帧刻意被处理成相似或相同的动作表现
  3. 有连招链,一些BOSS在特定情况下会连续出招

因此,虽然你可能完美的记住了每一个慢刀的感觉,但当它们混合在一起的时候,特别是快慢混合的时候,仍然是非常容易出错的(就像例题与应用题之间的关系一样)

在这一点上,《艾尔登法环》的做法我认为完全没有问题,因为这完全符合了慢刀的设计目的:

其考察的更多是对游戏的理解、对情况的判断以及对BOSS的了解程度


既然刚才说到绝大部分慢刀是定值出手,那非定值的是什么样子的呢?

经典的来了!

举拐棍

“怎么会事呢?”

我们先完整的看一下这招

 elseif arg1:HasSpecialEffectId(TARGET_SELF, 5027) then if arg1:IsInsideTargetCustom(TARGET_SELF, TARGET_ENE_0, AI_DIR_TYPE_F, 120, -1, 8) and f35_local4 < 60 then arg2:ClearSubGoal() arg2:AddSubGoal(GOAL_COMMON_ComboRepeat, 10, 3004 , TARGET_ENE_0, 0, 0, 0, 0, 0) return true

这一招是3004,它的触发有3个条件:

  1. 玛尔基特拥有5027效果
  2. 玩家位于其正前方120°,半径为8的扇形范围内
  3. f35_local4是前面声明的变量,一个1~100的随机数,与【读指令篇】中的用法一样,这里代表60%概率

先不管5027这个效果是用来干什么的,我们继续看动作

3004是一个总时长为6秒的超长动作。是的,你没想错,如果玛尔基特把这个动作全部做完了,就会发生最喜闻乐见的情况:举半天最后把拐杖放下来不打了

“我说停停,你不讲武德”

实际上,在3004招式释放期间,玛尔基特会做两件“特别”的事:

每两帧为自己附加一次5033效果
  • 在招式中1.4S~5.1S之间,每两帧,玛尔基特都会尝试为自己附加一次5033效果。
  • 在招式中1.4S起到这一招式完整结束,玛尔基特都具有JumpTable[0](23:End if AI ComboAttack Queued),这个的意思是,如果玛尔基特的连招列表当中被添加了其他招式,那么允许它在JumpTable覆盖的期间跳出当前招式(提前结束)

回到我们最爱的Interrupt()

 elseif arg1:HasSpecialEffectId(TARGET_SELF, 5033) then if arg1:IsInsideTargetCustom(TARGET_SELF, TARGET_ENE_0, AI_DIR_TYPE_F, 120, -1, 999) then arg2:ClearSubGoal() arg2:AddSubGoal(GOAL_COMMON_ComboRepeat, 10, 3020, TARGET_ENE_0, 0, 0, 0, 0, 0) return true end

在检测到自身有5033效果后,如果玩家位于其前方120°扇形的方向上(999在这里代表无限远),则清空招式列表,同时玛尔基特的连招列表中会被添加上3020招式

而3020则是举拐棍的唯一指定衔接动作

3020

(3020的前30帧是完美衔接举拐棍动作的)

同时,这也解释了玩家们争论的焦点:这一招是不是在读指令?

很多玩家认为这一招是否下劈,取决的玩家是否输入了翻滚或攻击指令;而其实它的逻辑非常单纯,只是判断了一下玩家是否在其前方而已。

简单梳理一下:

玛尔基特在释放举拐杖后会快速、多次的为自己添加5033效果,这么做的目的是保证AI可以快频率但又不是每帧去判断玩家的位置,一旦发现玩家位于其前方120°扇形区域,就会跳出举拐杖的动作,执行下劈攻击。如果在5.1秒里都没有检测到玩家位于前方,就会把3004招式完整播完,也就是举半天最后放下拐杖。


显然,最终的表现玩家们并不太认可

这并非仅仅是BOSS举着武器呆呆罚站6秒的问题,而是这6秒的时间里玩家也没办法做出正常的反馈行为,一般玩家的最优解可能就是陪它站6秒,这简直太糟了。

在玛尔基特举起拐杖后,玩家的决策大概可以分为3种:

  1. 保持移动让自己持续在BOSS身后,6秒后其招式结束
  2. 尝试砍它或者喝药,但由于BOSS一直在转向,如果你停了砍它一刀,它就会转到你正面,然后开劈
  3. 主动走到BOSS正前方,诱使其出招,自己翻滚/弹反/格挡后反击

不论哪一种都挺糟糕的,因为这一招式和玛尔基特会做出的其他行为已经完全不符了。

事实上,在《艾尔登法环》的“格调”下,这个问题有一个成本极低但有些弱智的解决方法,就是调整这一招式的转向速度。

在【读指令篇】中,我们看到熔炉骑士的某些招式在出招前拥有逆天的300转向速度,而举拐杖这一招是多少呢?我们来看下:

转向速度

(呵呵,可怜的100。那确实是转不过我们褪色者嗷)

有人可能会担心,你提了转向速度,BOSS跟个陀螺一样,表现能正常吗

这一点大可不必担心,玛尔基特本身的招式中,基本没有转向速度低于120的情况,而拥有220、240、280的转向速度的招式也有很多。这不仅仅是玛尔基特和熔炉骑士的问题,《艾尔登法环》中的大部分BOSS都拥有极高的招式转向速度以保证其能持续面向玩家。

至于这招为什么只有100,我只能说,可能是宫崎英高的怜悯吧

另外,上文中提到的举拐杖的判定条件之一,5027效果,其实是它的前置动作“匕首挥砍”中附加的。这一效果在玛尔基特的整个AI文件中仅在此招式中添加,因此,举拐杖的前置唯一指定招式只会是“匕首挥砍”。

匕首挥砍

很多人会说自己在通关过程中基本没见过举拐杖这一招,这也是正常的,因为它的触发几率确实非常低。反编译的脚本中,位于前面的判定比后面的判定优先级更高,而触发举拐杖的招式前还有不少内容:

 if arg1:IsInterupt(INTERUPT_ActivateSpecialEffect) then arg1:SetNumber(5, 1) arg1:SetNumber(8, arg1:GetNumber(8) + 1) if arg1:GetNumber(8) >= 4 then arg1:SetNumber(8, 0) arg1:SetNumber(7, arg1:GetNumber(7) + 1) end if arg1:HasSpecialEffectId(TARGET_SELF, 5025) then if arg1:IsInsideTargetCustom(TARGET_SELF, TARGET_ENE_0, AI_DIR_TYPE_F, 160, -1, 5.5 + arg1:GetMapHitRadius(TARGET_SELF)) then arg2:ClearSubGoal() if arg1:IsInsideTargetCustom(TARGET_SELF, TARGET_ENE_0, AI_DIR_TYPE_F, 120, -1, 3.5 + arg1:GetMapHitRadius(TARGET_SELF)) and f35_local4 < 60 then arg2:AddSubGoal(GOAL_COMMON_ComboRepeat, 10, 3003, TARGET_ENE_0, 0, 0, 0, 0, 0) else arg2:AddSubGoal(GOAL_COMMON_ComboRepeat, 10, 3001, TARGET_ENE_0, 0, 0, 0, 0, 0) end return true elseif arg1:IsInsideTargetCustom(TARGET_SELF, TARGET_ENE_0, AI_DIR_TYPE_F, 360, -1, 8) and arg1:GetNumber(1) < 1 and arg1:HasSpecialEffectId(TARGET_SELF, 16200) then arg1:SetNumber(1, arg1:GetNumber(1) + 1) arg2:ClearSubGoal() arg2:AddSubGoal(GOAL_COMMON_ComboRepeat, 10, 3013, TARGET_ENE_0, 0, 0, 0, 0, 0) return true end elseif arg1:HasSpecialEffectId(TARGET_SELF, 5026) then if arg1:IsInsideTargetCustom(TARGET_SELF, TARGET_ENE_0, AI_DIR_TYPE_F, 120, -1, 10.5) and f35_local3 > 5.5 then arg2:ClearSubGoal() arg2:AddSubGoal(GOAL_COMMON_ComboRepeat, 10, 3018, TARGET_ENE_0, 0, 0, 0, 0, 0) return true elseif arg1:IsInsideTargetCustom(TARGET_SELF, TARGET_ENE_0, AI_DIR_TYPE_F, 160, -1, 6) then arg2:ClearSubGoal() arg2:AddSubGoal(GOAL_COMMON_ComboRepeat, 10, 3005, TARGET_ENE_0, 0, 0, 0, 0, 0) return true elseif arg1:IsInsideTargetCustom(TARGET_SELF, TARGET_ENE_0, AI_DIR_TYPE_F, 360, -1, 8) and arg1:GetNumber(1) < 1 then if arg1:HasSpecialEffectId(TARGET_SELF, 16200) then arg1:SetNumber(1, arg1:GetNumber(1) + 1) arg2:ClearSubGoal() arg2:AddSubGoal(GOAL_COMMON_ComboRepeat, 10, 3010, TARGET_ENE_0, 0, 0, 0, 0, 0) return true end elseif arg1:IsInsideTargetCustom(TARGET_SELF, TARGET_ENE_0, AI_DIR_TYPE_F, 120, -1, 40) and f35_local3 >= 7.5 and arg1:GetNumber(2) < 1 then arg1:SetNumber(2, 2 + 1) arg2:ClearSubGoal() arg2:AddSubGoal(GOAL_COMMON_ComboRepeat, 10, 3017, TARGET_ENE_0, 0, 0, 0, 0, 0) return true end elseif arg1:HasSpecialEffectId(TARGET_SELF, 5027) then if arg1:IsInsideTargetCustom(TARGET_SELF, TARGET_ENE_0, AI_DIR_TYPE_F, 120, -1, 8) and f35_local4 < 60 then arg2:ClearSubGoal() arg2:AddSubGoal(GOAL_COMMON_ComboRepeat, 10, 3004 , TARGET_ENE_0, 0, 0, 0, 0, 0) return true

更不用说即便真走到了判断5027效果这里,还有一个40%的不执行概率。因此,在打玛尔基特的过程中没见过这招也是正常的,不是BUG。


接下来,我们再看看慢刀的第二个问题:攻击判定

典中典之“打逆”

类似的情况大家应该多少都在游戏中碰到过,只不过在不慢放的前提下往往很难发现这可能是BOSS的问题,只会单纯感觉有一些奇怪,或者疑惑自己为什么被打中了。

攻击判定生成

这里我截取了蒙葛特此招式攻击判定生成的一帧,可以看到在锤子仍位于身后时攻击判定就已经生成了。结果自然是玩家站在蒙葛特身后也会被打飞。

一般而言,动作游戏中一次出招可以分为前摇、攻击、后摇三个部分(最简单的情况),而伤害判定则只会在中间的攻击部分存在。这是非常合理的,前摇往往意味着招式的抬手,它是给予玩家的预警,也是一个连贯动作发力的起始,往往动作速度稍慢。在这一阶段附加伤害判定是不合理的:从表现上来讲,动作本身并没有发力到最大速度;从设计上来说,进一步压缩了玩家的反应时间,并带来了很多奇怪的问题。

我绝对饶不了你!

在《艾尔登法环》中,并不止有蒙葛特存在这样的问题,很多BOSS的攻击判定生成时机攻击判定的范围,都存在着严重问题。

老将尼奥

比如老将尼奥这招快速突进的劈砍,攻击判定生成时武器还完完全全在身后,并且判定的位置和突进的夹角达到了180°。这并不是一个“旋风斩”类的技能,但玩家站在他背后会被妥妥的打飞。得益于《艾尔登法环》中BOSS夸张的转向速度,单人游戏时你可能不太会发现这种问题,然而一旦召唤骨灰或是进入多人游戏,这就会变成一个大概率事件了。

这类攻击判定的问题不单单导致了BOSS攻击行为的不直觉和玩家难以躲避,还产生了另一个严重问题:

上图中我们需要关心的内容只有两个

  • InvokeAttackBehavior:生成攻击判定
  • JumpTable[0](5:InvokeParriedState(ArgC):玩家弹反窗口

可以看出,玩家的弹反窗口的生效时间,比攻击判定的生成慢了5帧

理论上,如果所有这类攻击都是从BOSS脑袋后边开始挥,那最终结果可能“负负得正”了,因为这样虽然攻击判定早就生成了,但5帧时间正好挥到差不多BOSS前方,玩家可以弹反。然而实际上,很多直接从BOSS面前抬手的招式,其弹反窗口也慢于攻击判定的生成。

可能有人会产生质疑

你懂不懂魂like啊,我们硬核游戏就是这样子的,弹反就是很难的!

正常来说,攻击判定生成和弹反窗口的时机应该是一致的,以往的魂系游戏,黑魂、只狼也确实是这么做的(我去看了),甚至是《艾尔登法环》中相当一部分BOSS,也是正常的

攻击判定生成和弹反窗口的时机一致

那攻击判定比弹反窗口快会存在什么问题呢?

如果你贴BOSS很近,那这类本来能弹反的招式,你永远弹反不到,因为在你弹之前,已经被打到了。

除了蒙葛特、老将尼奥以外,像拉达冈红狼(不咬刀时)、卢恩熊、王室幽魂之类的怪也都属于攻击判定问题的“重灾区”

“张嘴即判定”

另一个慢刀相关问题的原因其实和慢刀本身并不相关,但在快节奏的战斗中,往往也会被认为是慢刀带来的问题。

我们来看一个实际例子:

非慢放正常视角

提问,图里的玩家是怎么死的?

如果你看了本文前边的内容,此处大概率会这样认为

蒙葛特这招前戳攻击判定做得“不干净”,在收招阶段仍然留有攻击判定,所以虽然翻滚躲过了出招时的判定,但仍然被收刀时的碰撞框碰到了

这思路完全没错,但事实真的是这样吗?我们不妨在慢动作里看看:

慢放视角

事实上,即便慢放我们也很难推翻【收招攻击判定蹭到致死】的这一推论。然而这个问题的原因却比这个单纯很多,它仅仅是因为蒙葛特出招后的这个左侧跳跃招式具有攻击判定

侧跳的攻击判定

而由于蒙葛特自身AI的原因,它会经常性的释放侧跳:

function Morgott213000_Act47(arg0, arg1, arg2) arg0:SetNumber(7, arg0:GetNumber(7) + 2) if arg0:IsInsideTarget(TARGET_ENE_0, AI_DIR_TYPE_L, 180) then local f31_local0 = 4 local f31_local1 = 6003 local f31_local2 = TARGET_ENE_0 local f31_local3 = 0 local f31_local4 = AI_DIR_TYPE_R local f31_local5 = 0 arg1:AddSubGoal(GOAL_COMMON_SpinStep, f31_local0, f31_local1, f31_local2, f31_local3, f31_local4, f31_local5) GetWellSpace_Odds = 0 return GetWellSpace_Odds else local f31_local0 = arg0:IsInsideTarget(TARGET_ENE_0, AI_DIR_TYPE_R, 180) if f31_local0 then f31_local0 = 4 local f31_local1 = 6002 local f31_local2 = TARGET_ENE_0 local f31_local3 = 0 local f31_local4 = AI_DIR_TYPE_L local f31_local5 = 0 arg1:AddSubGoal(GOAL_COMMON_SpinStep, f31_local0, f31_local1, f31_local2, f31_local3, f31_local4, f31_local5) GetWellSpace_Odds = 0 return GetWellSpace_Odds end end end

由于相关内容比较零散,这里就不全贴了,总结起来就是:

  1. 如果玩家没有位于蒙葛特正前方120°时,左右横跳会具有相当的权重
  2. 在释放完部分攻击招式后,蒙葛特会根据玩家的方位(偏左/偏右),概率性进行左/右侧跳

因此,玩家一般很难直观判断出自己究竟是被攻击招式命中了还是被侧跳蹭到了(最可行的办法则是通过掉血来判断,侧跳的伤害肯定比一般攻击要低)

但是,无论判断与否,对玩家来说都是难以接受的体验。玩家好不容易凭实力躲过了快慢刀,却要被不知道什么时候会出来的无前摇侧跳命中造成硬直,打乱自己良好的立回节奏。

这里的问题就在于

为什么侧跳要有攻击判定?

这种设计在怪物猎人中很常见,怪物猎人当中的大部分龙在进行原地转向、前冲、侧移等行为的时候,脚下都会带有攻击判定,很少会有玩家认为这是不对的。我认为最基本的原因在于两点:

  • 合理性

当游戏中没有设定某些内容时,玩家会自然而然地从客观现实中代入自己的认知。怪物猎人中并没有明确指出哪样的攻击是会造成伤害的,哪样是不会的;而龙本身的体型都很大,远远大于玩家的模型,因此从现实世界的客观规律上来说,玩家如果在其移动时位于脚下,确实是会受到影响。就像没有任何人会质疑为什么站在移动神庙脚边会受到伤害一样。而像蒙葛特这种BOSS,本质上是人型,虽然模型比玩家大了一些,但侧跳的动作非常轻盈,很难联想到这种动作会造成伤害(更不用说你向侧面跳却能打到前面的人这种事了)

  • 统一性

如果一种动作会造成伤害,那与其相似或同类的其他动作也应该造成伤害,这样才能给玩家带来统一、连贯的认知。在《艾尔登法环》种,蒙葛特的侧移有伤害判定,但是几乎相同模型大小的很多人型BOSS却没有,这种差异使得玩家的认知产生割裂,自然就会带来困惑与质疑


总结一下

  • “定长”快慢刀在设计与实现上完全没有问题,在游戏当中也发挥了应有的作用,与历代FS社作品并无太大差异
  • “非定长”慢刀在设计上不存在问题,一定程度上丰富了BOSS的战斗体验,并且在整个游戏中出现的次数并不多,战斗仍然以“定长”快慢刀为核心。但在实现上,部分招式在AI中考虑的情况过于单一,最终导致表现出了问题
  • 攻击判定的范围与攻击判定的生成时机是《艾尔登法环》中部分BOSS的严重问题。如果说范围的大小是出于战斗体验变化的改革还勉强说得通的话,那同样的异形状武器在血缘诅咒中通过多个判定框去贴合,让判定范围与实际模型尽可能保持一致,而在《艾尔登法环》中直接一个大圆柱体了事的行为,我只能判定为“偷懒”判定生成时机的问题更像是在游戏发售的前夕,一种自上而下的意志导致大部分BOSS的攻击判定时机被修改了,因为弹反窗口的时机都是正确且精准的,所以我很难认为这是实际制作者“不会做”而导致的。这种自上而下的意志可能是骨灰在游戏制作的末期才被决定加入游戏、亦或是两次线上测试中的数据提示出BOSS的强度需要调整但时间又来不及了之类的(我随便说的)
  • 侧跳有攻击判定这一点,从合理上我不做太多的评价,但从统一性上来讲《艾尔登法环》是存在问题的

整体看下来,很多玩家觉得有问题、觉得奇怪的点,实际上确实是存在问题的。特别是很多“系列老玩家”,他们可以很明显的感觉到《艾尔登法环》的一些地方和之前的作品是有所不同的。革新与优化是大家一致支持的,然而一旦发现“退步”或“偷懒”,不论是因为工期、内容量还是什么别的原因,还是要坚定的指出来的。

快慢刀的这些问题,本质上“并不太”影响玩家正常游玩。就像大部分BOSSAI在中、近距上都有着良好的体验,但有些BOSS在较远距离的情况下会变成“复读机”或者“读指令怪”一样,上述快慢刀问题会在部分BOSS的个别招式中出现,概率低,且拥有一定的容错率,很少会直接致死

假如你从初见一个BOSS到击败它需要死20次,那本文中的一些问题也许会把这个数字改写为22。相信广大的褪色者们一定可以_________的吧!

艾尔登法环boss

来源:知乎 www.zhihu.com
作者:对面会更惨

摸鱼头条


分享到社交媒体
简体中文繁體中文English日本語한국어