20

如何让每秒显示帧数独立于游戏逻辑?这样无论显卡渲染速度有多快,游戏逻辑都以相同的速度运行。

4

8 回答 8

30

我认为这个问题揭示了对游戏引擎应该如何设计的一些误解。这完全没问题,因为它们是非常复杂的东西,很难做到;)

您的正确印象是您想要所谓的帧速率独立性。但这不仅仅指渲染帧。

单线程游戏引擎中的 Frame 通常称为 Tick。您处理输入、处理游戏逻辑并根据处理结果渲染帧的每个 Tick。

您想要做的是能够以任何 FPS(每秒帧数)处理您的游戏逻辑并获得确定性的结果。

在以下情况下会出现问题:

检查输入: - 输入是键:'W' 这意味着我们将玩家角色向前移动 10 个单位:

玩家位置 += 10;

现在,由于您每帧都这样做,如果您以 30 FPS 的速度运行,您将每秒移动 300 个单位。

但是,如果您改为以 10 FPS 运行,则每秒只能移动 100 个单位。因此,您的游戏逻辑与帧速率无关。

令人高兴的是,要解决这个问题并使您的游戏逻辑帧率独立是一项相当简单的任务。

首先,您需要一个计时器来计算每帧渲染的时间。这个以秒为单位的数字(因此完成一个 Tick 需要 0.001 秒)然后乘以您希望独立于帧速率的值。所以在这种情况下:

按住“W”时

playerPosition += 10 * frameTimeDelta;

(三角洲是“改变某事”的一个花哨的词)

因此,您的玩家将在单个 Tick 中移动 10 的一小部分,并且在整整一秒的 Ticks 之后,您将移动全部 10 个单位。

但是,当涉及到变化率也随时间变化的属性时,例如加速的车辆,这将下降。这可以通过使用更高级的积分器来解决,例如“Verlet”。

多线程方法

如果您仍然对您的问题的答案感兴趣(因为我没有回答它但提出了替代方案),那就是。将游戏逻辑和渲染分离到不同的线程中。它有它的缺点。足以让绝大多数游戏引擎保持单线程。

这并不是说只有一个线程在所谓的单线程引擎中运行。但是所有重要的任务通常都在一个中心线程中。诸如碰撞检测之类的事情可能是多线程的,但通常 Tick 的碰撞阶段会阻塞,直到所有线程都返回,并且引擎返回到单个执行线程。

多线程提出了一大类问题,甚至是一些性能问题,因为一切,甚至容器,都必须是线程安全的。并且游戏引擎一开始是非常复杂的程序,因此很少值得为它们增加多线程的复杂性。

固定时间步长方法

最后,正如另一位评论者所指出的,具有固定大小的时间步长,并控制您“步进”游戏逻辑的频率也是一种非常有效的处理方式,具有很多好处。

为了完整性,在此处链接,但其他评论者也链接到它: Fix Your Time Step

于 2008-08-20T04:41:16.140 回答
8

Koen Witters 有一篇关于不同游戏循环设置的非常详细的文章。

他涵盖:

  • FPS 依赖于恒定的游戏速度
  • 游戏速度取决于可变 FPS
  • 具有最高 FPS 的恒定游戏速度
  • 独立于可变 FPS 的恒定游戏速度

(这些是从文章中提取的标题,按可取性排序。)

于 2008-08-20T04:52:20.683 回答
4

你可以让你的游戏循环看起来像:

int lastTime = GetCurrentTime();
while(1) {
    // how long is it since we last updated?
    int currentTime = GetCurrentTime();
    int dt = currentTime - lastTime;
    lastTime = currentTime;

    // now do the game logic
    Update(dt);

    // and you can render
    Draw();
}

然后你只需要编写你的Update()函数来考虑时间差异;例如,如果你有一个物体以某种速度移动,那么每帧v更新它的位置。v * dt

于 2008-08-20T04:37:39.860 回答
2

那天有一篇关于翻转码的优秀文章。我想把它挖出来给你看。

http://www.flipcode.com/archives/Main_Loop_with_Fixed_Time_Steps.shtml

这是运行游戏的一个经过深思熟虑的循环:

  1. 单线程
  2. 在固定的游戏时钟
  3. 使用插值时钟尽可能快地处理图形

好吧,至少我是这么认为的。:-) 太糟糕了,在这篇文章之后进行的讨论很难找到。也许回程机器可以在那里提供帮助。

time0 = getTickCount();
do
{
  time1 = getTickCount();
  frameTime = 0;
  int numLoops = 0;

  while ((time1 - time0)  TICK_TIME && numLoops < MAX_LOOPS)
  {
    GameTickRun();
    time0 += TICK_TIME;
    frameTime += TICK_TIME;
    numLoops++;
// Could this be a good idea? We're not doing it, anyway.
//    time1 = getTickCount();
  }
  IndependentTickRun(frameTime);

  // If playing solo and game logic takes way too long, discard pending
time.
  if (!bNetworkGame && (time1 - time0)  TICK_TIME)
    time0 = time1 - TICK_TIME;

  if (canRender)
  {
    // Account for numLoops overflow causing percent  1.
    float percentWithinTick = Min(1.f, float(time1 - time0)/TICK_TIME);
    GameDrawWithInterpolation(percentWithinTick);
  }
}
while (!bGameDone);
于 2008-08-20T05:06:03.600 回答
0

Enginuity有一个稍微不同但有趣的方法:任务池。

于 2008-08-21T13:33:51.200 回答
0

在显示图形之前有时间延迟的单线程解决方案很好,但我认为渐进的方式是在一个线程中运行游戏逻辑,并在另一个线程中显示。

但是你应该以正确的方式同步线程;)实现需要很长时间,所以如果你的游戏不是太大,单线程解决方案就可以了。

此外,将 GUI 提取到单独的线程中似乎是一个很好的方法。您是否曾在 RTS 游戏中看到单位移动时弹出“任务完成”的消息?我正是这个意思 :)

于 2008-09-15T14:26:48.340 回答
0

这不涵盖更高的程序抽象内容,即状态机等。

可以通过调整帧时间间隔来控制运动和加速度。但是,比如在这个或那个之后 2.55 秒触发声音,或者在 18.25 秒之后改变游戏关卡等等。

这可以与经过的帧时间累加器(计数器)相关联,但是如果您的帧速率低于您的状态脚本分辨率,即如果您的更高逻辑需要 0.05 秒的粒度并且您低于 20fps,这些时间可能会被搞砸。

如果游戏逻辑在独立于 fps 的固定时间片上运行在单独的“线程”(在软件级别,我更喜欢这样做,或操作系统级别)上,则可以保持确定性。

惩罚可能是如果没有发生太多事情,您可能会在帧之间浪费 CPU 时间,但我认为这可能是值得的。

于 2010-01-29T17:35:49.763 回答
-2

根据我的经验(不多),杰西和亚当的回答应该让你走上正轨。

如果您想了解更多信息并深入了解其工作原理,我发现TrueVision 3D的示例应用程序非常有用。

于 2008-08-20T04:45:36.003 回答