本Wiki包含与单机游戏《只狼:影逝二度》相关的各类内容,包括常用攻略、人物介绍、数据整理等。
只狼游戏相关内容交流群:1102710456、667938183,欢迎热爱只狼的小伙伴加群交流。
也非常欢迎对本Wiki中尚未新建的页面进行图片或文字投稿,编辑方法参见:编辑帮助

全站通知:

Hks简介

阅读

    

2024-09-01更新

    

最新编辑:乐小乐Raku

阅读:

  

更新日期:2024-09-01

  

最新编辑:乐小乐Raku

来自只狼:影逝二度WIKI_BWIKI_哔哩哔哩
跳到导航 跳到搜索
页面贡献者 :
小泽1019
乐小乐Forzen

本页面内容是针对hks脚本编辑的mod制作技术的简介,请依据需要阅读。

简介与解包


hks(havokscript)脚本在游戏中是用来关联指令(之后一般称为Action),游戏内的响应(之后一般称为Reaction)与播放角色动画文件之间的逻辑对应关系的脚本。使用游戏解包工具对只狼进行解包之后,玩家的hks脚本在文件夹action内。


这里的Action一般是指由于玩家输入,或者类似的输入信号产生,使得玩家所操控的角色,狼,播放不同的动画所导致的行为,例如玩家在站立状态下按下攻击键,则玩家会做出普通攻击第一下的动作,这就是一个Action。游戏中Action产生的机制因为操作的灵活变化,便有很多种不同的可能性,有些例如检测预输入等机制并不需要玩家真的按下某一个按键,而是因为游戏实际需求,在某个节点中游戏自动帮玩家按下了某个虚拟的“按键”使得玩家做出了更加自然连贯的动作,这些统统会导致Action行为的产生。

而Reaction通常则是因为游戏在运行过程中因为某些客观原因自动发生的某些现象所导致的玩家播放动画的改变。举几个简单例子:玩家落地、玩家从地面进入水中、玩家受到敌人的攻击产生硬直动作(无论玩家是否在防御)、玩家进入强蹲区域、玩家进入非战斗区域等。这些都是并非由玩家操作,而是因为客观游戏内原因所产生的玩家播放动画的变化,我们统一称为Reaction。Action和Reaction我们在这里统称为行为(Behavior)。

这里值得一提的是,解包后action文件夹内除了玩家的hks文件之外还有敌人的一些hks文件,但是敌人的动作注册并不在这里,而在chr文件中,而相关的behbnd文件暂时无法修改。如果想要直接操控敌人做出动作仍然需要编辑AI脚本(但是这也使得我们不能直接修改敌人的硬直等动作的对应关系)。这里我们主要讨论修改玩家的hks文件

我们解包后得到的文件中,主要对玩家起到作用的有4个hks文件。这些文件是Lua脚本,需要使用较新版本的LuaDecompiler进行反编译得到源代码(但注意,使用较旧版本LuaDecompiler直接反编译狼的hks得到的hks文件并不完善,有很多bug,虽然较新版本没有大问题,但还是建议去github上直接下载由iitsigor大佬手动修补完善过后的hks文件,链接在此SekiroHKS)。编辑完成后,将后缀名改为.hks之后直接放在mods文件夹中的正确位置(/action/script)即可生效。与玩家动作有关的四个文件分别为:

  • c0000.hks 包含一些功能函数的定义,例如播放动画、hks初始化、玩家的移动方向速度控制等,全局变量需要在这里初始化
  • c0000_cmsg.hks 包含状态机的定义,以及游戏中实际调用的函数打包,主要使用c0000.hks中的函数
  • c0000_define.hks 所有宏定义,包括了hks脚本和参数编辑器中的参数、玩家输入信号内容等的全部关系,如果要新定义特效等需要在这里定义
  • c0000_transition.hks 包含实际生效的几乎所有内容,c0000.hks中的函数使用本文件中的变量以及函数来构造玩家动画之间的连接

状态机


