解包分析《艾尔登法环》中的BOSS AI设计

解包分析《艾尔登法环》中的BOSS AI设计

解包分析《艾尔登法环》中的BOSS AI设计(读指令篇)

对面会更惨,游戏策划

自从《艾尔登法环》发售以来,读指令这三个字一直都是玩家们争论的焦点之一。不少离谱的游戏实况表现和玩家群体中快速传播的动图让很多人对法环的一些 BOSS 行为充满了困惑。

事实上,魂系列 AI 读指令早已不是秘密,《黑暗之魂 1》在读、《血源诅咒》在读、《只狼》也在读。那么抛开大量新进魂系新人玩家涌入的因素,为什么《艾尔登法环》最为人所诟病呢?

本文将通过解包,以 AI 文件、动画文件、各类参数等内容为基础,尝试分析法环中的读指令问题究竟出在哪。

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


那么,《艾尔登法环》真的读指令了吗?真读了。

 

随便打开一份 AI 的解包文件,我们就可以看到关于 Interrupt 的 Function


废话不多说,先请出新人折磨王:熔炉骑士 AI

IsInterupt()

我们从头来看

 if arg1:IsInterupt(INTERUPT_UseItem) and arg1:HasSpecialEffectId(TARGET_SELF, 5039) == false then

前边不需要太多的解释了,就是玩家使用道具。那这个 Effect Id 5039 是干什么的呢?

熔炉骑士动作

我们打开熔炉骑士的动画文件后能看到大部分动作中有存在"AddSpEffect 5039”,这个东西其实是代表了熔炉骑士出招过程中自身 AI 不会去做打断的一个时长。

比如上图中这个挥砍的动作总时长是 2.5S,而「5039」占了前 1.5S,也就是说从 AI 的角度上,熔炉骑士一定会挥完这 1.5 S 的剑才能去做别的事情。这是非常合理的,总不能我在神龙摆尾摆一半,看到玩家在喝药,我就突然中断动作上去给他一刀吧?

老贼显然没这么离谱。


这里就可以看出「药检」的触发条件了

  1. 读到玩家喝药的输入指令
  2. 自身并没有在其他的招式硬直阶段

如果这两个条件都满足,就准备「药检」了:

if arg1:IsInsideTargetCustom(TARGET_SELF, TARGET_ENE_0, AI_DIR_TYPE_F, 120, 180, 5) then
            if f23_local4 <= 80 then
                arg2:ClearSubGoal()
                arg2:AddSubGoal(GOAL_COMMON_ComboTunable_SuccessAngle180, 10, 3000, TARGET_ENE_0, 999, f23_local2, f23_local3, 0, 0)
                return true
            else
                return true

IsInsideTargetCustom 实际上是在检测熔炉骑士和玩家间的位置关系,AI_DIR_TYPE 一共有 4 种,分别是:

AI_DIR_TYPE_F:自身前方
AI_DIR_TYPE_B:自身后方
AI_DIR_TYPE_L:自身左侧
AI_DIR_TYPE_R:自身右侧

后边的参数则是角度和距离。

因此这里的前置条件就是【如果玩家在熔炉骑士前方 120°、半径 5 米的扇形区域内】

loacl4 是 Goal.Interrupt = function 中定义的变量,就不贴了。它是一个 1~100 间的随机数,所以<=80 就是「有 80% 的几率」

ClearSubGoal 是清空熔炉骑士 AI 中当前的行为列表,也就是说本来熔炉骑士本来寻思着准备上天了但还没上,一看到你喝药,马上急眼不上了。

AddSubGoal 就是给熔炉骑士 AI 的行为列表中添加行为,参数比较多我们只说重要的:

10:所添加行为的寿命
3000:动作 Id
TARGET_ENE_0:目标
999:下一 combo 能否执行的目标距离判断

(寿命和下一 combo 的距离判断其实是非常重要的参数,直接决定了 AI 的最终表现,但是和本篇内容无关,所以先不细说)

那动作 Id:3000 是啥?

给你一刀

归纳一下:

触发「药检」后,如果玩家在熔炉骑士前方 120°、半径 5 米的扇形区域内,80%几率立刻给你一刀,20% 几率什么都不做

