Skip to content

Latest commit

 

History

History
402 lines (245 loc) · 27.6 KB

03.2-Game Loop.md

File metadata and controls

402 lines (245 loc) · 27.6 KB

游戏循环

目的

实现用户输入和处理器速度在游戏行进时间上的分离。

动机

假如有哪个模式是本书无法删去的,那么非游戏循环模式莫属。游戏循环模式是游戏编程模式中的精髓。几乎所有的游戏都包含着它,无一雷同,相比而言那些非游戏程序却难见它的身影。

为了解循环模式是如何大有作为,我们先来快速回顾一下内存的发展史。在那个大家都还留着络腮胡的编程年代,程序工作起来就像你家里的洗碗机——你塞进一段代码给机器,按下按钮,等待,获得输出结果,完成。那就是批处理程序——活干完了,程序也就终止了。

注解

络腮胡:Ada Lovelace(十九世纪中期的数学家,世界上第一位程序设计师)和海军少将 Grace Hopper(二十世纪的海军将军和计算机科学家)都留着极具纪念意义的络腮胡

今天你依然见得到它们,幸运的是今天的我们不再使用穿孔卡片来写代码。Shell脚本,命令行程序,甚至是将一堆标记性语言(Markdown)转变成这本书的那些小Python脚本都属于批处理程序。

CPU探秘

最终程序员们意识到,这种把批处理代码丢给计算机,离开几个小时后再回来查看结果的方式在程序排错上简直慢得可怕。他们需要实时的反馈——于是交互式编程诞生了。最早的一批交互式程序就包括了像下面这样的游戏:

YOU ARE STANDING AT THE END OF A ROAD BEFORE A SMALL BRICK
BUILDING . AROUND YOU IS A FOREST. A SMALL
STREAM FLOWS OUT OF THE BUILDING AND DOWN A GULLY.

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

注解

洞穴冒险:上面这个被称为”洞穴探险”(Colossal Cave Adventure),史上首个冒险游戏。

你可以和这个程序面对面地交谈。它等待你的输入,并对你的操作进行响应。你也许还会回应它的反馈,你们就这么一唱一和,就像你在幼儿园里所学的那样。当轮到你时,机器就静静地呆在那儿啥也不做,就像:

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

注解

退出游戏:这个程序永远地循环着,因此你无法退出游戏。真实的游戏会改为诸如while (!done) 并通过设置done标志的值来退出游戏。我省去了这些来让例子看上去更简单。

事件循环

如果剥去现代的图形UI应用程序的外衣,你将发现它们和旧的冒险游戏是如此地相似。你的文字处理器通常什么也不做地呆着,直到你按下了某个键或者点击了鼠标:

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

这与文本指令的主要差异在于,事件循环程序等待用户的输入事件,包括鼠标点击和键盘按键。基本上它还是像旧的文字冒险游戏那样运作,阻塞着自己等待用户输入,这是个大问题。

不同于其他大多数软件,游戏即便在用户不提供输入时也一直在跑。假如你坐下来愣盯着屏幕,游戏也不该卡住。动画依旧在播放,各种效果也在闪动跳跃,假如你运气不佳,怪物们则可能在不断地啃咬你的英雄!

注解

空闲状态:多数事件循环都包含一个”空闲”(“idle”)事件以便在没有用户输入时也能间歇性地处理事务,这对于闪烁的光标或者一个进度条而言已经足够了,但对于游戏而言远远不够。

这是实际游戏循环的第一个关键点:它处理用户的输入,但并不等待输入。游戏循环始终在运转:

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

上面是最基本的结构,我们稍后再改善它。processInput()处理相邻两次循环调用之间的所有用户输入。接着update()让游戏(数据)模拟迭代一步,它执行游戏AI和物理计算(这是常见顺序)。最后render()对游戏进行渲染以将游戏内容展现给玩家。

注解

见名知意,你可能已经猜到了,update()方法里正是个使用[Update模式](03.3-Update Method.md)的好地方。

时间之外的世界

假如循环不因输入而阻塞,那么试问:它运转得多快呢?游戏循环的每次执行通过某些值更新了游戏状态,从游戏世界中某个人物的视角来看,他们的时钟便往前走了一个单位。

