如何让每秒显示帧数独立于游戏逻辑?这样无论显卡渲染速度有多快,游戏逻辑都以相同的速度运行。
8 回答
我认为这个问题揭示了对游戏引擎应该如何设计的一些误解。这完全没问题,因为它们是非常复杂的东西,很难做到;)
您的正确印象是您想要所谓的帧速率独立性。但这不仅仅指渲染帧。
单线程游戏引擎中的 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
Koen Witters 有一篇关于不同游戏循环设置的非常详细的文章。
他涵盖:
- FPS 依赖于恒定的游戏速度
- 游戏速度取决于可变 FPS
- 具有最高 FPS 的恒定游戏速度
- 独立于可变 FPS 的恒定游戏速度
(这些是从文章中提取的标题,按可取性排序。)
你可以让你的游戏循环看起来像:
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
那天有一篇关于翻转码的优秀文章。我想把它挖出来给你看。
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);
Enginuity有一个稍微不同但有趣的方法:任务池。
在显示图形之前有时间延迟的单线程解决方案很好,但我认为渐进的方式是在一个线程中运行游戏逻辑,并在另一个线程中显示。
但是你应该以正确的方式同步线程;)实现需要很长时间,所以如果你的游戏不是太大,单线程解决方案就可以了。
此外,将 GUI 提取到单独的线程中似乎是一个很好的方法。您是否曾在 RTS 游戏中看到单位移动时弹出“任务完成”的消息?我正是这个意思 :)
这不涵盖更高的程序抽象内容,即状态机等。
可以通过调整帧时间间隔来控制运动和加速度。但是,比如在这个或那个之后 2.55 秒触发声音,或者在 18.25 秒之后改变游戏关卡等等。
这可以与经过的帧时间累加器(计数器)相关联,但是如果您的帧速率低于您的状态脚本分辨率,即如果您的更高逻辑需要 0.05 秒的粒度并且您低于 20fps,这些时间可能会被搞砸。
如果游戏逻辑在独立于 fps 的固定时间片上运行在单独的“线程”(在软件级别,我更喜欢这样做,或操作系统级别)上,则可以保持确定性。
惩罚可能是如果没有发生太多事情,您可能会在帧之间浪费 CPU 时间,但我认为这可能是值得的。
根据我的经验(不多),杰西和亚当的回答应该让你走上正轨。
如果您想了解更多信息并深入了解其工作原理,我发现TrueVision 3D的示例应用程序非常有用。