音游判定原理详解——从触摸屏幕到判定音符【Project SEKAI攻略】

  “音乐游戏”一般简称为“音游”,玩家需要配合音乐的节奏来进行一定的动作。

  《Project SEKAI》作为一个“移动端音游”,绝大多数玩家会使用手机、平板电脑等移动设备的触摸屏进行游玩,也有极少数的玩家不按常理出牌,使用手台、键盘乃至于触控板等非常规输入设备进行游戏。同时,作为“下落式音游”的一员,在游戏中音符会从界面的上方落下,玩家需要根据音符时机和类型在正下方的判定区域做出一定的操作,当音符与判定线完全重合时,便是音符完美的判定时机。

  本篇文章作为音符具体判定机制分析的前置攻略,将会以使用Unity引擎编写的《Project SEKAI》为例子带领各位读者简要了解一下一款音游是如何实现判定机制的,或许对其他音游也能有所启发。为了便于理解,下文会尽量避免对程序代码的讨论,仅说明基本原理与过程,部分细节可能不够严谨,还请谅解。

  本篇文章主要分为3个部分:

第1部分《从触摸屏幕到触摸对象》,介绍操作系统和Unity框架处理触摸事件的流程
第2部分《从触摸对象到触摸处理》,介绍游戏的主循环和触摸事件处理逻辑
第3部分《从触摸处理到音符判定》,介绍音符的判定时间、下落速度、状态机以及判定过程

一、从触摸屏幕到触摸事件

1.1 触摸事件生成与传递

  触摸屏作为移动设备上最主要的输入设备,当用户触摸时,硬件会以一定频率对屏幕进行触控采样,采样过程主要是获取用户触摸的位置信息,驱动程序会获取来自硬件的事件,经过处理后将触摸位置的坐标、用于追踪手指的追踪ID等信息生成内核事件。越高的触控采样率会让硬件采样时间间隔变短,对手指运动的追踪也更为准确。例如,iPad Pro提供了240 Hz的触控采样率,代表每秒钟会从触摸屏中采样240次,约4毫秒1次。

  在程序的执行过程中,如果需要获取触摸事件,会通过声明对触摸事件的监听器来实现。当触摸事件来临后,操作系统会从内核中取出相关事件,并经过一定处理和封装后,将触摸事件传递给正在监听这一事件的程序进行处理。

  这一过程可以简化为下图:

 

操作系统封装的触摸事件有以下程序关心的内容:

触摸动作:操作系统会识别出按下、移动、松开等动作
触摸位置:触摸位置的坐标
历史触摸位置:上一次触摸事件的触摸位置坐标
事件时间:触摸事件发生的时间
触摸点编号:为当前每个触摸点分配单独的编号
1.2 记录触摸事件

  Unity作为游戏引擎,会为游戏预处理来自操作系统的触摸事件,这样做的目的是使用Unity引擎的游戏可以使用统一的接口获取输入事件,有效减少了实际运行的操作系统给游戏带来的差异性。

  当接收到触摸事件时,Unity会记录这些触摸点的许多信息,游戏着重关注以下内容:

触摸阶段:按下、移动、按住不动、松开等阶段
触摸位置:触摸位置的二维坐标,以屏幕左下角为原点
手指编号:会为正在触摸的手指各分配一个编号
位移向量:如果产生了一定滑动位移,将会记录滑动的位移向量
  例如,一根手指在极短时间内快速执行了“按下”、“移动”、“松开”的过程,Unity会记录下3个“手指编号”相同、“触摸阶段”不同的触摸事件:

二、从触摸事件到触摸处理

2.1 游戏主循环

  在设计游戏框架时,需要一个游戏主循环(Game Loop)每隔一段时间循环处理游戏逻辑,比如完成音符判定、音符移动位置等。不过,无论使用什么策略来确定间隔时间,由于硬件的性能限制,是很难做到完全等间隔的。例如,如果在这一步固定让音符位移10像素会因为时间间隔不均匀导致无法匀速运动,需要根据和上一次处理的时间差来进行进一步的运算。 

为了方便起见,大部分游戏会以“1帧”作为时间间隔进行循环,“帧”指的是游戏中的单幅画面,例如帧率为每秒60帧代表1秒中之内会有60个画面。同样的,每帧之间的时间间隔并非完完全全固定,也会因为性能原因产生波动。值得一提的是,并非所有游戏会在每帧都处理一遍游戏逻辑,例如知名的沙盒游戏《Minecraft》,无论显示帧率有多少,会以每秒20游戏刻的固定速度进行游戏更新,当然如果卡顿了也会导致游戏刻变少。

  同样在Unity引擎中游戏主循环也不需要开发者自己来实现,Unity为游戏提供了2种不同的循环模式:每帧处理一次、与帧率不同的固定频率处理。《Project SEKAI》采用了每帧更新一次的策略,判定相关的逻辑也是每一帧执行一次。

