《游戏程序设计模式》 2.2 - 游戏循环

intent
程序员

    把用户输入、处理器速率与游戏时间解耦合。
shell

motivation编程

    若是有一种这本书不能不讲的模式,那么就是这个模式。游戏循环(Game Loop)是游戏程序设计模式的精粹。几乎每一个游戏都使用它,还并不彻底同样,而相对的,游戏以外的程序不多使用这个模式。
windows

    为了看它到底多有用,咱们快速回忆下。在过去的电脑编程中,程序的工做就行洗碗机。你倾倒一大堆代码进去,按一个按钮,等着,而后获得结果。完毕。这些是批处理程序-一旦工做完成,程序结束。
设计模式

    今天你仍然能看到它,只是没必要写到打孔卡上了。shell脚本,命令行,甚至把一堆markdown变成这本书的Python小脚本都是批处理程序。
api

interview with a cpu
浏览器

    最终,程序员意识到把一批代码留在办公室,几个小时后回来取结果是一个找出程序bug的很可怕很慢的方法。他们想要即时反馈。交互式程序出现了。首先出现的一部分交互式程序就是游戏:markdown

YOU ARE STANDING AT THE END OF A ROAD BEFORE A SMALL BRICKBUILDING . AROUND YOU IS A FOREST. A SMALLSTREAM FLOWS OUT OF THE BUILDING AND DOWN A GULLY.网络

> GO IN函数

YOU ARE INSIDE A BUILDING, A WELL HOUSE FOR A LARGE SPRING.

    你会有一个与程序的实时对话。它等待输入,而后响应。而后你回复,如此反复。当轮到你时,它什么都不作。就像:

while (true)
{
  char* command = readCommand();
  handleCommand(command);
}

Event loops

    现代图形应用,若是你剥掉它的外壳,与之前文字冒险游戏是同样的。文本处理器在你按下一个键或点击一些东西以前,什么都不作:

while (true)
{
  Event* event = waitForEvent();
  dispatchEvent(event);
}

    主要的不一样就是text command换成了user input event-鼠标点击和键盘事件。它仍然像文字冒险游戏,程序会由于等待输入而阻塞,这是个问题。

    不像其它大多数软件,游戏在没有输入的状况下仍然运行。若是你盯着看,游戏画面不会冻结。动画会一直播放。视觉效果飞舞闪烁。若是你不走运,怪物会啃你的英雄。

    这是游戏循环的第一个关键部分:它等待输入,可是不能阻塞。循环老是继续:

while (true)
{
  processInput();
  update();
  render();
}

    后面咱们将会改进它,可是基本步骤仍是都在的。processInput处理上次调用以来的输入。update更新一次游戏。它处理AI和物理检测(一般按此顺序)。最后render绘制游戏,这样玩家就知道发生了什么。

a world out of time

    若是循环不会由于输入阻塞,那么将会致使一个明显的问题:以多快的速度循环?每一次游戏循环会更新必定量的游戏状态。从游戏中居民角度来看,它们的时钟已经向前走了一下。

    同时玩家的时钟也在走。若是以真实时间测量游戏循环的次数,咱们就获得了“每秒帧数”(fps)。若是游戏循环快,fps就高,游戏运行平滑流畅。若是慢,游戏就会抽搐像定格动画。

    经过原始的游戏循环,它能尽量快地运行,影响帧率的有两个因素。第一个是,每一帧要作多少工做。复杂的物理计算,大量的游戏对象,和许多图像细节会使你的CPU和GPU忙碌,会花费更长时间完成一帧。

    第二个是,底层平台的速度。更快的芯片能够在相同时间处理更多代码。多核CPU,GPU,专用音频硬件和操做系统的调度,都会影响一帧的工做量。

seconds per second

    在早期的视频游戏中,第二个因素是固定的。若是你为NES或APPLE IIe写游戏,你须要确切知道CPU型号,而后专门为其编码。全部你须要担忧的是,每一帧能作多少工做。

    旧的游戏被当心编码,每一帧作足够的工做使能够以须要的速度运行。若是你在一个更快或更慢的机器上运行游戏,游戏速度会加快或减慢。

    如今,不多开发者知道游戏运行的硬件。相反,游戏必须智能地适应不一样的设备。

    这就是另外一个关键的部分:游戏无论什么设备都要以固定速度运行。

