吐槽
滚动

学习记录&进度更新0628

弹幕相关写了一半了,角色的重生本来写好了 被我自己覆盖掉了【允悲】

睡了一下午没干活 再说吧

怎么判定物体相交和相交后的处理还是很困难啊

引用科普一下

欢迎大家到原网页学习:https://cowlevel.net/people/seiwell

 对于STG这种按帧推进的游戏,要使游戏运行速度恒定,就必须使游戏帧率恒定。正常游戏帧率都是60fps。为了将游戏帧率稳定在这个数字,无非通过两种方法,一种是依靠垂直同步,一种是自己写代码通过定时器发出渲染信号。

首先来看垂直同步。这个词大家可能很熟悉,但是并不一定知道这是指什么和什么同步。其实显示器的刷新率和游戏的帧率不是一回事。游戏的帧率可以变化,而显示器的刷新率总是60fps。或者说,显示器刷新的时间点与游戏渲染画面的时间点并不吻合。所谓垂直同步,就是将游戏渲染画面的时间点与显示器刷新画面的时间点同步起来。当开启垂直同步时,渲染出来的画面不是马上显示到屏幕上,而是会等待显卡的同步信号,接收到同步信号后再显示到屏幕。这个同步信号的频率必然和显示器的刷新率相等,如果显示器刷新率是60fps的话,开启垂直同步后游戏的帧率也会是60fps。所以开着垂直同步的话,就直接可以控制游戏帧率了。不过上次文章也说了,显示器刷新率其实也并不是那么可靠的数字,不一定是精确的60fps,而且开着垂直同步有时还会有一些负作用,所以游戏往往还是需要配备一套不使用垂直同步的帧率控制方法。

如果不使用垂直同步的话,渲染出来的画面就不用等待同步信号而直接渲染至屏幕。那么要控制帧率的话,就必须通过定时器来控制渲染画面的时机。这个动作可以简单概括为“每隔16.667毫秒渲染一次画面”。理论上是非常简单,但实际上这个方法非常有讲究,包括精度问题、定时器的实现问题,不过这些不是今天的重点,我们先放一下。今天我们只谈一下传统的控制方法为什么不适合做同调弹幕。至于为什么不适合,其实原因很简单,因为这是一个增量式的控制。所谓“每隔16.667毫秒渲染一次画面”,也就是说从上一幅画面渲染开始,经过16.667毫秒后渲染下一幅画面,如果中间出现运算量过大,发生延时,则下一幅画面开始渲染的时间点就会推迟,之后的画面统一延后。这样的机制必然会形成累计误差,即使是很小的帧率波动,累计到后面,都足以破坏版面与BGM的同步性。

至于怎么避免累计误差,这个思路也很简单。之所以会有累计误差是因为渲染时机是相对上一帧画面延后16.667毫秒确定的,如果渲染信号不是相对于上一帧画面而是相对于第一帧画面,就可以消除累计误差。这个逻辑可以简单表述为,如果当前时间为16.667毫秒的整数倍,就进行画面渲染。

下图示意了传统帧率控制方法与改良后帧率控制方法的差异,其中横轴为时间轴,刻度表示渲染的时间点。当帧率稳定时,各渲染时间点间隔相等。当发生了一个延时或者卡顿时,传统方法会将之后的渲染画面相应延后,以保证当前帧率为60fps。而对于改进后的方法,整体的渲染时间点不受延时或卡顿影响,当发生延时或卡顿后,画面会以最大速度渲染,以追赶整体进度。

b69b15634c8ffb4cb09288976350bbf0.png

在换个角度讲,传统方法是在保障当前帧率为60fps,而改良方法是在保障平均帧率为60fps。因此可以通过改良后的方法来消除累计误差,为同调弹幕提供技术支持。因为这种方法依据的时间不是上一帧的相对时间,而是绝对时间,所以我称它为绝对帧率控制法。

不过,这种方法存在一个比较显而易见的问题。如果游戏发生了持续的延时或者说掉帧,那么之后游戏会持续地对画面进行极速渲染以追赶整体进度。举例来说,Boss某张符卡堆了10000颗子弹,游戏帧率掉到了30fps,持续了十几秒,这张符卡一过,全屏消弹后,游戏会为了追赶整体进度而持续极速渲染,为此游戏可能会持续几秒钟运行在几百fps。这样给玩家的体验就是游戏先掉帧掉了十几秒,然后又加速运行了若干秒。如果是传统的帧率控制方法,可能仅仅只是掉帧这个缺陷而已,而改良后的方法又多出了加速运行的问题,反而使事情更糟。为了防止这种情况发生,必须为这种帧率控制方法配套一种主动丢帧策略。当游戏卡了超过指定帧数后,主动放弃追赶整体进度。或者说,设置一个追赶最大值,当游戏掉帧后,全速渲染追赶整体进度时只能追赶指定帧数,如果超过这个指定值后,则放任其掉帧,延后游戏进度。