注解

帧:游戏循环的一次更新可以用术语”tick”(“tick”)或“帧”(“frame”)来描述。

与此同时,玩家的实际时间也在流逝。假如用现实时间来衡量游戏循环的速度,我们就得到了游戏的”帧率”(FPS,frames per second)。假如游戏循环得很快,FPS的值便很高,游戏将会运行得十分快而流畅。反之,游戏就会拖拉得像场定格电影(stop motion movie)。

对于现在这个粗糙的游戏循环,它以其尽可能快的速度在运转。两个因素决定了帧率:

1.循环每一帧要处理的信息量。复杂的物理运算,一堆对象的数据更新,许多图形细节等等都将让你的CPU和GPU忙个不停,这都会让一帧消耗更多的时间。

2.平台的底层速度。越快的芯片在相同时间内处理更多的代码。多核,多GPU,专用声卡以及操作系统的定时器都影响着你在一帧里能干多少事情。

秒的长短

在早期的电视游戏中,这个秒数因子是被固定的。假如你为红白机(NES)或者苹果二代电脑(Apple IIe)写游戏,你就必须对执行你游戏的CPU有深度的了解,而且你要能(且必须)为它写专门的代码。你需要好好考虑游戏的每一帧都该做些什么。

早些的游戏被精心地编写成每帧仅执行必须的任务,以便它能够在开发者期望的速度下运行。但假如你在更快或更慢一些的机器上跑这样的游戏,游戏本身会发生变速的现象。

注解

“turbo”:这也就是那些旧的个人电脑总带着“加速”(“turbo”)按钮的原因。新一代的个人电脑变得更快,它们将无法运行那些旧的游戏——因为这些游戏跑起来会变得很快。关闭加速按钮可以减缓它们的运行速度以便进行游戏。

而今很少有开发者对他们游戏所运行的硬件平台有深度的了解。取而代之的是,我们必须要让游戏智能地(在速度上)适配于多种硬件机型。

这就是游戏循环模式的另一个要点:这一模式让游戏在一个与硬件无关的速度常量下运行。

(游戏循环)模式

游戏循环在游戏过程中持续运转。每循环一次,它非阻塞地处理用户的输入,更新游戏状态,并渲染游戏。它跟踪流逝的时间并控制游戏的速率。

使用情境

对于设计模式,宁可不用也不能错用,故每一章你都能看到这一部分,以便让我们冷静下来思考。设计模式的目标可不是为了让你毫无节制地往你的程序里塞代码。

但这一模式有所不同。我拍着胸脯说你会在你的游戏里使用它。假如你使用了游戏引擎,那么这一模式无需你亲手实现,但它依然存在(于引擎中)。

注解

于我而言,这就是“引擎”和”库”之间的差别。使用库时,你自己把握游戏循环并在其中调用库函数,而使用引擎时它自己掌握着游戏主循环并调用你的代码。

你可能会想,我的回合制游戏应该不需要这家伙吧?不,尽管回合制游戏中,游戏状态总是随着双方回合的轮转而更新,但游戏中视觉和听觉的模块却也一直在运转,即便当你正在自己的回合犹豫着下一步行动时,动画和音效也依旧在运转。

使用须知

我们这里所讨论的循环是游戏中举足轻重的部分。正所谓程序90%的时间都花在10%的代码上——而游戏循环部分的代码就在这10%之中。你必须小心翼翼,并时刻考虑它的效率。

注解

谈论这些听起来不靠谱的统计,正是那些正牌机械或电气工程师不把我们当回事的原因吧!

你可能需要与操作系统的事件循环进行协调

假如你在一个带有图形UI和内置事件循环的操作系统或平台上构建游戏,那么在游戏运行时就有两个应用程序循环在执行。它们需要很好地协作。

有时你可以对其进行控制使得游戏只执行你的游戏循环。例如,你放弃珍贵的WindowsAPI来开发游戏,那么你的main()函数可以简化为一个游戏循环。其中你可以调用PeekMessage()处理并从操作系统中分派事件。不同于GetMessage(),PeekMessage()并不阻塞等待用户输入,所以你的游戏循环会持续地运转。

