把批处理代码丢给计算机,离开几个小时后再回来查看结果的方式,在程序排错上简直慢得可怕。他们需要即时反馈——于是交互式编程诞生了。
真实的游戏循环的第一个关键点:它处理用户的输入,但并不等待输入。游戏循环始终在运转:1
2
3
4
5
6while(true)
{
processInput();
update();
render();
}
游戏循环模式的另一个要点:这一模式让游戏在一个与硬件无关的速度常量下运行。
模式
一个游戏循环会在游戏过程中持续地运转。每循环一次,它非阻塞地处理用户的输入,更新游戏状态,并渲染游戏。它跟踪流逝的时间并控制游戏的速率。
使用情境
任何游戏里面都可以使用到它。
使用须知
你可能需要和操作系统的事件循环进行协调。
示例代码
跑,能跑多快就跑多快
我们已经看到最简单的游戏循环,它的问题在于你无法控制游戏运转的快慢。在较快的机器上游戏循环可能会快得令玩家看不清游戏在做什么,在慢的机器上游戏则会变慢变卡。
小睡一会儿
假如你希望让游戏以60帧/秒运行,也就是说你大概有16毫秒的时间来处理每一帧。假如你确实能够在这16毫秒以内进行所有的游戏更新与渲染工作,那么你就可以以一个稳定的帧率来跑游戏。你所需要做的就是处理这一帧,接着等待下一帧的到来。1
2
3
4
5
6
7
8
9while(true)
{
double start = getCurrentTime();
processInput();
update();
render();
sleep(start + MS_PER_FRAME - getCurrentTime());
}
小改动,大进步
我们目前的问题可以归结为:
- 每次更新游戏花去一个固定的时间值。
需要花些实际的时间来进行更新。
假如第二步的时间长于第一步,那么游戏就会变慢。那么我们可以不那么频繁地更新游戏并且能够追赶上游戏的进行速度。
具体想法是计算这一帧距离上一帧的实际时间间隔以作为更新步长。帧处理花费的实际时间越长,这个步长也就越长。这个办法使得游戏总会越来越接近于实际时间。他们称此为变值时间步长(或者浮动时间步长)1
2
3
4
5
6
7
8
9
10double lastTime = getCurrentTime();
while(true)
{
double current = getCurrentTime();
double elapsed = current - lastTime;
processInput();
update(elapsed);
render();
lastTime = current;
}这样一来,游戏可以在不同的硬件上以相同的速率运行。
- 高端机器的玩家能够得到一个更流畅的游戏体验。
但存在一个严重的潜在问题:我们使得游戏变得不确定且不稳定。物理引擎也将变得不稳定。
把时间追回来
渲染,通常是游戏引擎中不会变时步长影响的部分。由于渲染引擎表现的是游戏时间中的一瞬间,所以它并不关心距离上次渲染过去了多少时间。它只是把当前的游戏状态渲染出来而已。
这一事实可以利用。我们将使用固定时长更新,因为它使得物理引擎和AI都更加稳定。但我们允许在渲染的时候进行一些灵活的调整以释放一些处理器时间。
它像这样运作:距离上次的游戏循环已经过去了一段真实的时间。这一段时间就是我们需要模拟游戏的“当前时间”,以便赶上玩家的实际时间。我们通过一系列的固定步长来实现它。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16double 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();
}
常量MS_PER_UPDATE只是我们更新游戏的间隔。间隔越大,游戏跳帧越明显。但要注意的是被让它过短,你必须保证这个时间步长大于每次update()函数的处理时间。
我们给予了自己一些喘息的空间。通过将渲染拉出更新循环之外来实现这一点。
留在两帧之间
还有一个问题,就是残留的延迟。渲染有可能在两次更新之间。当进行渲染时,我们将其传入:1
render(lag / MS_PER_UPDATE);
这样在绘制的时候,就可以知道物体偏移的值为多少,可以准确描绘出物体的位置。
设计决策
你可能需要考虑这些问题。
1. 谁来控制游戏循环,你还是平台
使用平台的事件循环
- 你无需担心游戏核心循环的代码和优化问题。
- 它与平台协作得很好。无需担心它何时处理事件,如何捕获事件,或者如何处理平台与你输入模型之间不匹配的问题等。
- 你失去了对时间的控制。更糟的是,许多应用程序的事件循环在概念上的设计并不同于游戏——它们通常很慢并且断续。
使用游戏引擎的游戏循环
- 你无需自己编写。
- 坏消息是当出现一些与引擎循环不那么合拍的需求时,你无法获得循环的控制权。
自己编写游戏循环
- 掌控一切。
- 你需要实现平台的接口。
2. 你如何解决能量损耗
移动平台的发展需要你考虑不但要让你的游戏看来很棒,并且应尽可能地减少CPU的使用率。当完成了一帧中需要处理的所有工作时,你可能需要一个性能的上限来控制CPU进行休眠。
- 让它能跑多快跑多快。
你最好只在PC游戏上这么做。这样一来,任何空余的循环都要用于避免FPS或者图形保真度的不稳定。这可能给予你最好的游戏体验,但它会消耗更多的电量。 - 限制帧率
设置帧率上限(30 FPS或60 FPS)假如游戏循环在本时间片内已经完成了处理,那么剩余的时间它将休眠。
这给予了玩家一个足够好的体验并帮他们节省了电池能耗。
3. 如何控制游戏速度
游戏循环有两个关键部分:非阻塞的用户输入和帧时间适配。
非同步的固定时间步长
- 简单。
- 游戏速度直接受硬件和游戏复杂度的影响。其主要缺点是假如出现任何变化,将直接影响游戏速度。游戏速度受游戏循环影响。
同步的固定时长
- 依然很简单
- 这是省电的
- 游戏不会运行得很快
- 游戏可能会跑得很慢(假如一帧的更新和渲染花去过多的时间)
变时步长
- 它能适应过快或过慢的硬件平台
- 它使得游戏变得不确定且不稳定
定时更新迭代,变时渲染
- 它也能适应过快或过慢的硬件平台
- 它更复杂。它的主要缺陷在于实际的实现还有更多的工作要做。