游戏主循环(Game Loop)

时间:2023-03-08 17:02:20
游戏主循环(Game Loop)

游戏主循环是游戏的心跳,一般使用while循环进行主动刷新。

一次循环由获取用户输入、更新游戏状态、处理AI、播放音乐和绘制画面组成。

这些行为可以分成两类:

update_game(); // 更新游戏状态(逻辑帧),一般不耗时

display_game(); // 更新显示(显示帧),耗时(场景越复杂越耗时)

几个概念

游戏速度:每秒调用update_game的次数。

FPS:即帧率;每秒调用display_game的次数。

可变显示FPS:即可变显示帧率,每秒调用display_game且显示画面有变化的次数。

最简单的游戏循环

bool game_is_running = true;
while( game_is_running )
{
  update_game();
  display_game();
}

该循环主要的问题是忽略了时间,游戏会尽情的飞奔,能有多快就运行多快

我们会看到在性能好的机器上,物体运动得更快一些

FPS依赖恒定的游戏速度

const int FRAMES_PER_SECOND = ;
const int SKIP_TICKS = / FRAMES_PER_SECOND;
DWORD next_game_tick = GetTickCount(); // 返回当前的系统已经运行的毫秒数
int sleep_time = ;
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 >= )
{
   Sleep( sleep_time );
  }
}

优点:重新播放游戏会显得简单(因为每帧时间间隔固定,只需要记录下每一帧游戏的状态,回放时按照25帧的速度播放即可)

配置差的机器的表现:到某些复杂的游戏场景时,display_game绘制会耗费大量时间,影响游戏输入和AI的响应,游戏会变得很慢(卡)

当场景变得简单时,游戏会加速运行,直到match到正常的步伐,然后稳定到25帧

牛逼的机器的表现:对于高速移动的物体,对视觉效果有一些影响(原来可以跑300帧,现在被强制只能运行25帧);另外,由于调用了Sleep,会比较省电一些

结论:FPS阈值定义得太高会使得配置差的机器机不堪重负,定义得太低则会使得高端硬件损失太多视觉效果

可变FPS决定游戏速度

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时需要考虑当前帧与上一帧的时间差。

配置差的机器的表现:到某些复杂的游戏场景时,display_game绘制会耗费大量时间,影响游戏输入和AI的响应,游戏会卡顿

然而在下一帧,就会强制match到正常的步伐,这样我们就会看到一些跳变(经常发生一些违反物理规律的怪事)

牛逼的机器的表现:也可能会出现问题,原因是update_game的调用次数存在差异;越牛逼的机器,update_game的调用次数越多。这种差异引起的浮点数误差,会导致致命的错误

结论:该游戏模型只能用于单机游戏和状态同步网游,不能用于帧同步网游

注:帧同步以帧为基本计时单位的一个同步方案,具体来说每台机器都必须运行一样的逻辑帧顺序(如同看视频,允许有缓冲,但是帧序列都是一样的);
每台机器只用发送其所有的输入事件给其他机器,就可以在其他机器上得到与本机相同的运行结果。
优点:简单、网络流量只与输入事件的多少有关;可以将多人单机游戏(黑盒)改造成网络游戏。缺点:不允许有随机逻辑,且反外挂困难

最大FPS和恒定速度游戏

const int TICKS_PER_SECOND = ;
const int SKIP_TICKS = / TICKS_PER_SECOND;
const int MAX_FRAMESKIP = ;
DWORD next_game_tick = GetTickCount();// 返回当前的系统已经运行的毫秒数
int loops;
bool game_is_running = true;
while( game_is_running )
{
  loops = ;
  while( GetTickCount() > next_game_tick && loops < MAX_FRAMESKIP)
{
   update_game();
   next_game_tick += SKIP_TICKS;
   loops++;
  }
  display_game();
}

配置差的机器的表现:当渲染帧率下降到5(TICKS_PER_SECOND/MAX_FRAMESKIP)即:loops>=MAX_FRAMESKIP,游戏才会变慢(卡)