其他平台并不会轻易地让你退出事件循环。假如你以浏览器为平台,事件循环也已根植在浏览器执行模式的底层。其中事件循环负责显示,你同样要使用它来作为你的游戏循环。你可能会调用requestAnimationFrame()之类的函数以便浏览器回调回你的程序,并维持游戏的运转。

示例

做了这么长的介绍,游戏循环模式的代码已经不言而喻。我们将看到两个不同的实现版本,并比较它们的好坏。

游戏循环驱动着AI,渲染,和其他游戏系统,但这并不是模式本身的关键,所以这里我们将这些部分都虚化。实际上render(),update() 等这些部分留给读者作为练习(值得一试!)。

跑,能跑多快就跑多快

我们已经看到最简单的游戏循环:

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

它的问题在于你无法控制游戏运转的快慢。在较快的机器上游戏循环可能会快得令玩家看不清游戏在做些什么,在慢的机器上游戏则会一样变慢变卡。假如你还加入了重量级的模块或者进行AI,物理运算,游戏实际上会更卡。

小睡一会儿

我们首先来看看做一点小改动会如何。假设你希望让游戏以60帧/秒运行,也就是说你大概有16毫秒的时间来处理每一帧。假如你确实能够在这16毫秒以内进行所有的游戏更新与渲染工作,你就可以以一个稳定的帧率来跑游戏。你所需要做的就是处理这一帧,接着等待下一帧的到来,如下图:

注解

1000 ms/FPS = 毫秒每帧

代码如下:

while (true)
{
  double start = getCurrentTime();
  processInput();
  update();
  render();

  sleep(start + MS_PER_FRAME - getCurrentTime());
}

这里sleep()的方法确保即便过快地处理完一帧,游戏也不会运转得太快。但这办法在游戏运行过慢时并无作为。假如一帧的更新渲染时间超过了16毫秒,睡眠的时间为负——如果我们有让时间逆流的电脑,那许多事情都会很容易,遗憾的是并没有。

这时候游戏便慢下来。你可以通过减少每帧的工作量——减少图形处理量或者在AI上耍点小聪明,甚至直接砍了AI。但即便是在一台很快的机器上,这样做也会影响游戏的质量。

小改动大进步

让我们再试试稍复杂点的办法。我们目前的问题可以归结为:

1.每次更新游戏花去一个固定的时间值。

2.需要花些实际的时间来进行更新。(译者注:而这个“实际时间”是机器相关的)

假如第二步的时间长于第一步,那么游戏就会变慢。例如当需要16毫秒以上的时间来更新帧速为16毫秒每帧的游戏时,就可能无法维持运行速度。但假如我们能在每一帧中进行超过16毫秒的游戏状态更新,那么我们可以不那么频繁地更新游戏并且能够追赶上游戏的行进速度。

具体想法是计算这一帧距离上一帧的实际时间间隔以作为更新步长。帧处理花费的实际时间越长,这个步长也就越长(译者注:这个步长实际上等值于帧处理花费的实际时间)。这个办法使得游戏总会越来越接近于实际时间。他们称此为浮动时间迭代(或者变值时间迭代),代码如下:

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

在每一帧里,我们计算出自上次更新至今所花费的实际时间,即变量elapsed。当我们更新游戏状态时,将这个时间值传入。接下来游戏引擎必须负责将游戏世界更新到这个时间增量的下一个状态。

假设我们从屏幕左边向右边开了一枪。在固定时间迭代方法下,每帧中你根据子弹的速度移动它。在浮动时间迭代方法下,你通过时间差可以调整这个子弹的速度。随着迭代步长增加,子弹在每一帧越飞越快。于是子弹将在等同的实际时间中移动同样的距离,不论它花了20小步(较快的机器上)来完成的或4大步(较慢的机器上)来完成。

这办法看起来成功了:

1.这样一来游戏在不同的硬件上以相同的速率运行。 2.高端机器的玩家能够得到一个更流畅的游戏体验。

但,哎,我们面前还埋着个大坑:我们使得游戏变得不确定且不稳定。举个例子来说说我们自埋的坑吧:

注解

