GameLoop.md

游戏循环是游戏最核心的部分,所以翻译了一篇很好的讲游戏循环的文章。探讨了四种处理FPS和游戏更新的方式,然后分析了在不同配置硬件上的表现。

翻译:Game Loop

游戏循环(Game Loop)

每个游戏都包括一系列操作,比如获取用户输入,更新游戏状态,处理AI,播放音乐和音效,游戏渲染。这一列操作都要在游戏循环(Game Loop)里完成。 游戏循环事每个游戏的核心。在这篇文章里,我不会讲前面所说操作的细节,只关注游戏循环。下面是游戏循环一个最简单的形式:

1
2
3
4
5
6
bool game_is_running = true;

while( game_is_running ) {
update_game();
display_game();
}

上面游戏循环最大的问题就是没有处理时间,游戏就只是一直运行。在低配硬件上跑的慢,高配硬件上跑的快。为了使得在不同配合的硬件上跑的一样快,我们就需要处理时间。处理时间有很多种方式,我们会在后面的章节里讨论。首先,让我们解释一下两个后面都要用到的概念:

FPS: FPS是 Frames Per Second的缩写,也就是 帧数/秒。在上面代码里,也就是函数 display_game() 一秒运行多少次。 (25 FPS 就是 25帧/秒。)

Game Speed: Game Speed 也就是游戏的速度,它表示 游戏状态一秒更新的次数。或者换句话说: 函数update_game()一秒调用的次数。

FPS依赖于固定的 Game Speed

实现

一种简单处理时间问题的方法是 让游戏以稳定速度(25帧/秒)运行。具体的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const int FRAMES_PER_SECOND = 25; // FPS设置为25
const int SKIP_TICKS = 1000 / FRAMES_PER_SECOND; // 每次游戏循环固定的运行时间
// GetTickCount() 返回从游戏开始度过的时间,单位是毫秒
DWORD next_game_tick = GetTickCount();


int sleep_time = 0;
bool game_is_running = true;

while( game_is_running ) {
update_game();
display_game();

next_game_tick += SKIP_TICKS;
// 如果早早干完活,就去睡觉。
sleep_time = next_game_tick - GetTickCount();
if( sleep_time >= 0 ) {
Sleep( sleep_time );
}
else {
// Shit, we are running behind!
}
}

这个实现一个大大的好处就是:简单。因为你知道update_game()一秒调用25次,所以代码很直接就可以写出来。例如,在这种游戏循环里实现一个重播的功能很简单。如果你地游戏里没有使用随机数,你就可以将用户输入造成的改变写到日志里,然后直接重新播放他们。在你的测试硬件上,你可以调试 FRAMES_PER_SECOND 到一个理想值,但在不同配置的硬件上会发生什么,我们看一下。

低配硬件

如果硬件能跑25FPS,那没关系。但问题是,低配硬件可能跑步了25FPS。因为低配硬件跑的慢啊。更糟糕的情况是游戏有的模块跑的很慢,有的模块跑的速度还行。这时候上面处理时间的方式就使得游戏无法运行。

高配硬件

游戏在高配置的硬件上运行的很好,但是会浪费很多时钟周期。如果你地配置可以支持你运行300FPS,而你只让游戏跑25FPS,真的很浪费啊。而且,你还可能会错过很多牛逼的特效,特别对于快速移动的物体。另一方面,对于移动设备,这样做也会空空的消耗电池,浪费能源啊。

结论

让FPS依赖于固定的游戏速度,这种方案实现简单,游戏的代码也简单。但是有一些问题,如果FPS设置的高,低配硬件跑起来就有问题。FPS设置的低,高配硬件上就会浪费一些视觉效果。

游戏速度依赖于变化的FPS

实现

另一个实现游戏循环的方法就是让它运行速度尽可能的快。让FPS来主宰游戏的速度。这一帧和上一帧时间的差来更新游戏循环。

1
2
3
4
5
6
7
8
9
10
11
DWORD prev_frame_tick;
DWORD curr_frame_tick = GetTickCount();