当场景变得简单时,游戏会加速运行,直到match到正常的步伐,然后稳定到50帧

牛逼的机器的表现:游戏会以稳定的50帧速度更新,渲染速度也尽可能的快;但渲染速度超过了50帧时,有一些帧的画面将会完全相同,所以显示FPS实际上也等同于最快50帧

结论:如果定义过高的FPS阈值,会让配置差的机器吃不消,过低则会让牛逼的机器难以发挥性能

独立的可变显示FPS和恒定的游戏速度

const int TICKS_PER_SECOND = ;
const int SKIP_TICKS = / TICKS_PER_SECOND;
const int MAX_FRAMESKIP = ;
DWORD next_game_tick = GetTickCount();// 返回当前的系统已经运行的毫秒数
int loops;
float interpolation;
bool game_is_running = true;
while( game_is_running )
{
  loops = ;
  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 );
}

逻辑帧(玩家输入、AI)本身并不需要很高的速度,25帧就足够了

渲染则放任不管,任其飞奔;与前面的方案相比,display_game多了一个插值参数,我们需要在display_game里面实现一个接受插值参数的预言函数

逻辑帧为25帧,如果渲染时不时用插值计算,显示帧会被限定在25帧。25帧可以很好的展示游戏画面,不过对于高速的物体,更高的帧率会有更好的效果

所以,我们需要一个插值和预言函数让高速移动的物体在显示帧之间平滑的过度

插值和预言函数

游戏状态更新在一个恒定的帧率下运行着,当你渲染画面的时刻,很有可能就在两个逻辑帧之间

假设你已经第10次更新了你的游戏状态,现在你需要渲染你的场景,这次渲染就会出现在第10次和第11次逻辑帧之间

很有可能出现在第10.3帧的位置。那么插值的值就是0.3。举个例子说,一辆赛车以下面的方式计算位置

position = position + speed;

如果第10次逻辑帧后赛车的位置是500,速度是100,那么第11帧的位置就会是600. 那么在10.3帧的时候你会在什么位置渲染你的赛车呢?

显而易见,应该像下面这样:

view_position = position + (speed * interpolation)

注:position=500,speed=100,interpolation = 0.3

现在,赛车将会被正确地渲染在530这个位置。基本上,插值的值就是渲染发生在前一帧和后一帧中的位置。

你需要做的就是写出预言函数来预计你的赛车/摄像机或者其他物件在渲染时刻的正确位置。

你可以根据物件的速度来计算预计的位置。这些并不复杂。

对于某些预计后的帧中出现的错误现象,如某个物体被渲染到了某个物体之中的情况的确会出现。

由于游戏速度恒定在25帧,那么这种错误停留在画面上的时间极短,难以发现,并无大碍。

配置差的机器的表现:当渲染帧率下降到5(TICKS_PER_SECOND/MAX_FRAMESKIP),此时loops>=MAX_FRAMESKIP,游戏才会变慢

当场景变得简单时,游戏会加速运行,直到match到正常的步伐,然后逻辑帧稳定到25帧

牛逼的机器的表现:逻辑帧会保持25帧,插值的方案可以让游戏在高帧率中有更好的画面表现。

结论:最好的游戏主循环实现。不过,必须实现一个插值计算函数

整体总结

讨论了4个可能的实现方法,其中有一个方案是要坚决避免的,那就是可变FPS决定游戏速度的方案。

恒定的帧率对移动设备而言,可能是一个很好的实现;如果你想展示你的硬件全部的实力,那么最好使用独立的可变显示帧率和恒定的游戏速度的实现方案。

如果不想麻烦的实现一个预言函数,那么可以使用最大FPS和恒定的游戏速度的实现方案,唯一需要考虑的是找到一个合适的FPS阈值。

参考

http://www.koonsolo.com/news/dewitters-gameloop/

http://gameprogrammingpatterns.com/game-loop.html