游戏主循环是游戏的心跳,一般使用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

游戏主循环(Game Loop)的更多相关文章

  1. Cocos2d-x 动手实现游戏主循环

    因为Cocos2d-x封装的非常好,所以对于非常多新手,他们仅仅知道先new一个场景,在场景上加入布景或精灵,然后用Director的runWithScene便能够执行游戏了.假设给一个精灵加个动作, ...

  2. Cocos2d-x 3.2 学习笔记(十六)保卫萝卜 游戏主循环与定时器

    保卫萝卜~想法一直存在于想法,实战才是硬道理!有想法就去实现,眼高手低都是空谈.   一.游戏主循环GameSchedule      主循环是游戏处理逻辑,控制游戏进度的地方,处理好主循环是很重要的 ...

  3. [UE4]游戏主循环

    游戏的运行模型 理解游戏的运行模型,对处理很多游戏错误有非常大的帮助. 游戏是有一个主循环的.那么游戏主循环做了什么事情呢? 游戏主循环一次就表示一帧,游戏主循环包括:接受输入.处理游戏逻辑.渲染.S ...

  4. WordPress主循环(The Loop)函数have_posts(),the_post()详解

    WordPress中调用文章标题是the_title();调用文章内容时用到the_content();调用文章的作者时用到the_author();等等这些函数,都需要在主循环中使用,下面就介绍一下 ...

  5. 【Unity3D与23种设计模式】游戏的主循环——Game Loop

    游戏与其他软件最大的不同 就是游戏有Update逻辑 一般的软件是由"事件"驱动 因为它不会突然跑出来一只"兔子" 因此,只有游戏才有"帧" ...

  6. Vue+WebPack游戏设计:自动背景贴图和游戏主循环的实现

  7. cocos2d-x游戏引擎核心之三——主循环和定时器

    一.游戏主循环 在介绍游戏基本概念的时候,我们曾介绍了场景.层.精灵等游戏元素,但我们却故意避开了另一个同样重要的概念,那就是游戏主循环,这是因为 Cocos2d 已经为我们隐藏了游戏主循环的实现.读 ...

  8. master线程的主循环,后台循环,刷新循环,暂停循环

    InnoDB存储引擎的主要工作都是在一个单独的后台线程master thread中完成的.master thread的线程优先级别最高.其内部由几个循环(loop)组成:主循环(loop).后台循环( ...

  9. Cocos2dx源码赏析(1)之启动流程与主循环

    Cocos2dx源码赏析(1)之启动流程与主循环 我们知道Cocos2dx是一款开源的跨平台游戏引擎,而学习开源项目一个较实用的办法就是读源码.所谓,"源码之前,了无秘密".而笔者 ...

随机推荐

  1. 35 网络相关函数(三)——live555源码阅读(四)网络

    35 网络相关函数(三)——live555源码阅读(四)网络 35 网络相关函数(三)——live555源码阅读(四)网络 简介 5)NoReuse不重用地址类 6)initializeWinsock ...

  2. COGS 2416.[HZOI 2016]公路修建 & COGS 2419.[HZOI 2016]公路修建2 题解

    大意: [HZOI 2016]公路修建 给定一个有n个点和m-1组边的无向连通图,其中每组边都包含一条一级边和一条二级边(连接的顶点相同),同一组边中的一级边权值一定大于等于二级边,另外给出一个数k( ...

  3. 搭建JavaWeb服务器

    JDK安装可以参考 http://www.cnblogs.com/a2211009/p/4265225.html Tomcat安装可参考 1.由于服务器配置比较低综合考虑,选择ubuntu系统进行搭建 ...

  4. putty快速设置本地代理

    sudo plink -D 127.0.0.1:8888 -l root -P 443 -pw xxx 104.xxx.xxx.xxx

  5. ubuntu 13.10 skype登不上问题

    首先打开sources.list sudo gedit /etc/apt/sources.list 如果是13.10添加源: deb http://archive.canonical.com/ubun ...

  6. Node.JS初识

    对Node.JS的认识 1.Node 是一个服务器端 JavaScript 解释器: 2.Node 的目标是帮助程序员构建高度可伸缩的应用程序,编写能够处理数万条同时连接到一个物理机的连接代码.处理高 ...

  7. Light OJ 1068

    数位DP #include <cstdio> #include <cstring> using namespace std; ; ; long long n; int f[MA ...

  8. QListWidget

    1.失去焦点背景颜色,代码设置全选的时候,背景会是白色,需要设置失去焦点背景颜色.(设置焦点,会出现白转化成设置背景色,效果不好) QPalette p; p.setColor(QPalette::I ...

  9. static 修饰内部类

    static一般用来修饰成员变量或函数也修饰代码块,一般不能修饰类,但是可以修饰内部类,被修饰的内部类可以直接作为一个普通类来用,不需要创建一个外部类的实例,而普通内部类的引用需要创建一个外部类的实例 ...

  10. Effective C++ -----条款14: 在资源管理类中小心copying行为

    复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为. 普遍而常见的RAII class copying行为是:抑制copying(使用私有继承 ...