“确定性”表示每次你运行程序,假如给与同样的输入,那么你将得到完全一致的输出。如你所想,在具有确定性的程序上排错要容易多了——一旦找到导致错误的输入,那么它每次都能重现BUG。

计算机天生具有确定性,它们机械地执行程序。当混乱的现实世界参杂进来时它们就比变得不确定。例如,网络,系统时钟,线程定时器等都很大程度地依赖于程序控制之外的真实世界。

假设在一个双玩家的网络游戏中,Fred使用的是强大的游戏机而George用的是他祖母的古董PC机,我们之前讨论的子弹在他们的屏幕上飞来飞去。在Fred的机器上,游戏运行得飞快,也就是说每一帧处理所需的时间都极短。让我们把帧填满:假设在Fred的机器上子弹飞过屏幕共执行了50帧,那么George那苦逼的机器可能只能在这样的时间里执行5帧。

这意味着在Fred的机器上,游戏的物理引擎更新了子弹的位置50次,而George的机器只执行了5次。多数游戏采用浮点数,而它们会带来舍入误差。 你每次将两个浮点数相加,其返回的结果都可能出现左右偏差。Fred的机器做了比George机器10倍多的运算,所以他累计了更多的误差。在他们的机器上,子弹将在不同的位置消失。

这只是变时迭代方法可能导致的麻烦之一,问题还多着呢。为了以实时来运行,游戏的物理引擎会做实际物理规则的近似。为了防止这近似计算”炸飞上天”,系统进行了减幅运算。这个减幅运算被小心地安排成以某个固定时长迭代进行。因此,物理引擎也将变得不稳定。

注解

“炸飞上天”(“Blowing up”)在这里取字面意思。当物理引擎出问题时,游戏中的对象可能已完全错误的速度飞到天上去。

这个例子其不稳定性只是作为一个警醒我们的例子,它会引导我们更进一步。

把时间追回来

渲染,是引擎中通常不会受变时迭代影响的部分。由于渲染引擎表现的是游戏时间中的一瞬间,所以它并不关心距离上次渲染过去了多少时间。它只是把当前的游戏状态渲染出来而已。

注解

这很大程度上是成立的。诸如动态模糊等效果可能受到时间迭代的影响,但假如它们出现一些偏差,玩家往往也注意不到。

这一事实可以利用。我们将使用固定时间更新,因为它使得物理引擎和AI都更加稳定。但我们允许在渲染的时候进行一些机动的调整以释放出一些处理器时间。

它这样运作:距离上次的游戏循环已经过去了一段(真实的)时间。这一段时间就是我们需要模拟的游戏”当前时间”,以便赶上玩家的实际时间。我们通过一系列的定时步骤来实现它。代码大致如下:

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()函数的处理时间,即便在最慢的机器上也须如此。否则,你的游戏便跟不上现实时间。

注解

我把它就这么留在这,但你可以对其采取一些安全措施:当内部更新循环次数超出一定迭代上限时,让循环终止。这样游戏可能会变慢,但总比完全卡死好。

幸运的是,我们给予了自己一些喘息的空间。我们通过将渲染拉出更新循环之外来实现了这一点。这一方法解放了大量的CPU时间。最后的结果是,游戏通过定时步长更新,实现了在多硬件平台上以恒定速率进行游戏模拟。只不过在低端机器上玩家会看到游戏窗口里出现跳帧的情况。

留在两帧之间

眼下还留有一个问题,也就是残留的延迟。我们以固定的时间步长更新游戏,但在随机的时间点进行渲染。这意味着从玩家的角度来看,游戏常会在两次更新之间展现出完全相同的画面。

让我们看看时间线:

如你所见,我们的更新十分紧凑而固定,同时我们在任何可能的时间进行渲染。渲染的频度低于更新,且不稳定。这些都没有问题。问题在于我们并不总在更新的时间点进行渲染。看看第三次渲染,它介于两次更新之间:

设想一个子弹正横穿屏幕,首次更新时它在左侧,而第二次更新将它移动到屏幕右端。渲染在两次更新之间的某个时间点进行。以我们现在的实现方式,它将依然在屏幕左端。这意味着动作看起来会显得卡顿而不流畅。

