序列模式--游戏循环模式

理论要点

  • 什么是游戏循环模式:将游戏的进行和玩家输入解耦,和处理器速度解耦。即每个游戏必有的主循环update的实现。

  • 要点:关键点就是实现如何控制游戏的速度(帧率)和CPU负荷与游戏效果之间权衡。

  • 使用场合:任何游戏或游戏引擎都拥有自己的游戏循环,因为游戏循环是游戏运行的主心骨。

代码分析

1,下面我们试着一步步来实现一个游戏循环模式,看看我们游戏之所以能一直运行更新,底层到底是怎样驱使的呢?
先来看一个最简单的阻塞式循环:

while(true)
{
    //阻塞等待用户输入
    Event* event = waitForEvent();
    //处理用户输入
    dispatchEvent(event);
}

很明显,大多数游戏即使没有玩家输入时也继续运行。 如果你站在那里看着屏幕,游戏不会冻结,动画继续动着。因此真实的游戏循环应该是非阻塞式的:它处理用户输入,但是不等待它。

while(true)
{
    //玩家输入,没有输入也往下执行
    processInput();
    //更新一步游戏数据
    update();
    //绘制游戏
    render();
}

目前我们大部分游戏循环基本原理都是这种结构。
接下来我们就在这个基础框架上一步步完善。首先,第一个问题是这个循环到底运行多快?很明显不同硬件下它们的运行速度是不一样,在快速机器上,循环会运行的太快,玩家看不清发生了什么。 在慢速机器上,游戏慢的跟在爬一样,这肯定影响游戏体验。因此我们游戏循环另一个关键任务:不管潜在的硬件条件,以固定速度运行游戏。

2,我们先来增加一个简单的小修复如何。假设你想要你的游戏以60FPS运行。这样每帧大约16毫秒(1000 / 60)。 只要你用少于这个时长进行游戏所有的处理和渲染,就可以以稳定的帧率运行。 你需要做的就是处理这一帧然后等待,直到处理下一帧的时候,就像这样:
这里写图片描述
代码看上去像这样:

while (true)
{
  double start = getCurrentTime();
  processInput();
  update();
  render();

  sleep(start + MS_PER_FRAME - getCurrentTime());
}

如果它很快的处理完一帧,这里的sleep()保证了游戏不会运行太快。 如果你的游戏运行太慢,这无济于事。 如果需要超过16ms来更新并渲染一帧,休眠的时间就变成了负的。 如果计算机能回退时间,很多事情就很容易了,但是它不能。

3,我们接着上面的问题:某帧消耗的时间大于帧率,那么会导致游戏世界时间滞后,而且永远追不上。相当于多余的时间永远被消耗掉了。
如果这样,我们是不是可以考虑不用固定sleep延时,而直接把上帧到本帧的真实时间作为update的更新时间。它像这样:

double lastTime = getCurrentTime();
while (true)
{
  double current = getCurrentTime();
  double elapsed = current - lastTime;
  processInput();
  update(elapsed);
  render();
  lastTime = current;
}

这样的话,不管什么硬件update都是以真实时间来更新数据的,看上去已经没什么问题了,是吧。
但是,悲剧,这里有一个严重的问题: 游戏不再是确定的了(elapsed不确定),也不再稳定。 这是我们给自己挖的一个坑:

假设我们有个双人联网游戏,Fred的游戏机是台性能猛兽,而George正在使用他祖母的老爷机。 在Fred的机器上,游戏跑的超级快,每个时间间隔都很小。 比如,在射击游戏中,我们塞了50帧在子弹穿过屏幕的那一秒。 可怜的George的机器只能塞进大约5帧。
这就意味着在Fred的机器上,物理引擎每秒更新50次位置,但是George的只更新5次。 大多数游戏使用浮点数,它们有舍入误差。 每次你将两个浮点数加在一起,获得的结果就会有点偏差。 Fred的机器做了10倍的操作,所以他的误差要比George的更大。 同样的子弹最终在他们的机器上到了不同的位置。

这是使用变化时间可引起的问题之一,还有更多问题呢。 为了实时运行,游戏物理引擎做的是实际机制法则的近似。 为了避免飞天遁地,物理引擎添加了阻尼。 这个阻尼运算被小心地安排成以固定的时间间隔运行。 改变了它,物理就不再稳定。

4,上面例子这种不稳定性太糟了,它唯一存在原因是作为警示寓言,引领我们到更好的东西……
好,既然上面问题原因是由于更新时间不确定引起,那么我们有么有办法让这个时间稳定下来呢?看看下面这个实现:

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。 这个变量表明了游戏世界时钟比真实世界落后了多少,然后我们使用一个固定时间步长的内部循环进行追赶。 一旦我们追上真实时间,我们就渲染然后开始新一轮循环。 示例图类似这样:
这里写图片描述

5,我们接近完善只差一步了,注意没有,上面示例虽然更新时间已经固定了,但是渲染是任意时刻。这从玩家角度看,就好像这样:
这里写图片描述
这样我们就不总能在正确的时间点渲染, 看看第三次渲染时间,它发生在两次更新之间。想象一颗子弹飞过屏幕,第一次更新时,它在左边。 第二次更新将它移到了右边。 这个游戏在两次更新之间的时间点渲染,所以玩家期望看到子弹在屏幕的中间。 而现在的实现中,它还在左边。这意味着看上去移动发生了卡顿。

方便的是,我们知道渲染距离两次更新的时间(MS_PER_UPDATE),即lag变量存储。所有我们可以把渲染距离两帧之间时间百分比作为参数传给render:

render(lag / MS_PER_UPDATE);

这样之后,假设子弹在屏幕左边20像素的地方,正在以400像素每帧的速度向右移动。 如果在两帧正中渲染,我们会给render()传0.5。 它绘制了半帧之前的图形,在220像素。是吧,这样就能平滑的移动了。

当然,也许这种推断是错误的。 在我们计算下一帧时,也许会发现子弹碰撞到另一障碍,或者减速,又或者别的什么。 我们只是在上一帧位置和我们认为的下一帧位置之间插值。 但只有在完成物理和AI更新后,我们才能知道真正的位置。所以推断有猜测的成分,有时候结果是错误的。 但是,幸运的,这种修正通常不可感知。 最起码,比你不使用推断导致的卡顿更不明显。

好~游戏循环就先说到这里,结束!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值