游戏与普通程序最大的不同点在于:
游戏不像其他大多数软件,游戏即使在没有玩家输入时也继续运行。 如果你站在那里看着屏幕,游戏也不会冻结。动画会持续播放。视觉效果继续闪烁。 如果运气不好的话,怪物会继续暴揍你的角色。
那么维持这一切的必要条件是什么,在代码层面又是如何维持你的游戏世界不陷入时停的呢?
这个概念就是:循环
循环的意图
将游戏的进行和玩家的输入解耦,和处理器速度解耦。
事件循环
游戏即使在没有玩家输入时也继续运行。
这是真实游戏循环的第一个关键部分:它处理用户输入,但是不等待它。
while (true) {
processInput();
update();
render();
}
// processInput()处理上次调用到现在的任何输入。
// update()让游戏模拟一步。 运行AI和物理。
// render()绘制游戏,
什么是帧率
我们用实际时间来测算游戏循环运行的速度,就得到了游戏的“帧率”(FPS)。如果游戏循环的更快,FPS就更高,游戏运行得更流畅、更快。
两个因素决定了帧率:
1、每帧要做多少工作
复杂的物理,众多游戏对象,图形细节都让CPU和GPU繁忙,这决定了需要多久能完成一帧。
2、底层平台的速度
更快的芯片可以在同样的时间里执行更多的代码。 多核,GPU组,独立声卡,以及系统的调度都影响了在一定时间内能够做多少东西。
每秒的帧数
早期的游戏被仔细地编码,一帧只做一定的工作,开发者可以让游戏以想要的速率运行。
但是如果你想要在快些或者慢些的机器上运行同一游戏,游戏本身就会加速或减速。
一个游戏循环在游戏运行过程中被不断执行。 每一次循环,它无阻塞地处理玩家输入,更新游戏状态,渲染游戏。 追踪时间的消耗并控制游戏的速度。
优化上面的简易循环代码
上面代码的问题是无法控制游戏运行得有多快。
假设游戏需要以60FPS运行,那么每帧约等于16ms。只要用少于这个时长处理游戏内的所有内容,就可以以稳定的帧率运行。所需要做的就是处理这一帧,然后等待,直到处理下一帧。
while (true) {
double start = getCurrentTime();
processInput();
update();
render();
sleep(start + MS_PER_FRAME - getCurrentTime());
}
但如果每次循环消耗的时间超过16ms,那它永远也跟不上
需要解决的问题:
1、每次更新将游戏时间推动一个固定量。
2、消耗一定量的真实时间来处理它。
因此还可以基于上帧到现在有多少真实时间流逝来选择前进的时间。这一帧花费的时间越长,游戏的间隔越大。
double lastTime = getCurrentTime();
while (true){
double current = getCurrentTime();
double elapsed = current - lastTime;
processInput();
update(elapsed);
render();
lastTime = current;
}
每一帧,计算上次游戏更新到现在有多少真实时间过去了。在更新游戏状态时将其传入,然后游戏引擎内推进一定的时间量。
假设有一颗子弹跨过屏幕。 使用固定的时间间隔,在每一帧中根据它的速度移动它。 使用变化的时间间隔,根据过去的时间拉伸速度。 随着时间间隔增加,子弹在每帧间移动得更远。 无论是二十个快的小间隔还是四个慢的大间隔,子弹在真实时间里移动同样多的距离。
但这个方案也不合理:游戏不再是确定的了,也不再稳定。
游戏时间追逐真实时间
计算上一次游戏循环过去了消耗了多少真实时间。 为游戏的“当前时间”模拟推进相同长度的时间,以追上玩家的时间。
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只是更新游戏的间隔。 这个间隔越短,就需要越多的处理次数来追上真实时间。
// 可以通过限制内层循环的最大次数来保证游戏不会完全卡死
由于render()次数比update()次数更少,就有可能出现render()发生在两次update()之间。
因此,需要在lag不为零且lag比MS_PER_UPDATE更小时,跳出循环进行渲染。 lag的剩余量就是到下一帧的时间。
render(lag / MS_PER_UPDATE);
决策
使用平台的事件循环:
1、简单
不必担心编写和优化自己的游戏核心循环。
2、平台友好
你不必明确地给平台一段时间让它处理它自己的事件,不必缓存事件,不必管理任何平台输入模型和你的不匹配之处。
3、失去了对时间的控制。
使用游戏引擎的循环:
1、不必自己编写
编写游戏循环非常需要技巧。 由于是每帧都要执行的核心代码,小小的漏洞或者性能问题就对游戏有巨大的影响。 稳固的游戏循环是使用现有引擎的原因之一。
2、无法自己编写
代价就是如果引擎无法满足真正的需求,开发者也没法获得控制权。
自己写:
1、完全可控
可以做任何想做的事情。可以为游戏的需求订制开发。
2、需要与平台交互
应用框架和操作系统通常需要时间片去处理自己的事件和其他工作。 如果拥有应用的核心循环,平台就没有这些时间片了。 开发者得显式定期检查,保证框架没有挂起或者混乱。