目的

把游戏时间的进程从用户输入和处理器速度中解耦出来。

动机

要说有哪个模式是本书必须要涵盖的,非此模式莫属。游戏循环是“游戏编程模式”的典范。几乎每个游戏都有此模式,却又没有两个是完全相像的,而游戏之外的程序则鲜有使用。

让我们来踏上回忆的旅途,来看看此模式何其有用。在遥远的年代,计算机编程领域的所有人都蓄着大胡子,程序运作起来和你的洗碗机别无二致。你需要载入代码,按下按钮,然后静候结果。这就是批模式(batch mode)的程序——工作完成时,程序也随之结束。

时至今日这类程序也是存在的,但是我们不必在打卡机上写程序了,可喜可贺。Shell脚本,命令行程序,甚至是把一堆Markdown文件转换成本书格式的一小段Python脚本都属于批模式程序。

夜访CPU

最后程序员们意识到,要知悉程序中bug,他们需要跑到机房放下代码,在数小时后返回查看结果,这种方式极度缓慢。他们需要的是即刻反馈。交互式(interactive)程序就此诞生。游戏就是最初的几个交互式程序之一:

  1. YOU ARE STANDING AT THE END OF A ROAD BEFORE A SMALL BRICK
  2. BUILDING . AROUND YOU IS A FOREST. A SMALL
  3. STREAM FLOWS OUT OF THE BUILDING AND DOWN A GULLY.
  4. > GO IN
  5. YOU ARE INSIDE A BUILDING, A WELL HOUSE FOR A LARGE SPRING.

此游戏是Colossal Cave Adventure,首款冒险类型的游戏。

你能和程序进行实时交互。程序会等待你的输入,然后再把结果返回给你。你则在此之后进行回复,依次进行,就像是你曾在幼儿园学习到的一样。轮到你的时候,程序不会执行。比如:

  1. while (true)
  2. {
  3. char* command = readCommand();
  4. handleCommand(command);
  5. }

这是个无限循环,无法退出游戏。真正的游戏会使用while(!done),设置done来退出。为了使示例简单,我省略掉了这一部分。

事件循环

除去外衣表象之后,现代图像UI程序和老式冒险游戏具有惊人的相似之处。你的文本处理器通常情况下只是在静候按键或点击:

  1. while (true)
  2. {
  3. Event* event = waitForEvent();
  4. dispatchEvent(event);
  5. }

(文字游戏和GUI程序的)主要区别在于,程序等待的并不是文本指令,而是用户输入事件——点击鼠标和按下按键。这和老式文字冒险游戏的运作方式仍基本相似,程序等待用户输入时会阻塞,这是个问题。

游戏和其他大多数软件不同,即便用户没有输入,游戏也是无时不刻不在运行。如果你什么也不干只是盯着屏幕,那游戏进程也不会冻结。动画还会播放,视觉效果还会存在,如果不巧,游戏里的怪还会继续攻击你。

大部分事件循环里都包括一个“空闲(idle)”事件,你可以在没有用户输入的情况下间断地做一些事情。对于闪烁光标或者进度条来说这就足够了,但对于游戏来说这种做法过于低级。

这时真实游戏循环中的首个关键点:处理用户输入,但是不在此等待。循环是一刻不停的:

  1. while (true)
  2. {
  3. processInput();
  4. update();
  5. render();
  6. }

后面我们会完善这里,但是基本框架已经有了。processInput()处理自上次调用来的任何用户输入。然后update()会把游戏模拟推进一步。这个方法会运行AI和物理逻辑(通常是这顺序)。最后,render()绘制游戏,玩家就可以看到发生了什么。

你可能会从名字上猜出来,update()是使用Update Method模式的好地方。

时间之外

如果循环不在输入处阻塞,那么自会引出一个显而易见的问题:循环要多快呢?每一次游戏循环都会把游戏状态推进相等的距离。在游戏世界里的住民看来,就是里面的时钟朝前走了一下。

与此同时,玩家自己的时钟也在走动。如果我们用现实时间来表示游戏循环的速度,就会使用到游戏“每秒帧数(frames per second)”这个词。如果游戏循环快一点,那么FPS就会更高,游戏就更丝滑,更快速。如果循环速度低,游戏就会有卡顿,就像是定格电影一样。

我们现在这个循环比较粗略,机器能跑多快它就有多快,而影响帧率的因素有两个。第一个是每帧需要进行的工作量。复杂的物理计算,大堆的游戏对象,以及大量的图像细节都会榨干你的CPU和GPU,也会使得完成一帧消耗的时间更长。

第二个因素则是底层平台的速度。更快的芯片同样时间内能处理更多的代码。更多的核心数、GPU数、独立声卡以及系统调度器都会影响到每刻能完成的工作量。

一秒是多少秒

对于早期的游戏而言,第二个因素是固定的。如果你开发过NES或者Apple IIe游戏的话,你完全会知道游戏运行在什么CPU之上,而且你能(过去也确实这样做的)转为其开发代码。你只需要关心每一刻会执行多少工作就行了。

老一点的游戏的编码都是谨小慎微的,它们在每一帧里做的工作不多不少,恰好能够使游戏以开发者想要的速度来运行。但是如果你在一个更快或者更慢的机器上去玩同一个游戏,那么游戏本身就会加速或降速。

这就是老式PC会有“加速”按钮的原因了。速度更快的新PC玩不了老游戏,因为游戏会运行过快。关闭加速按钮会使机器运行速度降下来,从而老游戏也拥有可玩性了。

译者注在现代电脑上与加速按钮对等的东西也许就是限制CPU频率了,不过你可能还需要让CPU以单核甚至单线程形式运行才能玩得动很老的游戏。

而今天的开发者们鲜有机会能够知晓游戏会在什么样的硬件上运行。我们的游戏必须要智能地自适应各种各样的设备。

这正式游戏循环的另一项关键任务:以底层硬件无关的恒定速度来运行游戏。

模式

游戏循环在游戏流程中会持续运行。游戏会在循环的每一回合以非阻塞的形式处理用户输入,更新游戏状态,渲染游戏。游戏循环会追踪时间的流逝,控制游戏流程的速率。

何时使用

宁可不用,不可用错。这一章节通常是给读者浇盆冷水的。设计模式的目的并不是为了在代码里面塞满这些东西。
但是这个模式有些不同。我可以自信地讲,你一定会使用这个模式。如果你使用的是游戏引擎,那么你不用自己编写这个模式,引擎已经将其包纳。
也许你会觉得制作回合制游戏就不需要Game Loop模式了。但即使是这样,尽管游戏状态只在玩家回合执行完才会推进,游戏的视效音效状态则不是这样。动画和音乐即使在游戏“等待”你执行回合时也会运行。

谨记在心

示例代码

设计决策

另请参阅