顺带一提,我们实际上知道渲染时相邻两帧之间的间隔长度:也就是变量lag。当这个值小于更新时间步长时,我们跳出更新循环,而不是当lag为0时跳出。那么此时lag剩余的量呢?其实这个量就是我们进入下一帧的时间间隔。

当进行渲染时,我们将其传入:

render(lag / MS_PER_UPDATE);

注解

标准化:这里我们将它除以MS_PER_UPDATE是为了将值标准化。这样传入render()的值将在0(恰好在前一帧)到1(恰好在后一帧)之间(忽略更新时间步长)。通过这一方法,渲染引擎无需担心帧率。它仅仅处理0-1值之间的情况。

渲染器知道每个游戏对象的属性以及其当前速度。假设子弹在距离屏幕左侧20像素的地方并以400像素每帧的速率向右移动,假设我们在两帧的正中间渲染,传入render()的参数值即为0.5。故它绘制了下半帧的子弹飞行情况,也就是在距离屏幕左侧220的位置。锵!流畅的动作。

当然,可能会遇到推断错误的情况。当计算下一帧时,子弹可能撞上了障碍物,或者减速了等等。我们只是设想其前一帧的位置以及下一帧可能所在的位置并在两者之间插值地渲染其位置。但除非物理引擎和AI更新完成,否则我们并不能确切地知道子弹究竟会在哪。

故推断含有猜测的成分,有时将会出错。幸运的是,这些程度的修正通常并不引人注目。至少,比起你完全不做预测时的卡顿要不起眼得多。

设计决策

尽管这章已经写得够长了,但我还是留下了许多额外的问题。一旦你考虑诸如与显示刷新速率的同步,多线程,GPU等因素,实际的游戏循环将会变得复杂许多。在这样的高级层面上,你可能需要考虑以下这些问题:

谁来控制游戏循环,你还是平台?

这是你或多或少都要回答的一个问题。假如你的游戏嵌入在浏览器里,那么你往往无法自己来写游戏循环。浏览器自带基于事件的机制已经预先包含了这一循环。类似地,假如你使用了现成的游戏引擎,你也将依赖于它的游戏循环而不是自己来控制。

  • 使用平台的事件循环:

    • 这相对简单,你无须担心游戏核心循环的代码和优化问题。
    • 它与平台协作得很好。你显然无需担心它何时处理事件,如何捕获事件,或者如何处理平台与你的输入模型之间不匹配的问题等等。
    • 你失去了对时间的控制。平台将在其认为合适的时间调用你的代码。假如其频度无法达到你所预期,很遗憾。更糟的是,许多应用程序的事件循环在概念上的设计并不同与游戏——它们通常很慢并且不连续。
  • 使用游戏引擎的游戏循环:

    • 你无需自己写代码。写游戏循环需要不少技巧。由于其核心代码每一帧都会执行,其微小的错误或性能问题都可能对你的游戏产生很大的影响。具有一个紧凑靠谱的游戏循环是考虑使用现存引擎的重要原因。
    • 你不需要亲自来写。当然,坏消息是当出现一些与引擎循环不那么合拍的需求时,你却无法获得循环的控制权。
  • 自己写游戏循环:

    • 你来掌控一切。你可以做你想做的任何事。你可以完全依照你游戏的需求来设计它。
    • 你需要实现平台的接口。应用程序框架和操作系统通常希望你能划分出一些时间来供它们处理事件并做一些其他事。假如你掌控你程序的核心循环,那么它们便得不到这些时间。你显然周期性地将控制权交给系统以保证应用程序的框架不会混乱。

你如何解决能量耗损?

五年前我们无须讨论这个问题。那时游戏运行在电视设备或专用手持设备上。但随着智能手机,笔记本电脑,移动游戏的大力发展,你现在是该好好考虑这个问题了。一个跑起来很炫的游戏,但它却将玩家的手机变成一个3分钟将果汁蒸发的加热器,这可不是个让人们开心的好游戏啊。

现在你需要考虑不但要让你的游戏看来很棒,并且尽可能低减少CPU的使用率。当你完成了一帧中所做的所有工作时,你可能需要一个性能的上限来控制CPU进行休眠。

  • 让它能跑多快跑对快:

