假设在一个 60Hz 的设备上做一个动画,我怎么才能知道啥时候我可以绘制下一帧的内容;
有没有一种方法,我可以注册一个函数,这个函数在每一帧更新的时候都会调用一下,使得我可以绘制下一帧的内容
这只是学习用的,因为对于这方面,真的无从入手!
求指教!或者指个学习的明路也行!
目前我可以通过 Sleep
函数模拟等待一个帧的时间(1000 / 60)
#include <graphics.h>
#include <windows.h>
int main() {
initgraph(1080, 720);
int x = 200;
int y = 200;
fillcircle(x, y, 30);
for (int i = 0; i < 400; i += 2) {
Sleep(16);
cleardevice();
fillcircle(x + i, y, 30);
}
closegraph();
return 0;
}
首先你说的满帧是什么概念,相同的一个游戏,在3080 PC上能跑100 FPS, 在1060上 FPS 只有30,不同的硬件条件下帧数不同,所以不对满帧有个明确的定义,问题是没有办法解释也没有办法理解的,我权且认为你说的是以某个固定的帧率(比如说与显示器刷新率同步)运行你的游戏程序。
什么是帧率
游戏程序本质上是对真实世界(或者幻想世界)的模拟,现实世界基本上我们认为是连续的,但是电脑只能以离散的方式在一个个的时间点上驱动游戏,这个一般称为游戏循环
典型的结构像这样:
每次
update
都会更新游戏世界的进度,比如说游戏中某个实体拥有属性speed = 5
,每次update
会将它的属性x += 5
。render
本质上是获取当前的游戏状态绘制出来。在 1 秒钟内运行多少个游戏循环周期称为 FPS(frames per second)。
显然我们能得出一些结论:
FPS 通常由因素决定:
因此我们只能限制最大帧率,对于低帧率则无能为力。
比如说在我的电脑上
update()
需要至少 10 ms,render()
需要至少 6 ms,那么我的 FPS 最高只能约为 60,没有任何方法在同样的游戏逻辑,同样的渲染质量下获得更高的 FPS固定帧率
假设我们想把游戏的 FPS 固定在 60
如果使用游戏引擎或者框架,一般来说都有很简便的方法设置期望帧率,比如说 Unity 可以设置
targetFrameRate
,使用 SFML 的有setFrameLimit
考虑自行手动实现,有一些不同的方式。
一种简单方式是在每次循环末尾添加一个延迟, 如果期望是 60 FPS,我们必须限制游戏循环每次运行的时间不超过 1000/60 约等于 16 ms,大致的实现像这样:
添加在循环末尾的延迟可以确保我们不会更新的太快,比如说 1000 fps 的机器上速度为 5 的实体可能一瞬间跑出几个屏幕的距离,我们的人眼根本不会感知到有运动的物体存在。
但是重复一遍,不幸的是没有办法确保原来运行的很慢的代码让游戏慢下来。
这种情况下,能做的只能以某种方式减少一些计算时间以确保 FPS 可以达标。
有时候,在游戏中某一帧因为各种原因有了巨大的 lag 之后,我们希望捕获到这种状态并一些某种方式的补偿,一种可能的方式叫做
Variable Time Step
。还是举之前的例子,我们的游戏中有个实体每次更新时 x 坐标加 5 ,在一台设备上,程序以 60 FPS 运行,在另一台老设备上,只能以 30 FPS 运行,那么这个实体在老设备上的速度只有新设备上的一半。
我们可以引入一个变量,记录真实世界的时间流逝,用它来更新游戏世界。在
update
中,如果发现落后了 10 ms, 我们可以将游戏世界提前 10 ms, 这样以较低的更新频率保持在最新状态,大致代码像这样:update
中,使用elapsed
缩放实体的速度, 例如: elapsed = 16 ms,speed = 5, elapsed = 32 ms, speed = 10。这种方式的优点:
当然也有缺点:
我们都知道浮点数是无法精确表示的,而使用时间变量进行缩放一定会产生浮点数,不同的浮点数相加结果会有差别。这在一些需要精确数值的场景下可能会发生错误(比如物理引擎)。
举个例子,在 A 设备上,单位时间内游戏更新了一帧,这一帧让某个实体的 坐标加上了0.3, 在 B 设备上,相同的时间内游戏更新了两帧,经过时间缩放后,实体的坐标分别加上了0.1 和 0.2,多数语言实现中
0.1 + 0.2 != 0.3
,这种细小的差距经过累积后就产生了完全不同的结果。既然使用时间缩放不适用所有的场景,我们可以换个思路:
固定时间步长,让
update
去适应渲染。举个可能不是很恰当的例子:A 和 B 一起包饺子, A 擀一个饺子皮需要 15 s, B 包一个饺子需要一分钟, 那么 A 每分钟可以休息 45 s,正常情况下,A 1分钟只需要擀一张饺子皮就可以和 B 配合了。偶尔 A 一不小心打翻了面盆,需要花两分钟来清理。这时候,在下一分钟内,A 需要放弃休息时间制作 4 张饺子皮以追赶进度。
用代码可以这样表示:
解释一下:
lag 表示游戏世界的时间落后于现实世界的程序,然后内部使用一个 while 循环用以追赶,直到所有滞后的时间都消费掉了,这时候的
update
是以固定的准确的时间来进行的。直到赶上了现实时间,然后渲染我们的游戏世界。
其他
游戏循环中还有一些其他有趣的事实,不好展开,简单提一下:
有时候当实体的速度过快时,我们的碰撞检测会出问题,想象一下一个表示玩家的矩形包围盒与 表示墙壁的的矩形进行碰撞时,如果玩家的速度过快,第一次更新时,玩家在墙的左侧,第二次更新后,玩家在墙的右侧,玩家直接穿过了墙,我们永远也检测不到碰撞,这时候我们可以增加游戏的时间刻度来确保玩家与墙会有相交的状态,比如原来一次 update 增加100px,现在改为 一次 update 增加 20 px, 但是我们会运行 5 次 update 。(当然具体到这个问题,碰撞检测有其他的解决方案,比如说扫掠形状,Ray-casting等)。
表达能力有限,关于游戏循环的更多知识,参考这个,PS: 这本书在微信读书上有中文版。