2.2 触摸事件获取

  Unity收到操作系统传递的触摸事件后,做的事情仅仅是记录触摸事件,并不会在收到事件后立刻交给游戏处理,需要等待游戏在处理游戏逻辑时主动获取上一帧之后的触摸事件。特别的是,虽然操作系统在传递触摸事件的时候提供了事件发生的时间,但Unity既不会记录事件发生的时间也不会记录收到事件的时间。

  例如,玩家在第k帧后立即进行触摸操作,在触控采样后这一事件进入Unity引擎,但游戏到了第k+1帧的时候才能得知这一事件,且游戏认为按下的时间是第k+1帧的时间,而非实际按下的时间: 

这样的设计使得Unity引擎下的音游在判定时会产生与真实输入的时间差,会导致“拖判”更容易发生,这一时间差的最大值为两帧的间隔时间,帧率越高间隔越小,拖判也越不明显。

  为了从根源上解决拖判的问题,游戏可以使用“Native Touch”插件等手段绕开Unity的触摸处理逻辑,直接接收操作系统的触摸事件。遗憾的是,可能是碍于技术水平或者是性能的问题,《Project SEKAI》并没有进行类似的改进,而是直接使用了Unity提供的触摸事件。更为遗憾的是,虽然提高游戏帧率能在一定程度上改善拖判的问题,但拥有120帧显示屏的iPad Pro却被锁死在了60帧,不得不接受拖判的现实。

2.3 触摸事件处理

  无论使用什么方法获取到触摸事件后,都需要对输入事件进行进一步的处理。作为一个下落式音游,一个非常重要的处理就是找到点击位置对应的下落轨道。

  《Project SEKAI》将主要的游戏区域分为12根轨道,Unity作为一个支持3D的引擎,这些轨道在三维空间中倾斜一定角度,通过近大远小的视觉效果,给玩家产生一种音符从远处逐渐接近判定线的“距离感”。当玩家触摸判定线及其上下位置时,会通过Unity将二维的屏幕坐标换算为点击位置的三维空间坐标,并使用这一坐标计算对应的轨道。值得注意的是,这一步得到的轨道并非诸如第1轨、第2轨的某一条轨道轨道,而是类似在第1轨的左起33%这样的准确位置。《Project SEKAI》每帧至多处理10个触摸事件。

  例如,假设只有2条轨道,下图所示的这条线便是第2轨的50%位置,这条线并非在平面上垂直于判定线,而是在空间上垂直: 

当然,在具体实现上可以认为每个轨道的宽度为1,直接以1个小数来表示触摸事件的轨道位置,例如2.5等。

  虽然《Project SEKAI》有12根轨道,但实际上音符会拥有一定宽度以达成某种意义上的“无轨下落”,音符可以通过左侧和右侧所在的轨道位置来表示其位置和宽度信息。


三、从触摸处理到音符判定

3.1 音符判定时间

  音游讲究的是玩家操作与音乐节奏的配合,一般来说会根据玩家完成操作的时机和音符与判定区域重合的时间进行对比,根据时间差给予一定的结果,这一过程称为“判定”,部分音游也会允许玩家微调判定区域的位置。判定后,根据不同的判定结果可能会获得不同的分数,也可能会发生减少生命、断COMBO等惩罚措施。

  例如,《Project SEKAI》会提供PERFECT、GREAT、GOOD、BAD、MISS五个判定结果,GREAT会减少得分、GOOD既会减少得分又会断COMBO、BAD和MISS不仅断COMBO还会扣除生命且不得分。

  每种音符有着自己的具体判定时间范围,甚至同一判定在延后和提前的情况下时间范围也不一定相同,但大体上可以总结为下图,比例仅作示意不代表真实比例:

可以看出,两侧的BAD判定时间范围决定了音符最大的判定时间范围,比BAD提前不会进行任何判定、比BAD还延后就会得到MISS。


3.2 音符下落速度

  作为一个下落式音游,音符需要从屏幕上方逐渐“下落”到达屏幕下方,大部分音游都提供了调速功能,可以改变音符下落的速度。《Arcaea》等部分音游也会根据BPM的变化来改变音符实际下落速度。

  《Project SEKAI》为玩家提供了[1.0, 12.0]区间内步长为0.1的下落速度调速。然而,12速的下落速度并不是1速的12倍,实际上音符从出现在轨道中开始直到到达判定线的时间(以下简称“下落时间”)与下落速度有以下关系:

也可以通过描点法画一个直观的函数图像,请注意这并不是一个连续函数且有定义域的限制:

可以看出,下落时间最长为4秒(1速)、最短为0.35秒(12速),速度约为11.4倍。下落速度决定了音符何时进入画面,只有已经出现在画面中的音符才可以参与到判定过程中,即使音符判定时间范围为无穷大也不可以盲点还未进入屏幕的音符。

3.3 音符状态机

  为了控制音符从初始化到消失的生命周期,游戏需要一些方法来管理各个音符,给予每个音符高度定制化的状态便是一个非常好的选择,这些状态之间可以根据预设的条件进行转移,并可以画出直观的状态转移图。

  例如,如果要设计一个需要点击3次才能完成判定的音符,可以画出如下状态转移图:

  在状态转移图上,圆圈代表状态、有向弧代表状态之间的转移,有转移条件并可以在转移时执行一些操作(比如进行判定)、从空白地方指向的状态代表初态(上图中的“等待”)、同心圆代表终态(上图中的“结束”)。到达终态后,一般认为音符的生命周期已经结束,不应该再进行进一步的操作。这样的数学模型也被称为“有限状态自动机”(简称“状态机”)。

3.4 音符判定过程

  为了实现上文所述的状态机,在游戏主循环中每次判定处理时,需要根据音符原始状态、当前条件(比如时间、触摸事件等)确定音符的新状态,并在状态转移过程中执行进入画面、判定等操作。

  这样的操作需要遍历当前可用(可以进入画面、未进入终态)的音符。工程上除了从第1个音符开始遍历以外,还可以使用链表加快遍历速度:将音符按时间排序后放入链表,进入终态后从链表中删除,遍历时从链表头开始,当音符超过“当前时间+下落时间”后结束遍历。还有一个更为简单的实现方式也能达到不错的性能:将音符按时间排序后,从第一个非终态的音符依次向后遍历,如果这个音符也到达了终态则更新这一位置,以此类推直到超过显示范围。

  《Project SEKAI》在每一帧执行的音符判定过程主要如下:

预处理音符:遍历可用音符,根据当前时间,为音符完成进入画面、离开画面等与触控无关的状态转移
检查音符判定时间:遍历可用音符,如果当前时间在音符判定范围内,就将这个音符放入可分配给触摸事件的音符列表
为触摸事件分配音符:遍历触摸事件,根据触摸轨道位置、触摸阶段等信息,主要检查触摸阶段是否符合当前状态的要求,以及触摸轨道位置是否离音符过远(不同音符类型宽松程度也不一样)。通过检查后为触摸事件分配对应的音符,如果有多个可用判定的音符,则取到达时间最近的音符,如果仍然有多个则取距离最近的音符。1个触摸事件最多只能对应1个音符,1个音符可以有多个触摸事件与之对应
对触摸事件所对应的音符进行判定:遍历触摸事件,寻找离音符中心距离最近的触摸事件,使用这一触摸事件和当前时间判定对应的音符,判定也会导致音符状态的变更。然后给剩下的触摸事件重新执行分配音符,然后继续这一过程
  举几个例子方便读者理解,下图的红点代表触摸位置,假设在音符边缘触摸的位置均符合音符要求: 

这几种情景下的触控事件分别起到如下效果:

情景1:下方的音符更早出现,触摸事件将用于下方音符的判定
情景2:触摸位置离左侧的音符较近,触摸事件将用于左侧音符的判定
情景3:虽然一开始会将2个触摸事件均按最近距离分配给左侧音符,但优先判定离音符中心最近的左侧触摸事件,左侧音符判定完成后,会将右侧触摸点重新分配给右侧音符完成判定

参考资料

多点触控协议 - Linux 内核文档(英文):https://www.kernel.org/doc/html/v4.16/input/multi-touch-protocol.html
触摸设备 - Android 开源项目:https://source.android.com/devices/input/touch-devices
MotionEvent - Android 开发者(英文):https://developer.android.com/reference/android/view/MotionEvent
MonoBehaviour-Update() - Unity 脚本 API:https://docs.unity.cn/cn/2019.4/ScriptReference/MonoBehaviour.Update.html
Input-touches - Unity 脚本 API:https://docs.unity.cn/cn/2019.4/ScriptReference/Input-touches.html
UnityEngine.Touch - Unity 脚本 API:https://docs.unity.cn/cn/2019.4/ScriptReference/Touch.html
TouchPhase - Unity 脚本 API:https://docs.unity.cn/cn/2019.4/ScriptReference/TouchPhase.html

结语

  需要说明的是,操作系统相关内容参考了Android的实现,它在操作系统开发方面提供了较为全面的文档与源代码。期待更为专业的玩家编写更详实的解析。

猜你喜欢

转载自blog.csdn.net/m0_69824302/article/details/130128677
今日推荐