游戏中玩家在做动作时身上会具有状态机供hks脚本识别。hks脚本通过判断玩家当前所处的状态机,识别Action行为或者Reaction行为并且判断各种条件来生效。在c0000_cmsg.hks中,所有的玩家动画更新行为函数(后缀为onUpdate)都需要传入一个数字作为参数,然后调用UpdateState生效,这个数字对应的就是玩家当前具有的状态机。在该文件的开头,以HKB_STATE开头的宏变量定义都是状态机定义。由于当前的解包技术问题,暂且无法得知每个变量对应的是什么动作的状态机,但是所幸,我们可以通过变量的名字猜个八九不离十。

游戏中定义的状态机有1300余种,如果在实际判断行为的时候每个都需要区分讨论工作量过高。因此hks脚本中为每个状态及指定了两个分类标准:一个叫做姿态(Style),一个叫做状态(State)。后续在c0000_transition.hks中判断状态机时更加常用的是判断当前状态机的姿态和状态,从习惯来讲存储在局部变量currentStyle和currentState中。存储每个状态机对应哪个姿态和状态的全局变量是g_paramHkbState,它是一个长线性表,其中对应状态机的下标处存储的是一个与该状态机信息有关的四元组。这个四元组的前两个值与启动某种摔落保护有关(c0000_transition.hks中的函数_UpdateFallProtection),一般在mod编辑中不起作用。后两个值则是对应的姿态与状态,标准的索引格式是:

g_paramHkbState[状态机名字][PARAM_HKB_STATE__STYLE_TYPE或PARAM_HKB_STATE__STATE_TYPE]

其中PARAM_HKB_STATE__STYLE_TYPE和PARAM_HKB_STATE__STATE_TYPE其实就是3和4。游戏中一共定义了16种姿态如下:

姿态表.png

其中最后一行只是为了记录一共有16种姿态便于遍历。其中包括玩家站姿,蹲姿等不同动作形态。而状态则较多,对玩家当前所处动作进行细分,例如攻击动作,攻击预备动作(居合),右手防御动作,左手防御动作等。更多的细节请自行解包观看。


行为定义与初始化

从本部分内容开始,可以结合hks与参数和输入的控制观看。

c0000.hks中的初始化函数Initialize()在游戏加载时会调用一次,用于为所有全局变量赋初值,以及初始化行为定义。注意这里调用了一个函数ValidateOrderTableInit(),这个函数定义并且初始化了所有行为(包括所有的指令操作和响应),我们在c0000_transition.hks中寻找该函数的定义:

行为初始化.png

这个函数由三个相同的部分组成,我们事实上只需要关心第一部分即可,也就是非叠加的输入指令以及响应行为的定义以及初始化。该函数做的其实就是进行了遍历,最终生成了一个名为g_behaviorValidateOrderByStyle的线性表(它已经在Initialize()中预先声明)。正如这个线性表的名字所说,它是针对玩家的姿态来决定在给定姿态下,如何定义一个行为。这其中一共用到了三个量,我们依次解释:

  • g_behaviorTable 这是一个线性表,其中每个位置存储的是16元组。其元素的下标就是每个行为所对应的值,可以在c0000_transition的开头处查询。举一个例子,BEH_A_GROUND_SP_ATTACK = 142表示玩家在地面上使用武技,则g_behaviorTable的第142个元素(是一个16元组)就对应于地面使用武技这个行为的信息。这个16元组由0和1组成,表示在上述的16种姿态下这个行为能否生效。例如地面使用武技当然不能在空中、水中发生,因此这些姿态所对应的就是0。
  • g_behaviorValidateOrder 这是一个纯粹用于遍历用的映射线性表,其中存的都是2元组,2元组的第一个值是上述行为所对应的值(记为A),例如BEH_A_GROUND_SP_ATTACK(此时A=142),第二个值则是相应g_ValidateReactionTable或者g_ValidateActionTable的第A个分量,用哪个组取决于该行为是输入指令还是相应,例如BEH_A_GROUND_SP_ATTACK是玩家的输入指令,因此该二元组实际上就是{142, g_ValidateActionTable[142]}。定义这个线性表的好处是只需要对这个线性表遍历一遍就能有效遍历所有指令和响应,完成在姿态控制下的初始化