elseif arg1:IsInsideTargetCustom(TARGET_SELF, TARGET_ENE_0, AI_DIR_TYPE_F, 120, 180, 10) then
            if f23_local4 <= 40 then
                arg2:ClearSubGoal()
                arg2:AddSubGoal(GOAL_COMMON_ComboTunable_SuccessAngle180, 10, 3005, TARGET_ENE_0, 999, f23_local2, f23_local3, 0, 0)
                return true
            elseif f23_local4 <= 80 then
                arg2:ClearSubGoal()
                arg2:AddSubGoal(GOAL_COMMON_ComboTunable_SuccessAngle180, 10, 3006, TARGET_ENE_0, 999, f23_local2, f23_local3, 0, 0)
                return true
            else
                arg2:ClearSubGoal()
                arg2:AddSubGoal(GOAL_COMMON_ApproachTarget, 3, TARGET_ENE_0, 5, TARGET_SELF, false, 9910)
                return true
            end

有了前边的经验,这里看起来就方便很多了:

【当玩家位于熔炉骑士前方 120°,5~10 米间的扇环时】

  • 有 40% 的几率使用,3005,即冲刺挥砍
冲刺挥砍
  • 有 40% 概率使用,3006,即咸鱼突刺
咸鱼突刺
  • 剩下 20% 的概率,GOAL_COMMON_ApproachTarget,会接近玩家。3 则是熔炉骑士走路的最大速度系数,大概这样
脑门上纹了个「急」

elseif arg1:IsInsideTargetCustom(TARGET_SELF, TARGET_ENE_0, AI_DIR_TYPE_F, 120, 180, 15) then
            if f23_local4 <= 80 then
                arg2:ClearSubGoal()
                arg2:AddSubGoal(GOAL_COMMON_ApproachTarget, 3, TARGET_ENE_0, 5, TARGET_SELF, false, 9910)
                return true
            else
                return true
            end
        end

同理,玩家处于半径 10~15 范围的扇环时

  • 80% 几率以最大速度接近玩家
  • 20% 几率什么都不做

至此,熔炉骑士的药检部分就全部结束了。

熔炉骑士读指令简单框图

有两点需要说明一下:

1. 看起来熔炉骑士只有玩家在其前方时才会「药检」,那我站在他背后是不是就安全了?

这是理论存在但实际不太存在的情况,除了部分招式的硬直状态,熔炉骑士调整朝向面向玩家是较高优先级的事情,而且根据玩家的相对位置(侧前、后方)不同,转向速度还会大幅度加快。

跟得上我的思必得吗?

更不用说,几乎所有招式中,都包含转向调整的窗口时长这种事了。

闪电五连转

就像这张图,是盾牌猛击的攻击行为,1.12S 的时间内,熔炉骑士可以做到依次以 30、240、120、240、360 的转身速度调整 5 次朝向(只要他需要),所以转身 180°给你一下都属于是牛刀小试了。

因此,在「药检」的设计上,只考虑前方的情况是非常合理的。

2.这是不是意味着我站在熔炉骑士 15 米开外喝药就绝对安全了?

理论上是的,至少熔炉骑士的「药检」AI 部分不会对你的行为做出反馈了。但这不意味着他本身的行为模块不会想办法搞你。

结论:

熔炉骑士的「药检」AI,通过玩家距离的不同分成了近、中、远 3 种。其中近距离(5 米)基本就是站在脸上了,很少有人会这样喝药;而远距离则需要玩家通过 BOSS 硬直、自身体力跑位去达成(15 米),因此熔炉骑士也不会做出即刻的攻击行为。中距离是玩家与熔炉骑士战斗中最常触发的情况,所以 FS 的设计师给中距离预备了 3 种不同的反馈,透过概率来决定。

这套设计我觉得本质上是没有问题的,针对玩家的特殊行为进行反馈;在常见情况中准备了多种行为,增加战斗多样性的同时也强化了对玩家反应维度、操作维度的考察,可圈可点。

而我认为可以优化的地方则是:

  • 「药检」模块直接清空原先的行为列表过于武断,必要性不强

喝药作为玩家的特殊行为,可以给反馈,但不需要每次立刻都要给反馈,行为列表本身有「寿命」,结束后自身就会清掉,不需要强制清空执行药检(或者加个概率清空)

  • 近、远距离均有 20% 的行为留白,但中距离却没有,3 种行为均是强压迫性,可以考虑 20% 留白

熔炉骑士在 idle 状态下基本常驻举盾,玩家本就难以对其造成伤害,留白并不会降低难度;留白也并不代表原地待着不动,而是在小概率的前提下给予战斗节奏的变化

