intent

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

motivation

如果有一种这本书不能不讲的模式,那么就是这个模式。游戏循环(Game Loop)是游戏程序设计模式的精粹。几乎每个游戏都使用它,还并不完全一样,而相对的,游戏之外的程序很少使用这个模式。

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

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

interview with a cpu

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

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时间。最终结果就是游戏在不同的硬件上以恒定速度运行。只是在慢机器上,游戏会波动。
文章来源:手游 http://www.lyouxi.com/ 手游

©著作权归作者所有:来自51CTO博客作者wx5dd6683504a6a的原创作品,如需转载,请注明出处,否则将追究法律责任

更多相关文章

  1. 关于OpenGL游戏全屏模式的设置
  2. 不懂游戏类型?敢说你懂游戏音乐
  3. 2d游戏设计,pygame 游戏开发
  4. 概念火热下的云游戏,究竟有着怎样的技术内涵?
  5. 秒级去重:ClickHouse在腾讯海量游戏营销活动分析中的应用
  6. C语言猜数游戏代码
  7. HBase分享 | 云HBase之OpenTSDB时序引擎压缩优化
  8. 7个最佳CSS优化技巧,可缩短页面加载时间
  9. 时间断点! 酗酒驾车, 合法饮酒年龄政策到底保护了谁? 合法饮酒年

随机推荐

  1. Android消息通信之无所不能的第三方开源
  2. Android中shape定义控件的使用
  3. Android数据推送实现方案
  4. Android中String资源文件的format方法
  5. android初级
  6. Android 开机图片/文字/动画 修改
  7. Android震动---启动、循环、取消控制
  8. Android(安卓)Studio 快捷键
  9. Android(一) 安卓概述
  10. Android Mac开发Android推荐软件