去年(2021年)购置了一款新手机,一加9 Pro,这是一款屏幕刷新率支持到了120fps的安卓手机。现在一时兴起,想把4年前自己的 flappybird
项目拿来玩玩,由于屏幕刷新率120fps,而以前是为60fps的刷新率写的代码,在这台手机上面运行结果发现游戏速度得很快,根本跟不上节奏。这必然是不应该的,这么大一个问题,绝对不能出现在我的项目里面,所以最近又研究了一下LibGdx
这款游戏引擎,希望能够修复这个问题。
一、问题原因
由于我在游戏内使用了LibGdx内置集成的box2d
物理引擎,该引擎提供了一个 step(timestep, velocityIterations, positionIterations)
函数,用来更新物理引擎的状态,通过不停的执行此函数,在这个物理引擎世界内的各个元素,就会开始按照设定的重力值进行运动了。因此,这个函数传入的第一个参数timestep
时间步长就会直接影响各个元素的运动效果。
最初,我的设备是60fps
的刷新率,因此我直接将timestep
的值传入了1/60
,也就是每秒计算60次,迎合屏幕刷新率,最终运行效果也是非常如意。过去的代码如下:
// GameScreen.java
@Override
public void render(float delta) {
world.step(1/60f, 6, 2); // 物理引擎世界更新
stage.act(); // 逻辑更新
stage.draw(); // 界面渲染
}
这个代码写死了物理引擎每次执行render
的时候都以1/60秒的时间步长去计算各个元素。当其在120fps
的设备上执行时,由于render
函数的执行次数每秒达到了120次,因此物理引擎内的各个元素的运动速度就比60fps
的屏幕上快了2倍,这哪能行啊。
二、解决办法
为了解决这个问题,我心想,既然1/60
用于60fps的屏幕,那我直接写 1/120
不就修复了。修复是修复了,但是这个值放到60fps的设备上运行后,又会变成慢动作
了。肯定不可以写死。有小伙伴可能会想了,render
函数本身传入的delta
参数值,不就是每次执行距离上次的时间间隔嘛,直接用它不就可以适配各种刷新率了。是的,想法是很美好的,但是现实是很残酷的,物理引擎如果想要得到一个稳定平滑的运动效果,那每次传入的timestep
的值就必须是相同的,而render
函数本身传入的delta
这个值实际上是一直在变化的,小数点后面三四位数几乎每次都会变动,这会导致我们构建的物理世界一会儿快一会儿慢,用户根本没有办法继续玩游戏下去了。
终于我想到了一个办法,当我设置1/60的时候,在120fps刷新率下会因为render
函数执行次数增加导致物理世界运动变快,那么我做一个判断,让world.step()
函数不要每次都执行,稍微定时一下,让它每秒只执行60次,不就迎合了吗。于是我写下了下面的代码:
// GameScreen.java
private float timePassed = 0f; // 声明一个变量来保存每次更新时间。
@Override
public void render(float delta) {
timePassed += delta;
if (timePassed >= 1/60f) {
world.step(1/60f, 6, 2); // 物理引擎世界更新
timePassed = 0f;
}
stage.act(); // 逻辑更新
stage.draw(); // 界面渲染
}
代码写毕,编译运行,轻松解决,但是但是但是,我这可是120fps
的手机啊,我这代码写下来,我玩起来怎么感觉还是一台60fps那种感觉啊,那这可不行啊,我这强迫症可忍受不了这种体验。于是我又开始慢慢的思考。
三、最终完美解决
经过我无数脑细胞的牺牲,我总结了上面这个解决办法的问题所在,我这属于是降纬操作了,活生生把120fps强行降低到60fps去计算物理世界的运动。那我为什么不反过来,我人为的一定要保证每一秒让物理世界计算120次,无论是60fps的设备还是120fps的设备,而对于60fps的设备,每秒钟render
的执行次数都才60次,怎么让物理世界执行120次呢?很简单啊,每次render
时在里面执行两遍step
函数,不就可以了。于是,我写出了下面的代码:
// GameScreen.java
@Override
public void render(float delta) {
// 先计算出这次执行render的间隔时间是 1/120 的几倍。
// 如果设备是120fps的,那么这里得到的 dt 就会一直为1左右,下面循环就只会执行一次。
// 如果设备是60fps的,那么这里得到的 dt 就会一直为2左右,下面循环就会执行2次,从而保证60fps的设备,也能做到每秒更新120次物理世界的运动。
double dt = delta/(1/120f);
for (int i = 0; i < dt; i++) {
world.step(1/120f, 6, 2); // 物理引擎世界更新
}
stage.act(); // 逻辑更新
stage.draw(); // 界面渲染
}
代码再次竣工,上机运行,120fps下丝滑流畅,60fps下依然完美运行,同时这样修改后,其它的重力参数和冲量参数也无需进行修改,均可在不同fps的设备上执行得到相同的效果。来看看效果(动图录制帧率好低啊):
上方我人为定的每秒执行 120 次物理世界计算,如果又来了一个 144fps 或者 300 fps 的设备,肯定又会出现物理世界运动变快的情况,但是我们现在有了这个办法,我们只需要把我们定义的每秒执行次数修改为希望的最大值,可以是 144,也可以是 300。
四、注意事项
此解决方案可能并不是完美的解决方案,也许存在着各种我暂未发现的问题,读者请酌情参考。