C 怎么控制每帧绘制的内容?

假设在一个 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;
}
回复
阅读 473
2 个回答

首先你说的满帧是什么概念,相同的一个游戏,在3080 PC上能跑100 FPS, 在1060上 FPS 只有30,不同的硬件条件下帧数不同,所以不对满帧有个明确的定义,问题是没有办法解释也没有办法理解的,我权且认为你说的是以某个固定的帧率(比如说与显示器刷新率同步)运行你的游戏程序。

什么是帧率

游戏程序本质上是对真实世界(或者幻想世界)的模拟,现实世界基本上我们认为是连续的,但是电脑只能以离散的方式在一个个的时间点上驱动游戏,这个一般称为游戏循环

典型的结构像这样:

while(true) {
    process_input()
    update_game_state()
    render()
}

每次 update 都会更新游戏世界的进度,比如说游戏中某个实体拥有属性 speed = 5,每次 update 会将它的属性 x += 5
render 本质上是获取当前的游戏状态绘制出来。
在 1 秒钟内运行多少个游戏循环周期称为 FPS(frames per second)。

显然我们能得出一些结论:

  • 游戏世界的时钟与现实世界的是再以不同的速度前进
  • 在游戏循环中记录每次的时间就可以计算运行帧率(这是你的第一个问题)
  • FPS 通常由因素决定:

    1. 每次循环中需要做的工作量
    2. 硬件的运行速度

因此我们只能限制最大帧率,对于低帧率则无能为力。
比如说在我的电脑上 update() 需要至少 10 ms, render() 需要至少 6 ms,那么我的 FPS 最高只能约为 60,没有任何方法在同样的游戏逻辑,同样的渲染质量下获得更高的 FPS

固定帧率

假设我们想把游戏的 FPS 固定在 60

如果使用游戏引擎或者框架,一般来说都有很简便的方法设置期望帧率,比如说 Unity 可以设置 targetFrameRate,使用 SFML 的有 setFrameLimit

考虑自行手动实现,有一些不同的方式。

一种简单方式是在每次循环末尾添加一个延迟, 如果期望是 60 FPS,我们必须限制游戏循环每次运行的时间不超过 1000/60 约等于 16 ms,大致的实现像这样:

float ms_per_frame = 1000/60.0;
while(true) {
    float start = current_time();
    process_input();
    update();
    render();
    float elapsed = current_time() - start
    sleep(0, ms_per_frame - elapsed)
}

添加在循环末尾的延迟可以确保我们不会更新的太快,比如说 1000 fps 的机器上速度为 5 的实体可能一瞬间跑出几个屏幕的距离,我们的人眼根本不会感知到有运动的物体存在。

但是重复一遍,不幸的是没有办法确保原来运行的很慢的代码让游戏慢下来。

这种情况下,能做的只能以某种方式减少一些计算时间以确保 FPS 可以达标。

有时候,在游戏中某一帧因为各种原因有了巨大的 lag 之后,我们希望捕获到这种状态并一些某种方式的补偿,一种可能的方式叫做 Variable Time Step

还是举之前的例子,我们的游戏中有个实体每次更新时 x 坐标加 5 ,在一台设备上,程序以 60 FPS 运行,在另一台老设备上,只能以 30 FPS 运行,那么这个实体在老设备上的速度只有新设备上的一半。

我们可以引入一个变量,记录真实世界的时间流逝,用它来更新游戏世界。在 update 中,如果发现落后了 10 ms, 我们可以将游戏世界提前 10 ms, 这样以较低的更新频率保持在最新状态,大致代码像这样:

float lastTime = current_time();
while(true) {
    float current = current_time();
    float elapsed = current - lastTime;
    process_input();
    update(elapsed);
    render();
    lastTime = current;
}

update 中,使用 elapsed 缩放实体的速度, 例如: elapsed = 16 ms,speed = 5, elapsed = 32 ms, speed = 10。

这种方式的优点:

  • 在不同的硬件设备上游戏世界以相同的速度更新
  • 我的机器越快,我的 FPS 越高,就有更流畅的画面

当然也有缺点:

  • 使用逝去时间进行缩放会产生一些非确定的效果。
  • 因为浮点数的特性会导致物理模拟产生不同的结果。
  • 网络同步变得困难

我们都知道浮点数是无法精确表示的,而使用时间变量进行缩放一定会产生浮点数,不同的浮点数相加结果会有差别。这在一些需要精确数值的场景下可能会发生错误(比如物理引擎)。
举个例子,在 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 张饺子皮以追赶进度。

用代码可以这样表示:

float previous = current_time()
float lag = 0.0f
while(true) {
  float current = current_time()
  float elapsed = current - previous
  previous = current
  lag += elapsed

  process_input();
  while(lag >= ms_per_update) {
      update_game_state();
      lag -= ms_per_update;
  }
  render()
}

解释一下:
lag 表示游戏世界的时间落后于现实世界的程序,然后内部使用一个 while 循环用以追赶,直到所有滞后的时间都消费掉了,这时候的 update 是以固定的准确的时间来进行的。
直到赶上了现实时间,然后渲染我们的游戏世界。

其他

游戏循环中还有一些其他有趣的事实,不好展开,简单提一下:

  1. 前面的固定步长更新的思路某种程度上可以实现更新和渲染的解耦,比如我们可以将渲染放在另一个线程中。
  2. 固定步长加上多次更新可以解决一些碰撞检测的问题。
    有时候当实体的速度过快时,我们的碰撞检测会出问题,想象一下一个表示玩家的矩形包围盒与 表示墙壁的的矩形进行碰撞时,如果玩家的速度过快,第一次更新时,玩家在墙的左侧,第二次更新后,玩家在墙的右侧,玩家直接穿过了墙,我们永远也检测不到碰撞,这时候我们可以增加游戏的时间刻度来确保玩家与墙会有相交的状态,比如原来一次 update 增加100px,现在改为 一次 update 增加 20 px, 但是我们会运行 5 次 update 。(当然具体到这个问题,碰撞检测有其他的解决方案,比如说扫掠形状,Ray-casting等)。

表达能力有限,关于游戏循环的更多知识,参考这个,PS: 这本书在微信读书上有中文版。

我真的不知道你为什么要从这么基础的地方学起,因为有太多的动画工具和库已经把这些工作帮你封装好了,根本不需要你来操心帧率的问题,重新发明轮子大多数时间是没必要的,如果你是做轮子的当我没说。
你的问题以我对计算机绘图的粗浅了解来尝试回答一下,如果需要自己计算每帧的数据的化,最普遍的技术就是双缓冲,一个缓冲用于显示,另外一个缓冲用于绘制,绘制好后二者交换,这样能够避免绘制导致的画面闪烁。如果你每次都是绘制好->显示->绘制好->显示,这样单位时间内你显示了多少帧就是你的帧率。至于说怎么在下一帧更新前计算好下一帧,反正你没算好下一帧之前你一直显示当前的缓冲内容,所以你计算的快慢无非是帧率的高低而已。

推荐问题