目的
动机
要说有哪个模式是本书必须要涵盖的,非此模式莫属。游戏循环是“游戏编程模式”的典范。几乎每个游戏都有此模式,却又没有两个是完全相像的,而游戏之外的程序则鲜有使用。
让我们来踏上回忆的旅途,来看看此模式何其有用。在遥远的年代,计算机编程领域的所有人都蓄着大胡子,程序运作起来和你的洗碗机别无二致。你需要载入代码,按下按钮,然后静候结果。这就是批模式(batch mode)的程序——工作完成时,程序也随之结束。
时至今日这类程序也是存在的,但是我们不必在打卡机上写程序了,可喜可贺。Shell脚本,命令行程序,甚至是把一堆Markdown文件转换成本书格式的一小段Python脚本都属于批模式程序。
夜访CPU
最后程序员们意识到,要知悉程序中bug,他们需要跑到机房放下代码,在数小时后返回查看结果,这种方式极度缓慢。他们需要的是即刻反馈。交互式(interactive)程序就此诞生。游戏就是最初的几个交互式程序之一:
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);
}
(文字游戏和GUI程序的)主要区别在于,程序等待的并不是文本指令,而是用户输入事件——点击鼠标和按下按键。这和老式文字冒险游戏的运作方式仍基本相似,程序等待用户输入时会阻塞,这是个问题。
游戏和其他大多数软件不同,即便用户没有输入,游戏也是无时不刻不在运行。如果你什么也不干只是盯着屏幕,那游戏进程也不会冻结。动画还会播放,视觉效果还会存在,如果不巧,游戏里的怪还会继续攻击你。
大部分事件循环里都包括一个“空闲(idle)”事件,你可以在没有用户输入的情况下间断地做一些事情。对于闪烁光标或者进度条来说这就足够了,但对于游戏来说这种做法过于低级。
这时真实游戏循环中的首个关键点:处理用户输入,但是不在此等待。循环是一刻不停的:
while (true)
{
processInput();
update();
render();
}
后面我们会完善这里,但是基本框架已经有了。processInput()
处理自上次调用来的任何用户输入。然后update()
会把游戏模拟推进一步。这个方法会运行AI和物理逻辑(通常是这顺序)。最后,render()
绘制游戏,玩家就可以看到发生了什么。
你可能会从名字上猜出来,
update()
是使用Update Method模式的好地方。
时间之外
如果循环不在输入处阻塞,那么自会引出一个显而易见的问题:循环要多快呢?每一次游戏循环都会把游戏状态推进相等的距离。在游戏世界里的住民看来,就是里面的时钟朝前走了一下。
与此同时,玩家自己的时钟也在走动。如果我们用现实时间来表示游戏循环的速度,就会使用到游戏“每秒帧数(frames per second)”这个词。如果游戏循环快一点,那么FPS就会更高,游戏就更丝滑,更快速。如果循环速度低,游戏就会有卡顿,就像是定格电影一样。
我们现在这个循环比较粗略,机器能跑多快它就有多快,而影响帧率的因素有两个。第一个是每帧需要进行的工作量。复杂的物理计算,大堆的游戏对象,以及大量的图像细节都会榨干你的CPU和GPU,也会使得完成一帧消耗的时间更长。
第二个因素则是底层平台的速度。更快的芯片同样时间内能处理更多的代码。更多的核心数、GPU数、独立声卡以及系统调度器都会影响到每刻能完成的工作量。
一秒是多少秒
对于早期的游戏而言,第二个因素是固定的。如果你开发过NES或者Apple IIe游戏的话,你完全会知道游戏运行在什么CPU之上,而且你能(过去也确实这样做的)转为其开发代码。你只需要关心每一刻会执行多少工作就行了。
老一点的游戏的编码都是谨小慎微的,它们在每一帧里做的工作不多不少,恰好能够使游戏以开发者想要的速度来运行。但是如果你在一个更快或者更慢的机器上去玩同一个游戏,那么游戏本身就会加速或降速。
这就是老式PC会有“加速”按钮的原因了。速度更快的新PC玩不了老游戏,因为游戏会运行过快。关闭加速按钮会使机器运行速度降下来,从而老游戏也拥有可玩性了。
译者注在现代电脑上与加速按钮对等的东西也许就是限制CPU频率了,不过你可能还需要让CPU以单核甚至单线程形式运行才能玩得动很老的游戏。
而今天的开发者们鲜有机会能够知晓游戏会在什么样的硬件上运行。我们的游戏必须要智能地自适应各种各样的设备。
这正式游戏循环的另一项关键任务:以底层硬件无关的恒定速度来运行游戏。
模式
游戏循环在游戏流程中会持续运行。游戏会在循环的每一回合以非阻塞的形式处理用户输入,更新游戏状态,渲染游戏。游戏循环会追踪时间的流逝,控制游戏流程的速率。
何时使用
宁可不用,不可用错。这一章节通常是给读者浇盆冷水的。设计模式的目的并不是为了在代码里面塞满这些东西。
但是这个模式有些不同。我可以自信地讲,你一定会使用这个模式。如果你使用的是游戏引擎,那么你不用自己编写这个模式,引擎已经将其包纳。
也许你会觉得制作回合制游戏就不需要Game Loop模式了。但即使是这样,尽管游戏状态只在玩家回合执行完才会推进,游戏的视效音效状态则不是这样。动画和音乐即使在游戏“等待”你执行回合时也会运行。