欢迎光临散文网 会员登陆 & 注册

音游制作杂谈(230411)

2023-04-11 19:29 作者:Fuxfantx  | 我要投稿

本次的杂谈涉及到制作下落式音游时的三个常见问题,先从最基础的“为什么引入状态模式、状态模式如何优化”谈起,然后聊到速度系统和差速系统的实现。


1、怎么实现一个可任意跳转的Autoplay?

// 如果从“调用游戏引擎的API”为切入点考虑怎么制作音游,那么很容易陷入“实例化一批新的Note,释放一批旧的Note”的迷思,然后顺理成章地把Note的击中处理成“分数、Combo等等相加一下,然后调个函数播放一下动画,最后销毁下物件”什么的。

// 如果需要实现“可任意跳转的Autoplay”,那么这个思路会很麻烦。跳转了之后,需要去考虑哪些Note还在屏幕上,哪些Note已经不在屏幕上,…最致命的是,如果有个Note处于“已经判定,动画播放了一半”的状态的话,复现的效果会很差。

// 因此,非常提倡把Note处理为“状态机+对象池”的模式。跳转到一个特定的时间时,先取出一批当时认为“需要显示在画面上”的Note,然后从对象池里取出一批游戏对象,设置下参数,引擎在当前Gameloop把脚本逻辑跑完一遍之后就会自动把你想要的“画在屏幕上”,完事。


2、分段优化

// 上面的工作模式讲到了“取一批Note”的过程,这个过程通俗地理解,就是把手头的Note都问一遍“你是否符合我的要求?”的过程。

// 假定一张谱有1000个Note,设备是60fps,那么每秒就是60000次这样的“发问”。

① 如果设备是PC,或者使用了il2cpp,乃至原生C库一类比较高效的架构,这个量级不会遇到太大的问题;但如果是移动设备的话,对于如此大量的“发问”,即使SoC的极限性能能支撑起这个量级,也有很大可能促成发热、费电、卡顿掉帧等。

② 另外,假设1分钟的谱面有1000个Note,10分钟的谱面有10000个Note。游戏在处理“10分钟的谱面”时,每个Gameloop在Note轮询方面都要承担10倍于处理“1分钟的谱面”的性能开支,而就整个游戏流程来看,10分钟的游戏流程Gameloop的量也是1分钟的游戏流程的10倍。Gameloop数g正比于t,note数正比于t,发问数是二者相乘,就正比于t的二次方了。这种二次关系对于稍微长一些的游戏流程会非常不利。

// 为此,有一个比较自然的想法,就是控制一下每个Gameloop的轮询范围。可以设想一下按时间为1-1000ms,1001-2000ms,…把所有的Note分进若干的组,那么Gameloop时只需要将时间做一下对应,就可以做到一个Gameloop最多轮询(完整显示组数+2)组Note。

// 这个优化收益还是比较可观的,试试,没效果也不会有啥损失。


3、变速和实时调速的实现

// 视觉变速系统(下称SC)有一种常规实现,就是构建一个SC节点表,以这个表为依据为每个Note计算一个SC系数(下称dtime)。

// 如何根据乐曲进度(从乐曲开始播放经过的秒数)t、SC节点表、玩家设置的速度倍率k设计一个dtime -> 画面pos的投射呢?

// 就个人的实现而言,一切的前提是“让其它形式的时间向ms时间靠拢”。这里假定我们存储的所有时间都是ms时间,便没有“其它形式的时间”。

// 接下来,就可以尝试给SC节点表的每一个节点追加一个dtime戳了。这里使用的是增量法,左边是“处理前”,右边是“处理后”,右边最后一列给了一个大概的算式。

// 讲解尽量通俗,就不给出具体实现了。

// 根据处理后的SC表,可以比较容易地算出一个Note的dtime,这里采用的是插值的思想,不展开讲解。

// 从数学上,可以这么表述从ms时间到dtime的一种映射:dtime = f(mstime)。根据插值思想,这个表达式的f是一个由若干线段构成的分段函数。

// dtime到场景pos又怎么映射过去呢?可以简单的假设一个“照度”的概念:

// 考虑一个Note,出现在一个假想的“画面顶端”,然后竖直地落到判定线上。“画面顶端”的位置记为y,“判定线”的位置记为y0,那么这个游戏场景的“照度”就是(y-y0)。

// 记照度为i,当前时间为t,那么一屏之内显示的dtime范围就是[ f(t), f(t)+i/k ],这里引入的比例系数k便是玩家设定(或谱面规定,或两者都有)的流速了。考虑pos = g(dtime)的形式,不难得出,时间为t时:

y = g( f(t)+i/k )

y0 = g( f(t) )

// 由于Note匀速下落,g(dtime)需要是一条直线,解这个变换的条件就这么凑齐了。

 

// 由于比例系数k有可能动态改变,按pos进行分组优化不太现实。因此,如果实行分组优化的话,可以考虑按dtime来分组。

// 加载一帧,轮询哪几组Note呢?取决于 f(t) 和 f(t)+i/k 两个值。设分组组距为整数w,把 f(t) 和 f(t)+i/k 强转为整数,轮询的第一个组为 f(t) // w(//表示整除);顺次轮询,一共轮询几个组?[f(t)+i/k]//w - f(t)//w + 1 组。


4、下落Note的多节点状态

// 如果一个Note会在下落纸带上相对运动,那么根据实际需要,可以给这个Note确定一个“出现在纸带上的时间”t0。但考虑到Note相对运动的用例没有那么常见,这个t0可以直接界定为0。

// 接下来,准备一个分段函数h,使得 t' = h(t) ,dtime计算相应地就变为了 dtime = f(t')。这意味着,“出现时间”后的每个Gameloop都要为这个Note确定一个t'。

// 因此,通常采用这样一种处理:将附带相对运动事件表的Note单独拎出来组成一个表,每帧给表内还存活着的Note推断它的t',再寻找t'对应的dtime。计算完 f(t) 和 f(t)+i/k 后,除了常规Note表之外,还在这个单独的表“发问”,询问里面的每个Note,是否要渲染出来?渲染出来的话,g(dtime)是多少?

// 最后……g(dtime)比判定线低的话,是否属于Autoplay要播放打击动画的情况?播放的话,播放多少?……

音游制作杂谈(230411)的评论 (共 条)

分享到微博请遵守国家法律