bool game_is_running = true;
while( game_is_running ) {
prev_frame_tick = curr_frame_tick;
curr_frame_tick = GetTickCount();

update_game( curr_frame_tick - prev_frame_tick );
display_game();
}

代码看起来有点复杂,因为我们需要在调用update_game()的时候考虑两帧之间的时间差。其实也没有多难,第一眼看起来这是处理时间问题的理想方式。我看到很多聪明的程序员用这种方式来实现游戏循环。有些人可能希望在他们用这种方式处理时间问题之前看到我的文章。因为这种方式不管是在低配还是高配硬件上都有问题!

低配硬件

高配硬件

总结

(这部分我没看懂)。

固定游戏速度,最大化FPS

实现

我们第一个实现是 FPS依赖于固定的游戏速度,它在低配的硬件上跑有问题,因为游戏速度和帧率都会下降。一个可能的方案就是保持游戏状态更新的速率,减少渲染帧率。具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const int TICKS_PER_SECOND = 50;
const int SKIP_TICKS = 1000 / TICKS_PER_SECOND;
const int MAX_FRAMESKIP = 10;

DWORD next_game_tick = GetTickCount();
int loops;

bool game_is_running = true;
while( game_is_running ) {

loops = 0;
// 保证游戏状态更新,先不管渲染
while( GetTickCount() > next_game_tick && loops < MAX_FRAMESKIP) {
update_game();

next_game_tick += SKIP_TICKS;
loops++;
}

display_game();
}

游戏每秒钟稳定的更新50次,渲染是尽可能的快。(译者注:我发现游戏速度并不一定稳定在50次/秒。如果update_game()display_game()SKIP_TICKS时间内就完成了,那游戏循环就可以更快的运行了?)

固定的游戏速度独立于变化的FPS

实现

上面的实现可不可以更好? 我们发现 update_game() 得到的游戏状态和 display_game()渲染的状态时不一致的,这两个函数直接拿的游戏状态有一个时间差。那么该怎么改进呢,那就是用插值的方法得到比较准确的display_game()状态。实现如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const int TICKS_PER_SECOND = 25;
const int SKIP_TICKS = 1000 / TICKS_PER_SECOND;
const int MAX_FRAMESKIP = 5;

DWORD next_game_tick = GetTickCount();
int loops;
float interpolation;

bool game_is_running = true;
while( game_is_running ) {

loops = 0;
while( GetTickCount() > next_game_tick && loops < MAX_FRAMESKIP) {
update_game();

next_game_tick += SKIP_TICKS;
loops++;
}
// 下面的代码可以达到更好的渲染效果
interpolation = float( GetTickCount() + SKIP_TICKS - next_game_tick )
/ float( SKIP_TICKS );
display_game( interpolation );
}

总结

游戏循环比你想象的要复杂。我们考察了4中可能的实现方案,其中一个是你应该竭力避免的:方案2。
对于移动设备,固定的帧率是个简单方便的方案。但是如果你想要发挥硬件的能力,你最好让游戏循环中FPS独立于游戏速度,可以通过预测函数来实现。
如果你不想用预测函数,那么你可以用固定游戏速度,变化FPS的方案,但是你应该找到低配和高配设备之间平衡的游戏更新速率。

文章目录
  1. 1. 游戏循环(Game Loop)
    1. 1.1. FPS依赖于固定的 Game Speed
      1. 1.1.1. 实现
      2. 1.1.2. 低配硬件
      3. 1.1.3. 高配硬件
      4. 1.1.4. 结论
    2. 1.2. 游戏速度依赖于变化的FPS
      1. 1.2.1. 实现
      2. 1.2.2. 低配硬件
      3. 1.2.3. 高配硬件
      4. 1.2.4. 总结
    3. 1.3. 固定游戏速度,最大化FPS
      1. 1.3.1. 实现
    4. 1.4. 固定的游戏速度独立于变化的FPS
      1. 1.4.1. 实现
  2. 2. 总结
,