你最好只在PC游戏上这么做(尽管越来越多的玩家在笔记本上跑PC游戏)。你的游戏循环从不明确地让系统休眠。任何空余的循环都要用于避免FPS或者图形保真度的不稳定。

这给予你最好的游戏体验,但它会吞噬电量。假如玩家在笔记本电脑上玩,他们需要一个很好的供电设备。

  • 限制帧率:

移动游戏通常更关注游戏的运转质量而不是最高的画质。许多移动游戏会设置帧率上限(60或30FPS)。假如游戏循环在本时间片内已经完成了处理,剩余的时间它将休眠。

这给予了玩家一个足够好的体验并帮他们节省的设备能耗。

如何来控制游戏速度?

一个游戏循环具有两个关键部分:非阻塞的用户输入和帧时间适配。输入的问题好解决。所以关键在于你如何解决时间的问题。游戏可运行的平台数目是有限的,且多数游戏只能在其中几个平台上跑。如何适应平台变化便是关键。

注解

做游戏看起来像是人类的天赋之一,因为每创造出一个能进行计算的机器,我们最先做的就是在它上面开发游戏。PDP-1是一台主频2kHz的机器,仅有4096字的内存,即便如此Steve Russell和他的几个同学还是在它身上创造出了Spacewar!(译者注:世界上第一款真正意义上的娱乐性游戏,双人飞行射击游戏)。

  • 非同步的定时迭代:见我们的第一个代码样例。你只需要尽可能快地执行游戏循环。

    • 简单。这是这一情况的主要(呃,也是唯一的)优点。
    • 游戏速度直接取决于硬件和游戏的复杂程度。其主要缺点是假如硬件出现任何变化,将直接影响游戏速度。它就像带着死飞的游戏循环。
  • 同步的定时迭代:在复杂平台上所要做的下一步是让游戏进行定时迭代,同时在循环的末尾增加一个延时,或者是同步点以防游戏运行得过快。

    • 依然很简单。比起最简单的例子,只需要追加一行代码。在多数游戏中,你都希望进行同步。或许你会为图形引擎增加双缓存并让翻转缓存的操作与显示的刷新率同步。
    • 这是省电的。这是移动游戏十分在意的一点。你不会希望非必要地耗损用户的电量。通过几毫秒的休眠而不是将每一帧都塞满操作,你能够省电。
    • 游戏不会跑得很快。它的速度可能是填满操作的游戏循环的一半。
    • 游戏可能会跑得很慢。假如更新一帧的更新和渲染花去过多的时间,游戏反馈将会变慢。由于这一模式并不将更新与渲染分离,在没有进一步优化的情况下它将很容易显露出这一缺陷。不进行外置帧渲染并同步时,游戏会变慢。
  • 变时迭代:我在此提到诸多解决方法中的这一种以警示那些我曾经建议避免使用它的游戏开发者们。记住这个方法为何不好,总有助益。

    • 它能适应过快或过慢的硬件平台。加入游戏无法跟上现实速度,它将以越来越快的步伐跟上。
    • 它使得游戏变得不确定且不稳定。当然这才是根本问题。物理和网络模块往往在变时迭代下变得运转困难。
  • 定时更新迭代,变时渲染:我们提及的例子中的最后一个办法也是最复杂,最具适配性的一个。它以固定时间步长进行更新,但将渲染与更新分离,并让渲染来跟进玩家的时钟。

    • 它也能适应过快或过慢的硬件平台。因为游戏能够实时更新,所以游戏状态不会落后于现实时间。假如玩家拥有顶尖的机器,它则将带来一个十分流畅的游戏体验。
    • 它很复杂。它的主要缺陷在于实际的实现还有需要工作要做。你需要协调更新迭代的步长使其在高端机上足够小(足够平滑),同时在低端机上不会让游戏跑得太慢。

参考

  • 讲述游戏循环模式的一篇经典文章是来自Glenn Fiedler的“Fix Your Timestep“。没有它这一章节就没法写成现在这样。
  • Witters的文章 game loops 也值得一看。
  • Unity的框架具有一个复杂的游戏循环,这里有一个对其很详尽的阐述。