if arg1:IsInterupt(INTERUPT_Shoot) then
        if arg1:HasSpecialEffectId(TARGET_SELF, 5039) == false then
            if arg1:IsInsideTargetCustom(TARGET_SELF, TARGET_ENE_0, AI_DIR_TYPE_F, 120, 180, 5) then
                return true
            end
        elseif arg1:IsInsideTargetCustom(TARGET_SELF, TARGET_ENE_0, AI_DIR_TYPE_F, 120, 180, 7.5) then
            if arg1:HasSpecialEffectId(TARGET_SELF, 14606) == true then
                arg2:ClearSubGoal()
                arg2:AddSubGoal(GOAL_COMMON_ComboRepeat_SuccessAngle180, 10, 3003, TARGET_ENE_0, 999, 0, 0)
                return true
            end
        elseif arg1:IsInsideTargetCustom(TARGET_SELF, TARGET_ENE_0, AI_DIR_TYPE_F, 120, 180, 10) then
            if f23_local4 <= 30 then
                arg2:ClearSubGoal()
                arg2:AddSubGoal(GOAL_COMMON_ComboTunable_SuccessAngle180, 10, 3005, TARGET_ENE_0, 999, f23_local2, f23_local3, 0, 0)
                return true
            elseif f23_local4 <= 60 then
                arg2:ClearSubGoal()
                arg2:AddSubGoal(GOAL_COMMON_ComboTunable_SuccessAngle180, 10, 3006, TARGET_ENE_0, 999, f23_local2, f23_local3, 0, 0)
                return true
            else
                arg2:ClearSubGoal()
                arg2:AddSubGoal(GOAL_COMMON_ApproachTarget, 3, TARGET_ENE_0, 5, TARGET_SELF, false, 9910)
                return true
            end
        elseif arg1:IsInsideTargetCustom(TARGET_SELF, TARGET_ENE_0, AI_DIR_TYPE_F, 120, 180, 15) then
            if f23_local4 <= 80 then
                arg2:ClearSubGoal()
                arg2:AddSubGoal(GOAL_COMMON_ApproachTarget, 3, TARGET_ENE_0, 5, TARGET_SELF, false, 9910)
                return true
            else
                return true
            end
        end
    end

像玩家 Shoot 类指令当然也是有读的,大同小异就不赘述了,感兴趣的可以自己看下。

除此以外,熔炉骑士还会读很多其他形式的玩家输入,整个读指令模块有 1000 行,约占其全部 AI 的 1/3


说到读指令,我们不得不提的重量级人物自然少不了这位:

马戏团双雄

直接来吧!

这东西内部命名叫「OldLion」,老~狮~子~

说实话,打开文件前我还想会不会是 BUG 或者是逻辑卡死了什么的,没想到打开后发现有点离谱,特别是在看完熔炉骑士之后。

老狮子的 AI 写法上明显和熔炉骑士习惯不同,99% 是不同的设计师制作的,但原理一样,所以我们也不多解释了:

if arg1:IsInterupt(INTERUPT_Shoot) and arg1:HasSpecialEffectId(TARGET_SELF, 5025) and f36_local3 > 6 then
        arg2:ClearSubGoal()
        if arg1:IsInsideTarget(TARGET_ENE_0, AI_DIR_TYPE_L, 180) then
            local f36_local5 = 0.5
            local f36_local6 = 6003
            local f36_local7 = TARGET_ENE_0
            local f36_local8 = 0
            local f36_local9 = AI_DIR_TYPE_R
            local f36_local10 = 0
            arg2:AddSubGoal(GOAL_COMMON_SpinStep, f36_local5, f36_local6, f36_local7, f36_local8, f36_local9, f36_local10)

老狮子执行躲避弹道的行为执行有 3 个前置条件:

  1. 检测到玩家输入弹道操作(投掷物、法术、弓箭等)
  2. 狮子自己身上有 5025 的状态(可以看出,这里逻辑和熔炉骑士是反的,狮子在少数过渡动作上添加了 5025,而在处于这些状态下时,去执行闪避;最终结果还是不打断常规出招)
  3. 与玩家间的直线距离大于 6

条件均满足后分为两种情况

  • IsInsideTarget 检测了与玩家间的位置关系,玩家位于其左侧 180°扇形时,执行 6003 行为
简单右跳
 else
            local f36_local5 = 0.5
            local f36_local6 = 6002
            local f36_local7 = TARGET_ENE_0
            local f36_local8 = 0
            local f36_local9 = AI_DIR_TYPE_L
            local f36_local10 = 0
            arg2:AddSubGoal(GOAL_COMMON_SpinStep, f36_local5, f36_local6, f36_local7, f36_local8, f36_local9, f36_local10)
        end
        return true

反之,自然就是玩家位于其右侧 180°扇形时,执行 6002 行为

简单左跳

没了。

这里有几个问题,都很严重:

1.单纯将与玩家的相对方位分成左右两个扇形基本没有实际意义