关于这个指定值,如果设得太大会存在游戏加速问题,如果设得太小则会存在累计误差问题。极端情况,当其取为无穷大时,则效果和没有这个值一样,游戏会无限追赶整体进度;当其取0时,则效果和传统帧率控制方法一样,游戏在卡顿后不会追赶整体进度,从而形成累计误差。具体取多少合适,需要根据实际情况,根据个人喜好来定。这里提供一个经验数据,从《东方百花宴》到现在的《弹幕音乐绘》,我这里这个值设的都是10帧(166.667毫秒),也就是说,游戏会把10帧以内的瞬间延时给消化掉,10帧以内的瞬间延时都不会造成累计误差,但是更大程度的延时或卡顿就会造成累计误差了。从实际效果看,10帧这个容忍量大大提高了同调弹幕的可行性。

【帧率控制(里)】

在第五篇的时候,我介绍了在不使用垂直同步的情况下,有两种控制帧率的逻辑,分别是依据增量时间的传统方法以及依据绝对时间的改进方法。当时仅从理论上进行了讲解,然而,帧率控制方法实际操作起来其实非常需要技巧,尤其在计时器的操作方面存在诸多细节,仅凭先前说的那些理论,估计对于大多想自己写弹幕程序的人来说并不能起到实质性帮助。因此,本文将从代码逻辑方面,更进一步的讲解帧率控制方法。

之前说过,要控制帧率60fps最基本的方法就是每间隔16.667毫秒进行一次画面渲染。那么这里必然牵涉到计时问题,首先面对的问题当然也就是计时器的选用问题。相信不管使用什么编程语言,肯定都有一堆计时器可以选用,我比较熟悉的C#中常用的计时器就有三种,包括System.Windows.Forms.Timer、System.Timers.Timer、System.Threading.Timer。然而实际这些计时器都不可行,问题当然出在精度方面。这些计时器精度最高也只在毫秒级,然而要控制到16.667毫秒则精度必须达到微秒级。至于使用毫秒级计时器会产生多大误差,以下可以简单算一下。

当渲染间隔为16毫秒时

帧率 = 1/0.016 = 62.5fps

当渲染间隔为17毫秒时

帧率 = 1/0.017 = 58.8fps

因此,在选用毫秒级计时器来控制16.667这个数字时,帧率至少会在58.8fps至62.5fps间波动,比如无法获得稳定的60fps。

要获得稳定的60fps,就必须使用微妙级精度的计时器,其归根结底就只剩一种选择,就是依靠Win32API中的这两个方法:

QueryPerformanceFrequency 和 QueryPerformanceCounter

通过这两个方法可以获取CPU主频以及CPU跳动的次数,从而计算时间。这样的计时方法可以达到电脑所能达到的最高计时精度。

C#中对这两个方法倒是有托管封装,用起来会相对方便一些。该方法具体位于System.Diagnostics名空间下的StopWatch类中,具体用法可以参见MSDN。如果使用其他语言的话,请参见WIN32 API。

在选定了计时器以后,我们来看下一个问题,关于定时器的实现方法。由于上述方法只能实现对当前时间的访问,并不能够实现定时触发,想要定时发出中断信号依然不能直接实现。换个通俗的说法,以上方法只能告诉你当前时间是多少,却并没有每隔16.667毫秒发出一个信号这样的功能。要实现每间隔16.667毫秒渲染一次画面,最简单粗暴的写法是使用循环查询。在程序中不断地读取当前时间,并判定是否达到指定时间,如果达到的话进行画面渲染,没达到的话继续读取时间。这种最简单粗暴方法的程序框图如下所示:

82aa5204578b2392ca3bbb6a5969a778.png

以上方法相当于在利用读取时间的代码来进行延时,虽然定时精度高,但是相当占用CPU资源,因为CPU始终在读取时间。显然这种方法会平白无故浪费CPU,并不是很可取。然而,如果通过降低查询的频率来节省CPU的话,必然导致定时精度降低,因此这里程序的写法上其实要有一点技巧,可以通过先延时再查询的方法。

各种编程语言基本都会有个方法叫Sleep,可以用于延时。当使用Sleep方法时,其实是将线程挂起一段时间,并不占用CPU资源,然而其精度也只在毫秒级,不能直接用于帧率控制。不过,这里我们却可以利用Sleep方法来降低CPU的负担。当一帧画面渲染完毕后,先查询一下当前时间,得到距下一次渲染画面还差多少时间,然后依据此时间进行Sleep延时。举例来说,查询发现距离下一次渲染画面的时间还有11.5毫秒,那么可以先Sleep延时个11毫秒,然后进入循环查询模式,来保证定时精度,这样,相当于去掉了11毫秒的CPU负荷,仅有0.5毫秒处于循环查询模式。此方法的程序框图如下:

67e91e0753ba5d290334220a94b70b7f.png

用此方法,可以在确保帧率控制精度的前提下,大大降低CPU负担。此外,以上框图所示的是传统增量式的帧率控制方法,如果要使用绝对帧率控制法,则仅在第一帧时进行计时器清零,之后将判断“是否到达16.667毫秒”改为“当前时间是否到达16.667毫秒的整数倍”即可。

以上便是今天关于帧率控制的科普。

Contents licensed under Creative Commons by-nc-sa 3.0.
Post Comment

Post Comment