the pattern

    游戏循环在游戏运行中会持续不断的执行。每一次循环,它不阻塞的处理用户输入,改变游戏状态,渲染游戏。它追踪时间的流逝控制游戏的速度。

when to use it

    使用错的模式比不使用更糟,因此这章正常提醒不要过分热情。设计模式的目标不是尽量将模式塞满代码。

    可是这个模式不一样。我能够确定你会使用这个模式。若是你使用一个游戏引擎,即便不本身写,它仍然被使用了。

    你可能觉得若是你写一个回合制游戏不会用到它。即便游戏状态不变,视觉的和音频的部分也会更新。动画和音乐都会运行,当游戏等待玩家回合时。

keep in mind

    咱们这里讨论的是游戏最重要的一部分代码。有句话说“90%的时间花费在10%的代码上”。游戏循环的代码绝对在那10%中。注意这些代码,注意它的效率。

you may coordinate with the platform's event loop

    若是你为一个有内置消息循环os或平台写游戏,你会有两个循环。你须要使两个协调运行。

    有时,你能够掌控只使用你本身的循环。例如,若是你用windows api写游戏,你的main只能有一个循环。里面,你能够调用PeekMessage处理分发系统消息。不像GetMessage,PeekMessage获取用户输入不会阻塞,你的循环会一直运行。

    其余平台不会让你轻易退出消息循环。若是你的目标是浏览器,消息循环是深深地内置在执行模型里的。你要使用内置循环做为循环。你会调用相似requestAnimationFrame函数,这个函数调用你的代码,保证游戏运行。

sample code

    对于这么长的介绍,游戏循环的代码其实很是直白。咱们将会看看几个变种,分析优势和缺点。

    游戏循环驱动AI,绘制和其它游戏系统,可是这不是这个模式的重点,因此咱们直接调用虚构的函数。实现render,update还有其它的留给读者当作练习。

run,run as fast as you can

    咱们已经看过最简单的游戏循环:

while (true)
{
  processInput();
  update();
  render();
}

    这个的问题是你没法控制游戏循环的速度。在快机器上,它运行的很快。在慢机器上,它运行的像龟速。若是,你在一帧还有大量工做,像ai或者物理等,要作,那么还会更慢。

take a little nap

    第一个变种,咱们添加一个简单的修改。假设你想让游戏有60fps。一帧有16毫秒。只要你能够在这时间内完成游戏处理和绘制的工做,你就能够保证一个稳定的帧率。全部你须要作的就是处理一帧,等待下一帧的绘制,就像:

    

    代码像这样:

while (true)
{
  double start = getCurrentTime();
  processInput();
  update();
  render();
  sleep(start + MS_PER_FRAME - getCurrentTime());
}

    sleep保证了,若是一帧处理的很快,循环不会执行太快。可是,若是游戏运行太慢,它就毫无用处。若是update和render花费时间超过16ms,sleep时间将会是负值。若是,咱们能使电脑时间回退,一切都会很简单,很惋惜,咱们不能。

    相反,游戏慢下来了。你能够经过减小一帧的工做量解决此问题-减小图形和特效或者简化AI。可是,这会影响游戏质量,甚至在快机器上。

one small step,one giant step

    让咱们尝试一些更复杂的方法。咱们的问题基本上归结为:

    1.每一次update都会更新必定量的游戏时间

    2.会花费必定量的现实时间来处理update

    若是,第二步比第一步用时长,游戏就会慢下来。若是咱们想经过16ms来更新超过16ms的游戏内容,那么咱们将没法保持。可是,咱们能够经过超过16ms的时间,更新超过16ms的游戏内容,下降update的频率,这样仍能保持。

    主意就是根据自上一帧依赖通过的现实时间来更新游戏时间。一帧须要的时间越长,游戏更新的时间也就越长。游戏老是能跟上现实时间,由于它一次更新的游戏时间就是根据现实时间来的。它们被称为可变或流动时间步长。像这样:

double lastTime = getCurrentTime();
while (true)
{
  double current = getCurrentTime();
  double elapsed = current - lastTime;
  processInput();
  update(elapsed);
  render();
  lastTime = current;
}

    每一帧,咱们计算从上一帧以来,流逝了多少现实时间。当咱们更新游戏状态,咱们将这个时间传进去。引擎根据这个时间更新游戏。

    假设有一颗子弹从屏幕射过。经过固定时间步长,每一帧,子弹根据速度移动。经过可变时间步长,你能够根据流逝的时间缩放子弹速度。随着时间步长变大,子弹一帧移动的距离也会变大。子弹将会在相同现实时间内经过屏幕,不论是20小步仍是4大步。这看起来像个胜利者:

  •     游戏以一致的速率运行在不一样的硬件上。

  •     玩家使用快机器会获得更流畅的效果。

    可是,有一个潜伏的严重问题:游戏不肯定也不稳定。这里有一个陷阱:

    假设有一个二人网络游戏,fred有一个高性能游戏机,george有一个老古董pc。上述子弹从两人的屏幕上飞过。在fred的机器上,游戏运行很快,因此每一个时间步长很小。咱们假设,子弹用50帧穿过屏幕。在George的机器上可能只有5帧。

    这说明在fred的机器上,物理引擎更新子弹位置50次,可是George只有5次。大多数游戏使用浮点数,容易产生舍入偏差。每一次你相加两个浮点数,你获得的答案会有一点偏差。fred计算的次数是George的10倍,因此fred的偏差会比George大。同一个子弹在不一样的机器上会到达不一样的位置。

    这只是可变时间步长致使的一个棘手问题,还有不少问题。为了以现实时间运行,游戏物理引擎逼近真实力学定律。为了使模拟不飞起,会使用阻力。阻力当心地调到一个肯定时间步长。步长不一样,物理就变得不稳定。

    不稳定是很恶心的,这里的例子只是一个反面例子,这引导咱们走向更好……

play catch up

    不受可变时间步长影响的部分一般是渲染。由于渲染引擎捕获的是一瞬,并不关心通过了多长时间。它绘制碰巧出现的东西。

    咱们能够利用这个事实。咱们将会以固定时间步长更新游戏,由于这样更简单也更稳定。可是,什么时候渲染能够有灵活性,为了释放处理器时间。

    就像这样:必定量的现实时间从上一帧流逝。这就是咱们须要模拟的游戏时间,以遇上现实时间。咱们以固定时间步长作这些事。就像这样:

double previous = getCurrentTime();
double lag = 0.0;
while (true)
{
  double current = getCurrentTime();
  double elapsed = current - previous;
  previous = current;
  lag += elapsed;
  processInput();
  while (lag >= MS_PER_UPDATE)
  {
    update();
    lag -= MS_PER_UPDATE;
  }
  render();
}

    还有一些东西。在每一帧开始,咱们更新lag根据流逝的现实时间。这个用来计算游戏时间落后现实时间多少。咱们再写一个内部循环更新游戏,一步是固定时间,直到遇上现实时间。一旦咱们要遇上,咱们渲染,而后从头再来。你能够想象成这样:

    

    注意,这里的时间步长再也不是可见的帧。MS_PER_UPDATE是咱们更新游戏的粒度。步长越短,想遇上现实时间须要处理的时间越长。所需时间越长,游戏波动越大。理想状况下,你想它很短,快过60fps,这样游戏在快机器上能够模拟得高保真。

    可是不能过短。你必须确保时间步长大于update所需的时间,甚至在最慢的机器上。不然,你的游戏不可能赶得上现实时间。

    幸运的是,咱们有一些喘息的空间。诀窍是,把渲染从update中拿出来。这将节省大量cpu时间。最终结果就是游戏在不一样的硬件上以恒定速度运行。只是在慢机器上,游戏会波动。

(未完)

相关文章
相关标签/搜索