如何将游戏逻辑与显示分离?

20

如何使显示帧率与游戏逻辑独立?这样游戏逻辑就可以以相同的速度运行,无论视频卡能够渲染多快。

8个回答

31

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

你有一个正确的印象,就是你想要所谓的帧率独立性。但这并不仅仅是指渲染帧。

在单线程游戏引擎中,帧通常称为 Tick。每次 Tick 时,你会处理输入、处理游戏逻辑,并根据处理结果渲染一个帧。

你想要做的是能够以任何 FPS(每秒帧数)处理你的游戏逻辑,并且具有确定性结果。

以下情况会导致问题:

检查输入: - 输入为键:'W',表示我们将玩家角色向前移动 10 单位:

playerPosition += 10;

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

但是如果你改为以 10 FPS 运行,你每秒只能移动 100 个单位。因此,你的游戏逻辑并不是帧率独立的。

幸运的是,解决这个问题并使你的游戏逻辑帧率独立是一个相当简单的任务。

首先,你需要一个计时器,它将计算每一帧渲染所需的时间。这个数字以秒为单位(因此每个 Tick 完成需要 0.001 秒)然后乘以你想要实现帧率独立性的任何内容。所以在这种情况下:

按住 'W' 键

playerPosition += 10 * frameTimeDelta;

(Delta 是“某事物的变化量”的花哨说法)

因此,在单个 Tick 中,你的玩家将移动 10 的某个分数,经过完整的一秒钟的 Tick 后,你将已经移动了全部的 10 个单位。

然而,在属性随时间而改变其变化速率(例如加速的车辆)的情况下,这种方法会失效。这可以通过使用更高级的积分器(例如“Verlet”)来解决。

多线程方法

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

这并不意味着在所谓的单线程引擎中只运行一个线程。但所有重要的任务通常都在一个中央线程中。一些像碰撞检测之类的事情可能是多线程的,但通常Tick的Collision阶段会阻塞,直到所有线程都返回,引擎恢复到单个执行线程。

多线程呈现了整个非常庞大的问题类别,甚至包括一些性能问题,因为所有东西,甚至容器,都必须是线程安全的。而且游戏引擎本身就非常复杂,所以很少有必要增加多线程的复杂性。

固定时间步长方法

最后,正如另一个评论员所指出的,具有固定大小的时间步长,并控制您“步进”游戏逻辑的频率,也可以是处理此问题的一种非常有效的方法,具有许多好处。

为了完整起见,在此处提供链接,但另一个评论员也提供了链接:修正您的时间步长


请问您能否详细说明如何在多个设备(多人游戏)上实现时间同步?因为对于每个设备,单线程方法需要不同的时间...如果您有相关材料可供阅读,我将不胜感激。 - arenaq
这是一个非常非常困难的话题,我无法在这里开始回答。我建议你去谷歌搜索一下,那里有很多好的资源。 - Adam

8

Koen Witters撰写了一篇非常详细的文章, 讨论不同的游戏循环设置。

他涵盖了以下内容:

  • 以恒定游戏速度为基础的FPS
  • 以可变FPS为基础的游戏速度
  • 最大FPS下的恒定游戏速度
  • 与可变FPS无关的恒定游戏速度

(这些是从文章中提取的标题,按顺序列出。)


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更新其位置。


2
在flipcode上有一篇关于这个问题的优秀文章,我想找出来并呈现给你。这是一个精心设计的游戏循环:
  1. 单线程
  2. 在固定的游戏时钟下运行
  3. 使用插值时钟尽可能快地显示图形
至少我认为是这样。:-)可惜这篇文章后面的讨论很难找到。也许wayback机器可以帮助解决这个问题。
http://www.flipcode.com/archives/Main_Loop_with_Fixed_Time_Steps.shtml
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);

0

这并不涵盖更高级的程序抽象内容,例如状态机等。

通过调整帧时间间隔来控制运动和加速是可以的。但是,如何在此之后2.55秒触发声音或在18.25秒后更改游戏等级等问题呢?

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

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

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


0

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


0

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

但你应该立即同步线程;这需要很长时间来实现,因此如果你的游戏不太大,单线程的解决方案就可以了。

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


-2

从我的经验(不多)来看,Jesse和Adam的答案应该能让你走上正确的道路。

如果你想了解更多关于这个如何工作的信息和见解,我发现TrueVision 3D的示例应用非常有用。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接