在上面的执行种我们还用到了g_ValidateReactionTable或者g_ValidateActionTable两个新线性表,这两个线性表是游戏机制的关键,我们来重点讲解。两者的结构类似,我们以后者为例。

线性表g_ValidateActionTable中的元素同样以行为所对应的值作为下标,在每个下标处,该线性表存储一个函数。在未来玩家动画更新的过程中,游戏会调用当前姿态下可以生效的每个函数,如果一个函数返回了TRUE,则该行为被判定为生效,其所对应的动画播放逻辑被启用;否则返回FALSE,则跳过,继续检测下一行为。因此这些函数的函数内容其实就定义了这个行为在什么情况下会发生:对于指令而言,就是进行了怎样的输入;对于响应而言,就是游戏内部发生了什么事情(这些函数都需要传入状态机作为参数,方便检测)。我们举一个简单的例子:

地面释放攻击.png

行为BEH_A_GROUND_RELEASE_ATTACK(对应于值141)直译过来是“地面释放攻击”,它实际上捕捉的是玩家在蓄力普攻(突刺)过程中松开攻击键,此时会转为正常的普攻的这个指令,从而引导玩家的动画从蓄力突刺中取消转而做出正常普攻动作的行为。事实上玩家连按左键做出5次普攻中前两次普攻都是可以按住蓄力的,所以这个行为实际上在游戏运行中经常发生。

那么它的内容究竟是什么呢?直译其判断语句,就是:玩家身上具有HID为SP_EF_REF_TAE_GROUND_RELEASE_ATTACK_CANCEL的特效(实际上是特效100338),并且玩家松开了攻击键或玩家身上具有HID为SP_EF_REF_IN_WATERSIDE_AREA的特效(实际上是特效108020).事实上特效100338只在玩家蓄力普攻(左手边和右手边都有)的动作中一开始的一小段有,所以翻译过来其实就是:如果玩家在蓄力普攻的开始一小段时间内,松开了攻击键或者进入了水边区域,则自动释放做出正常普攻的动作(具体是什么还要看这个行为的效果是什么),这与我们的印象相吻合。

通过分析g_ValidateActionTable与g_ValidateReactionTable中定义的各种函数的内容,我们就可以理解游戏中的各种指令、现象在什么条件下会生效;如果mod制作者想要自己新定义一种操作或者一种能够改变玩家状态的情况,则需要按照上面的步骤逐一在这些线性表当中注册,方便代码在运行过程中自动完成初始化。

行为的生效

在游戏中玩家每次播放动画的更新都会调用名为UpdateState()的函数,该函数传入状态机作为参数,依次调用Control()、Validate()、FireStateEndEvent()三个函数来完成一次更新过程。其中Control()函数用于检测当前状态机,对一些变量(例如全局变量)进行更新;FireStateEndEvent()用于控制一些游戏机制所致的自动发生动画,例如玩家死后在地面上持续躺着,玩家的复活动画,玩家复活后应该播放什么动画等。实际检测行为并使之生效的函数是Validate(),我们着重讲它。

Validate()函数的结构与上述初始化函数ValidateOrderTableInit()的结构类似,我们这里不再重新给图片,而是简述其内容:简单来说,该函数通过遍历初始化生成的g_behaviorValidateOrderByStyle以及另外两个叠加动作的线性表中当前姿态允许生效的部分,逐一检测每个行为所对应的定义函数是否返回TRUE,如果返回,则调用函数_ActivateBehavior()使其生效。我们着重来讲_ActivateBehavior()这个函数。

_ActivateBehavior()函数占了c0000_transition.hks的大半篇幅,它实际上就是每个行为生效后播放动画的逻辑。其输入两个参数,状态机与行为ID,函数体由大量的条件分支组成,其实就是针对每个行为设计了动画的播放逻辑。这里提一句播放动画的函数是FireEvent(动画名),由于前面相同的解包原因,每个动画的编号 我们无法直接得知,但是可以通过我们对游戏的理解以及其名字猜出一个字符串对应的是什么动画。类似前面,我们还是同样举一个小例子来说明:

释放攻击生效.png

与上面相同,我们还是举了普通攻击释放这个行为的例子。可以很明显看到,除了开头调用了一个开始动画的函数(实际上是修改了有一些全局变量的值)并且将行为组参数切换到右手外,基本就是根据当前状态机做条件判断决定播放哪种普攻动画。从上到下依次为:第一下普攻、第二下普攻、反手第一下普攻(左下到右上,部分动作后衔接普攻出现,下同)、反手第二下普攻、防御后普攻、弹开后普攻、招架后普攻、垫步后普攻、蹲下普攻到一半站起来、跑步中普攻。虽然我们从这里不能直接知道它们对应到哪一段动画,但是意思还是很明显的,可以之后在DSA和游戏中寻找对应。

阅读该函数的内容能够帮我们熟悉玩家的动作逻辑;如果要在mod中修改,无论是添加新的行为,还是在原有行为上做变化都是可行的。

实例:玩家的防御与抖刀

我们再来讲一个稍微复杂点的例子来说明hks的运作,这里取游戏实现玩家防御与抖刀作为例子。如果想要对这个机制有一个初步了解可以参考wiki主页下拉菜单中“玩家的防御机制”,或者看琐帝的这个专栏:https://www.bilibili.com/read/cv9113025

在hks脚本中,实现玩家“带弹开的防御”的动作是玩家在各种条件下的举刀动作,与防御与抖刀机制相关的是下面的两个行为:

防御行为.png

上图中下面的行为是玩家的抖刀行为。抖刀行为的判定比较简单,也就是允许战斗区域、非水边、非强蹲区域中按下防御键就会触发抖刀。

而上面的举刀防御机制的判断比较复杂,我们来梳理一下逻辑。if条件句中第一个紫色括号(在倒数第二行结尾结束)与后面的关系为and,因此允许战斗区域、非水边、非强蹲区域是必须的。然后再看紫色括号内的内容,逻辑为:非第一个蓝色括号,或,非第二个蓝色括号,或,后面一大段内容。后面的一大段内容是对玩家转伞的判断,大致就是玩家使用能旋转的机关伞在开伞防御的状态下按下防御键,因此我们这里可以忽略这一段内容,重点放在前两个蓝色括号。第一个蓝色括号中的内容清晰明了,注意逻辑上非(P或Q)就是(非P且非Q),这里P是玩家按下防御键时间小于0,而Q是“玩家没按下防御键,且不具有HID为SP_EF_REF_ENABLE_PRESS_DEFLECT_GUARD的特效(特效100367)”,从而Q的否定就是“玩家按下防御键,或者具有特效100367”。总结一下,第一个蓝括号中的内容就是“玩家按下了防御键,或者玩家按住防御键直到出现特效100367”,这里特效100367在许多动作中都有,其起到的作用实际上是允许玩家之前输入的按住防御键操作生效,这是在Yapped中给出的注释“可按住防御取消动作”的准确意义。