由于魂系游戏中怪物基本都会随时调整方位来保证时刻朝向目标,因此在玩家不动的情况下,这个判断结果只取决于横跳动作后的朝向调整中的细微误差。最终导致的就是上面马戏团双雄的图里,左右横跳看起来完全是随机的。给人一种只写了 50% 左跳、50%右跳 的逻辑,廉价感很强

2.没有设置读指令的距离上限

老狮子这里躲避的判断前提只有距离大于 6,却没有上限,熔炉骑士的 AI 中上限是 15。如果我没记错的话,15 应该是大部分法术技能打不到的距离了,卡这个距离是非常合理的设计。大于这个距离,即便你放法术,我也只会常规逼近而不会虚空闪避

3.完全没有做 SpaceCheck

if f40_local0 >= 5 and SpaceCheck(arg0, arg1, 0, 5) == true then
        f40_local10 = f40_local13
    elseif SpaceCheck(arg0, arg1, -45, 5) == true then
        if SpaceCheck(arg0, arg1, 45, 5) == true then
            if f40_local1 <= 50 then
                f40_local10 = f40_local11
            else
                f40_local10 = f40_local12
            end
        else
            f40_local10 = f40_local12
        end
    elseif SpaceCheck(arg0, arg1, 45, 5) == true then
        f40_local10 = f40_local12
    else

这是我从《艾尔登法环》中红灵 NPC 的 AI 里截取的,可以看到里边大量使用了 SpaceCheck 进行判断,这其实是在判断自身周围一定范围内有没有障碍物。《只狼》中的大部分 NPC 会在执行侧闪行为前进行类似的判断,这非常合理:右边有障碍物,你还非得往右边闪吗?而这老狮子,从图上可以看出,右边已经是墙了,自己还在往墙里怼,带给玩家的表现就很差,直白来说就是显得傻。

4.没有提供多种反馈形式、没有留白

熔炉骑士的反馈根据距离远近分为了 3 类,每种距离内又分别分成了 2、3、2 小类。老狮子的反馈有且仅有一种(左右横跳不算两种),并且在没有检测距离上限的情况下,没有留白就意味着逢 Shoot 必跳,势必是要被批判一番的。

实际上,老狮子的现有资源就已经可以支持多种反馈形式:

强扑

如果玩家在 6~10 米范围内发射投射物,我是不是可以给予 20% 的概率前扑攻击,增加压迫性?

后跳

是不是可以把后跳加入躲闪序列中?横跳下怪物与玩家距离不变,后跳改变了距离,就会有新的 AI 模块被激活,产生变数。

最后就是留白,不一定每次玩家 shoot 都一定要有反馈。

这样一来,可能谈不上多好,起码不会被送到马戏团里了。


结合熔炉骑士来看,老狮子的设计师我感觉是资历较浅亦或是新人 / 应届生:

  • 老狮子的 AI 里,函数中所使用的参数都使用了已经定义好默认值的变量,而熔炉骑士的制作者直接在函数的传参里填了值。虽然前者很规范,但做多了在保证没问题的前提下,____________吧?
  • 一般来说,操作不可打断的窗口条件时,标签肯定是【不可打断】,而不是反过来把可以打断的地方全都贴上能打断的标签。
  • 同样执行玩家方位检测时,熔炉骑士的制作者使用了 IsInsideTargetCustom,而老狮子的制作者使用的是 IsInsideTarget,它们功能基本一致,区别是前者拥有额外的两个参数输入,用来判断玩家的距离。简单来说就是该严谨的地方严谨了。

总结:

正如文章开头所说,读指令在某些使用情景下是完全没有问题的,它不仅可以动态的改变战斗的节奏,还可以让玩家更好的感知到自己行为所产生的反馈,显得 AI 更加「聪明」。这也是该设计方向能够在 FS 的游戏当中传承至今的重要原因。《黑暗之魂 1》里 A 大就已经能对玩家的远程攻击产生 3 种不同形式的反馈了,《只狼》里人人都知道「药检」的存在,却很少有人去喷它是不合理的。而《艾尔登法环》中被玩家截出的种种啼笑皆非的读指令事件,除了少数是因为 BUG,绝大部分还是因为实际制作者层面出现了问题:

至少在读指令这一块,有些怪物的 AI 甚至不如《黑暗之魂 1》考虑的周到

这并不是老狮子制作者的问题,我更倾向于认为由于制作周期、开放世界制作量指数级爆炸等因素,导致资深员工疲于生产内容,不能去做太细致的指导,也没有时间去 review 这种「细枝末节」的东西。

最后,希望小高拿了今年的 GOTY,多招点人,让我早点玩到 DLC 和新作。