第二个蓝色括号内的内容则比较有意思,按照之前的逻辑,这应该代表“玩家同时具有HID为SP_EF_REF_TAE_ENABLE_ADD_ACTION_INPUT_GUARD和SP_EF_REF_TAE_ENABLE_ADD_ACTION_INPUT_GUARD_CANCEL的特效”,这两个特效分别是100382和100383。换言之,当玩家同时具有100382和100383时,hks脚本会判定为玩家进行了一次防御输入,这次防御输入的判定和玩家按下一次防御键的效果是完全相同的。100382特效只有两个动作具有:299024和299060,这两个动作都是不具有骨架运动,生效时直接叠加在当前动作骨架上,前者的效果是赋予玩家0.2s的100382特效,我们现在暂且称该动作为“防御记录”;后者的效果是赋予玩家0.3s的100382和105010特效,我们称该动作为“连弹保护”。虽然它们的生效场合不同,但是它们的作用基本相同:用于实现游戏中时间跨度比较长的,准确来说是跨动画的“防御预输入”或者说“防御延迟输入”。举一个简单的例子,在DSA中,记录玩家输入的TAE只能在一个动作之内实现预输入,也就是玩家输入后直到允许相关动作跳转的TAE才会实际在游戏中做出动作。但是如果一个输入的状态想要带入另一个状态机,或者说带入另一个动画该如何呢?这里游戏会使用一对类似100382和100383的特效对实现。例如当玩家撑伞防御时输入跳跃,则为了平滑起见,玩家会先做出收起机关伞的动作再起跳,此时玩家的跳跃输入实际上从撑伞的动作被“带到”了收伞的动作,然后延迟做了出来。因此如果玩家装备未经任何强化的机关伞,在撑伞起见输入防御,此时因为玩家无法转伞,游戏想要实现玩家先将伞收起来再做出普通举刀防御的动作,其实现方法就变成:玩家的防御输入导致玩家做出收伞动作的同时还为玩家叠加了299024动作,使玩家拥有了100382特效。hks中有其他机制保证在玩家身上有100382特效而没有100383特效的情况下持续给玩家添加299024动作保证100382特效不会中断。在玩家的收伞动作中的某一帧,玩家身上出现了100383特效(读者可以在DSA中自行确认),于是上述防御行为定义中的第二个蓝色括号被触发,游戏帮助玩家在这一帧虚拟地输入了一次防御,使玩家在收伞后做出了举刀防御的动作。知道了这个原理之后,我们可以称诸如100382的特效为“预输入记录特效”,诸如100383的特效为“预输入释放特效”。事实上不仅是防御,大部分的键位预输入都有类似的机制,读者可以自行发掘。

总结一下,什么情况下会判定玩家进行了(举刀)防御输入?有以下三种情况:第一,玩家按下了防御键,这是最简单直白容易理解的防御输入,无论之后是保持按住还是按一下就松开,只要按下的节点动画允许跳转到防御动画,玩家就会做出举刀的动作;第二,玩家在不能跳转防御的时间点按住防御键不松手,直到出现了100367特效,这就是某种意义下短暂的“防御预输入”的效果,起到先前按住的防御键取消当前动作的效果,通常的跳转时机要比合适的时间按一下输入防御要晚;第三种,在具有100382特效的时候获得了特效100383,这就是真正的防御预输入的效果,通常是用于游戏作某些动作衔接,方便玩家在开始举刀防御之前先做一个别的动作,然后再举刀,连弹保护就是其中一种最典型的情况。

举刀防御逻辑.png

上图是玩家举刀防御行为生效后的动画播放逻辑。从其中的if条件句开始,在非战斗状态下收到输入信号会直接返回不产生任何效果;第二个分支我们暂时略过;后续的分支依次判断:玩家在2、3号冲击力(3号对应2冲,2号和1号都是1冲这里产生区别)受伤硬直中(特效100345),则播放受伤后防御动画;玩家在奔跑中(特效100303和100304),则播放奔跑后防御动画;玩家处于某些反手动作中(特效100274),则播放反手防御动画(这里特别说一句,该动作就是“动作后防御”,因为部分动作衔接普通举刀很不自然因此特地做了反手举刀,实际上也有动作衔接正手举刀,例如右手侧3冲弹开);再下面的一大段是转伞(中间还需要判断有没有纸人),最后是普通的举刀防御。

我们再回头看刚才略过的第2个分支,翻译过来就是:玩家具有100285特效并且没有100383特效时,播放(实际上是叠加)299060动作。这就是“连弹保护”的机制。299060动作所具有的额外105010特效的效果是:增加玩家的防御弹刀力,使得敌人攻击具有该特效且正在防御的玩家时判定为弹刀,同时做出被弹开动作;并且用HID将玩家的防御后动作链接到弹开动作而不是招架动作。简单来说,如果防御中的玩家在具有105010特效时被敌人击中则判定为成功弹开,否则判定为普通招架。玩家的举刀动作的前0.3s具有该特效,这就是我们所谓的“完美弹开”。而100285特效则在玩家的大部分招架动作和弹开动作上都有。

这里我们来看一下如果敌人以中等的固定频率(例如长爪蜈蚣的连抓)用低冲击力攻击攻击玩家时发生了什么事情:敌人的第一次攻击即将命中玩家时,玩家输入了防御,出发了“举刀防御”的行为判定,上述条件判断给出结果是做出普通举刀防御动作,敌人的攻击打在具有105010特效且举刀防御的玩家身上,玩家成功弹开敌人的攻击,开始做弹开动作。在弹开动作的进行过程中,敌人的第二次攻击袭来,玩家在被命中之前再次输入了防御,在上述行为判定中满足第二条条件(有100345特效且没有100383特效),触发了连弹保护,该输入的效果是给玩家100382特效和105010特效。如果此时玩家再被命中则依然会被判定弹开(例如玩家去弹一心四连射和弦一郎四连射这种频率的攻击时),但事实上一般的攻击都没有这么高的频率,因此玩家继续做第一次弹开动作直到100383特效出现。此时根据我们之前讲的预输入机制,100382特效与100383特效同时生效额外补充了一次输入,hks脚本再次进入上述举刀防御的逻辑判断,这一次因为有100383特效从而并没有进入第二个分支,而是进入了反手防御的分支(也有可能就是普通防御的分支,取决于弹开动作是否有100274特效),玩家在100383特效出现的下一帧开始做反手防御动作并删除刚才该行为导致的299060动作。在反手防御动作的前0.3s内第二次攻击命中玩家,同样判定为弹开,因此玩家开始做第二次弹开动作,之后的步骤就重复了。

从上面的步骤我们可以看出,如果玩家受到极高频率的,次数大于等于三次的攻击(例如秘传一心、长爪蜈蚣左手三连、水生凛131等),尽管第一次会正常触发弹开,第二次在连弹保护生效期间内受击同样判定为弹开,但是玩家的弹开动作并没有持续到100383特效出现的那一帧,反手侧防御动作并未触发。此时如果玩家在第二次弹开动作中再次受击,则即便玩家已经再次预先输入了防御,因为防御输入时仍然会进入第二个分支给玩家动作299060,但是上一个299060动作还没播放完(虽然该动作有0.3s的特效添加,但是持续1s),因此该次输入实际上什么都没有做。此时玩家第二次输入防御添加的连弹保护早已失效,敌人将成功命中没有105010特效地玩家(100382特效也早已结束,玩家不会做出正常的反手防御,从而在弹开动作中受击)。在尝试弹开长爪蜈蚣的左手三连和水生凛的131时,应该让第三次输入尽可能晚(也就是尽可能准地弹开第三下),保证玩家在100285特效消失后(实际上和100383特效出现是同一帧)再输入防御,保证做出正常的防御动作。当然这样做的容错非常低。而对于弹开弦一郎的飞渡,虽然难以完全弹开的原因类似,但是我们处理的方法不相同,是通过让连弹保护同时覆盖两刀的方法实现的,容错同样很低。

然后是抖刀的机制,见下面的图:

抖刀行为.png

抖刀行为生效后的动画判断非常简单,也就是当玩家一直不停地按下防御键时,如果抖刀特效存在(特效100319,举刀动作都有),则不停地往下派生新的抖刀动作,否则就正常按方向做防御(从而与举刀防御不冲突)。三个逐步加重的抖刀动作中105010特效的持续时间逐渐缩短(玩家越来越难以弹开敌人的攻击),并且动作会额外附带特效降低玩家弹开敌人后造成的躯干伤。这个机制称为“防抖刀机制”。由于游戏设计原因,一心的四枪会因为防抖刀机制而难以全弹。按照我们上面的分析,第二枪会打在玩家的连弹保护上,但是事实上因为该枪为8号(0级)冲击力,即便判定为弹开也不会触发玩家的弹开动作,玩家正常做弹开动作直到100383特效出现,玩家开始做反手防御动作,在新的105010特效时间内弹开了第三枪。第三枪仍然为0级冲击力,这导致尝试弹开第三枪而再次输入防御的玩家触发了抖刀使得105010特效时间缩短,无法成功弹开第四枪。