什么时候应该使用固定或可变时间步长?


257

游戏循环应该基于固定时间段还是可变时间段?一个人永远是上等的,还是正确的选择会因游戏而异?

可变时间步长

物理更新传递了“自上次更新以来经过的时间”参数,因此与帧速率有关。这可能意味着要进行计算position += distancePerSecond * timeElapsed

优点:流畅,更易于编码
缺点:不确定性,无论步长或步长都是不确定的

deWiTTERS示例:

while( game_is_running ) {
    prev_frame_tick = curr_frame_tick;
    curr_frame_tick = GetTickCount();
    update( curr_frame_tick - prev_frame_tick );
    render();
}

固定时间步

更新可能甚至不接受“经过的时间”,因为它们假定每次更新都是在固定的时间段内进行的。计算可以按进行position += distancePerUpdate。该示例包括渲染期间的插值。

优点:可预测,确定性(更容易进行网络同步?),更清晰的计算代码
缺点:不同步以监控垂直同步(除非进行插值,否则会引起抖动的图形),最大帧速率受限(除非进行插值),难以在框架内工作采取可变的时间步长(例如PygletFlixel

deWiTTERS示例:

while( game_is_running ) {
    while( GetTickCount() > next_game_tick ) {
        update();
        next_game_tick += SKIP_TICKS;
    }
    interpolation = float( GetTickCount() + SKIP_TICKS - next_game_tick )
                    / float( SKIP_TICKS );
    render( interpolation );
}

一些资源


6
在游戏中使用可变的时间步长,在物理上使用固定的步长
Daniel Little

7
我不会说可变时间步长更容易精确地编写代码,因为使用固定时间步长,您“不必在所有地方都将timeElapsed变量与所有计算混淆”。并不是那么难,但是我不会添加“更容易编写代码”作为专业人士。
pek 2011年

是的,我想我指的是您无需插值可变的时间步长。
尼克·索内维尔德

@pek我同意你的看法。可变时间步长具有更简单的编码游戏循环,但是您需要在处理该可变性的实体中进行更多编码,以便“适应”变化。固定时间步长会使游戏循环的编码更加复杂(因为您必须准确地补偿时间近似方差,并重新计算要添加的额外延迟或要跳过的更新次数以保持固定),但是对于将始终必须处理相同的时间间隔。总体而言,没有一种方法比另一种方法更简单。
Shivan Dragon 2012年

您可以从该工具检查这些visuall: s3.amazonaws.com/picobots/assets/unity/jerky-motion/...虽然它不给你如何将它们看起来时帧速率不同的想法
巴迪

Answers:


135

有两个与此问题有关的问题。

  • 物理步速应该与帧频联系起来吗?
  • 物理是否应该以恒定的增量步进?

在Glen fielder的“修正您的时间”步骤中,他说“释放物理学”。这意味着你的物理更新率应被捆绑到您的帧速率。

例如,如果显示帧率为50fps,并且模拟设计为以100fps运行,则每个显示更新都需要采取两个物理步骤,以保持物理同步。

在Erin Catto对Box2D的建议中,他也建议这样做。

因此,请勿将时间步长与帧速率联系在一起(除非您确实必须这样做)。

物理步速是否应该与您的帧频联系在一起?没有。


Erin关于固定步长与可变步长的思想:

Box2D使用一种称为积分器的计算算法。积分器在离散的时间点模拟物理方程。...我们也不希望时间步长有太大变化。可变的时间步长会产生可变的结果,这使其难以调试。

格伦关于固定步进与可变步进的思想:

修正时间步伐或爆炸

...如果在汽车模拟中对减震器有一系列非常严格的弹簧约束,则dt的微小变化实际上会使模拟爆炸。...

物理是否应该以恒定的增量步进?是。


使用恒定增量来逐步进行物理操作而不将物理更新速率与帧速率绑定在一起的方法仍然是使用时间累加器。在我的游戏中,我更进一步。我将平滑函数应用于传入时间。这样,较大的FPS峰值不会导致物理场跳得太远,而是可以在一两帧内更快地模拟它们。

您提到固定速率时,物理场不会与显示同步。如果目标物理速率接近目标帧速率,则为true。更糟糕的是,帧速率大于物理速率。通常,如果可以承受的话,最好将物理更新速率设置为目标FPS的两倍。

如果无法承受较大的物理更新速率,请考虑在帧之间插入图形的位置,以使绘制的图形看起来比物理实际移动更平滑。


1
我在升级机器之前和之后都玩过The Floor is Jelly,这很愚蠢:这不是一回事,因为物理确实是从游戏循环中调用的(因此与帧速率相关),而不是从一个计时器。我的旧机器非常糟糕,因此经常在慢动作和过快动画之间切换,这对游戏玩法产生了很大影响。现在,它只是一个非常快的动作。无论如何,这个游戏是解决这个问题有多好的一个很好的例子(尽管仍然是一个可爱的游戏)。
2014年

55

我认为确实有3种选择,但您将它们列为2种:

选项1

没做什么。尝试以一定的时间间隔(例如每秒60次)进行更新和渲染。如果它落在后面,那就放心,不要担心。如果CPU跟不上您的游戏,那么游戏将变成缓慢的慢动作。此选项对于实时多用户游戏根本不起作用,但对单人游戏很好,并且已在许多游戏中成功使用。

选项2

使用每次更新之间的时间间隔来更改对象的移动。理论上很棒,尤其是如果游戏中没有任何东西加速或减速,而是以恒定的速度运动时。实际上,许多开发人员都不好地实现了这一点,并且可能导致冲突检测和物理方法不一致。似乎有些开发人员认为此方法比实际容易。如果要使用此选项,则需要大大提高游戏效率,并运用一些强大的数学和算法,例如使用Verlet物理积分器(而不是大多数人使用的标准Euler)以及使用射线进行碰撞检测而不是简单的毕达哥拉斯距离检查。不久前,我在Stack Overflow上问了一个与此有关的问题,并得到了一些很好的答案:

https://stackoverflow.com/questions/153507/calculate-the-position-of-an-accelerating-body-after-a-certain-time

选项3

使用Gaffer的“固定时间步长”方法。与选项1一样,以固定的步骤更新游戏,但是每渲染一帧就要进行多次(基于经过的时间),以使游戏逻辑与实时保持同步,同时保持离散的步骤。这样,易于实现的游戏逻辑(如Euler积分器)和简单的碰撞检测仍然有效。您还可以选择基于增量时间插值图形动画,但这仅用于视觉效果,而不会影响您的核心游戏逻辑。如果更新非常密集,则可能会遇到麻烦-如果更新落后,您将需要越来越多的更新来跟上进度,这可能使游戏的响应速度降低。

就个人而言,当我可以选择它时,我喜欢选项1;当我需要实时同步时,我喜欢选项3。我尊重选项2在您知道自己在做什么的情况下可能是一个不错的选择,但是我知道我的局限性足以使其远离它。


关于选项2:我不确定射线投射是否能比毕达哥拉斯距离检查更快,除非您在毕达哥拉斯的应用中使用蛮力手段,但如果不添加广相,那么射线投射也将非常昂贵。
Kaj 2010年

4
如果您以不相等的时间步长使用Verlet,则会把婴儿的洗澡水扔掉。Verlet之所以如此稳定的原因是,错误在随后的时间步中被抵消。如果时间步长不相等,则不会发生这种情况,您将重返物理领域。
drxzcl 2010年

选项3-听起来像乔尔·马丁内斯(Joel Martinez)对XNA方法的描述。
topright 2010年

1
这是一个很好的答案。这三个选项都有自己的位置,重要的是要了解它们何时合适。
Adam Naylor 2014年

我从事过的所有MMO(EQ,Landmark / EQNext [哭],PS2 [简短]和ESO),我们一直使用可变的帧时间。我从来没有参加过这个特定的决定,只是在事实发生后出现并利用了其他人的决定。
Mark Storer

25

我非常喜欢XNA Framework实现固定时间步长的方式。如果给定的绘图调用花费的时间太长,它将反复调用update,直到“赶上”。肖恩·哈格里夫斯(Shawn Hargreaves)在这里进行了描述:http :
//blogs.msdn.com/b/shawnhar/archive/2007/11/23/game-timing-in-xna-game-studio-2-0.aspx

在2.0中,绘制行为已更改:

  • 根据需要多次呼叫更新以赶上当前时间
  • 致电一次抽奖
  • 等到下一次更新的时间到了

在我看来,最大的优点就是您提到的那个优点,它使您所有游戏代码的计算变得非常简单,因为您不必在整个地方都包含该时间变量。

注意:xna也支持可变的时间步长,这只是一个设置。


这是进行游戏循环的最常见方法。但是,在使用移动设备时,电池寿命并不是很好。
knight666

1
@ knight666; 您是否建议使用更长的时间步,减少迭代次数可以节省电池寿命?
falstro 2010年

那仍然是一个变量更新-更新增量是根据帧渲染的时间而不是某个固定值(即1/30秒)而变化的。
丹尼斯·蒙西

1
@Dennis:据我了解,Update函数以固定的增量调用...
RCIX 2010年

3
@ knight666嗯-你怎么看?如果您启用了vsync并且没有结结巴巴-这些方法应该相同!如果你有VSYNC 关闭您更新更多的往往比你需要和可能浪费CPU(因此电池)通过不让它闲着!
安德鲁·罗素

12

还有另一个选择-解耦游戏更新和物理更新。如果您固定时间步长,尝试根据游戏的时间步长调整物理引擎会导致问题(旋转失控的问题,因为集成需要更多的时间步长,而这需要更多的时间,而又需要更多的时间步长),或者使其变得可变并获得怪异的物理学。

我看到的很多解决方案是让物理以固定的时间步长在不同的线程(在不同的内核上)运行。游戏根据可以抓取的两个最新有效帧进行内插或外推。插值会增加一些滞后,插值会增加一些不确定性,但是您的物理场将保持稳定,并且不会使时间步长失控。

这并非易事,但可能会证明自己是未来的证明。


8

就个人而言,我使用可变时步的变化形式(我认为是固定和可变的混合体)。我以多种方式对这个计时系统进行了压力测试,结果发现自己将其用于许多项目。我是否推荐所有东西?可能不是。

我的游戏循环计算要更新的帧数(我们称其为F),然后执行F个离散逻辑更新。每次逻辑更新均假设一个固定的时间单位(在我的游戏中通常为1/100秒)。依次执行每个更新,直到执行所有F个离散逻辑更新为止。

为什么要在逻辑步骤中进行离散更新?好吧,如果您尝试使用连续的步骤,突然之间就会出现物理故障,因为计算出的速度和行进距离乘以F的巨大值。

一个糟糕的实现只会做F =当前时间-最后一帧时间更新。但是,如果计算落后太多(有时是由于无法控制的情况,例如另一个进程占用了CPU时间),您将很快看到严重的跳过。很快,您尝试维护的稳定FPS就变成了SPF。

在我的游戏中,我允许“平稳”(某种程度)的减速来限制两次平局之间可能发生的逻辑追赶量。我通过钳位来做到这一点:F = min(F,MAX_FRAME_DELTA)通常具有MAX_FRAME_DELTA = 2/100 * s或3/100 * s。因此,不要在游戏逻辑落后时跳过帧,而应丢弃任何严重的帧丢失(这会减慢速度),恢复几帧,绘制并重试。

通过这样做,我还确保播放器控件与屏幕上实际显示的内容保持更紧密的同步。

最终产品的伪代码是这样的(delta是前面提到的F):

// Assume timers have 1/100 s resolution
const MAX_FRAME_DELTA = 2
// Calculate frame gap.
var delta = current time - last frame time
// Clamp.
delta = min(delta, MAX_FRAME_RATE)
// Update in discrete steps
for(i = 0; i < delta; i++)
{
    update single step()
}
// Caught up again, draw.
render()

这种更新并不适合所有情况,但是对于街机风格的游戏,我宁愿看到游戏速度变慢,因为发生了很多事情而不是错过帧并失去了玩家的控制权。与其他可变时间步进方法相比,我也更喜欢这种方法,这些方法最终会因帧丢失而导致无法再现的毛刺。


完全同意最后一点;在几乎所有游戏中,当帧率降低时,输入应“降低”。即使在某些游戏(即多人游戏)中不可能做到这一点,但如果可能的话,还是会更好。:P感觉比长框架然后让游戏世界“跳”到“正确”状态感觉更好。
Ipsquiggle 2010年

如果没有诸如街机之类的固定硬件,当硬件无法跟上时,拥有街机游戏会减慢模拟速度,从而使游戏在较慢的机器上作弊。

乔只有在我们关心“作弊”时才重要。大多数现代游戏并不是真正意义上的玩家之间的竞争,而只是带来有趣的体验。
伊恩

1
伊恩,我们在这里专门讨论街机风格的游戏,这些游戏通常是由高分榜/排行榜驱动的。我玩大量的大赛,我知道我是否发现有人在排行榜上故意放慢成绩,我希望他们的成绩被抹去。

并非试图减少您的答案,但我会将其解释为固定步骤,在此步骤中,渲染不直接依赖于物理更新速率,除了赶上物理优先于渲染。它绝对具有良好的品质。
AaronLS '17

6

这种解决方案并不能适用于所有情况,但是还有一个可变的时间步长级别-世界上每个对象的可变时间步长。

这似乎很复杂,而且确实可以,但是可以将其视为将游戏建模为离散事件模拟。玩家的每个动作都可以表示为一个事件,该事件在动作开始时开始,在动作结束时结束。如果存在任何需要拆分事件的交互(例如冲突),则取消该事件并将另一个事件推送到事件队列(可能是按事件结束时间排序的优先级队列)。

渲染完全与事件队列分离。显示引擎根据需要在事件开始/结束时间之间插值,并且可以根据需要在此估计中准确或草率。

要查看此模型的复杂实现,请参见空间模拟器EXOFLIGHT。它使用与大多数飞行模拟器不同的执行模型-基于事件的模型,而不是传统的固定时间片模型。此类仿真的基本主循环如下所示,为伪代码:

while (game_is_running)
{
   world.draw_to_screen(); 
   world.get_player_input(); 
   world.consume_events_until(current_time + time_step); 
   current_time += time_step; 
}

在空间模拟器中使用它的主要原因是必须提供任意的时间加速而不会降低精度。EXOFLIGHT中的某些任务可能需要花费游戏年才能完成,甚至32倍加速选项也将不足。您需要超过1,000,000x的加速度才能使用可用的sim卡,这在时间切片模型中很难做到。使用基于事件的模型,我们可以获得任意时间速率,从1 s = 7 ms到1 s = 1年。

更改时间速率不会更改sim卡的行为,这是一项关键功能。如果没有足够的CPU能力以所需的速度运行模拟器,事件将堆积起来,我们可能会限制UI刷新,直到清除事件队列为止。同样,我们可以根据需要快速完成sim卡,并确保我们既不会浪费CPU也不会牺牲准确性。

总结一下:我们可以在悠长的轨道上(使用Runge-Kutta积分)对一辆车进行建模,而另一辆车则同时沿地面弹跳-由于我们没有全局时间步长,因此将以适当的精度对这两种车进行仿真。

缺点:复杂性,并且缺少支持该模型的现成物理引擎:)


5

当考虑浮点精度并使更新保持一致时,固定时间步很有用。

这是一段简单的代码,因此尝试一下并查看它是否适用于您的游戏将非常有用。

now = currentTime
frameTime = now - lastTimeStamp // time since last render()
while (frameTime > updateTime)
    update(timestep)
    frameTime -= updateTime     // update enough times to catch up
                                // possibly leaving a small remainder
                                // of time for the next frame

lastTimeStamp = now - frameTime // set last timestamp to now but
                                // subtract the remaining frame time
                                // to make sure the game will still
                                // catch up on those remaining few millseconds
render()

使用固定时间步长的主要问题是,拥有快速计算机的玩家将无法利用速度。仅以30fps更新游戏时以100fps渲染与以30fps进行渲染相同。

话虽如此,可能有可能使用多个固定时间步长。60fps可用于更新琐碎的对象(例如UI或动画精灵),而30fps可用于更新非琐碎的系统(例如物理和),甚至可以使用较慢的计时器来进行后台管理,例如删除未使用的对象,资源等。


2
如果游戏是精心制作的,则render方法可以进行插值以进行30fps更新,实际上与以30fps进行渲染不同。
里奇(Ricket)

3

除了您已经说过的内容外,还可能取决于您想要游戏拥有的感觉。除非您可以保证始终保持恒定的帧速率,否则您可能会在某处放慢速度,并且固定时间和可变时间步长看起来会非常不同。已修复将使您的游戏在一段时间内进行慢动作效果,有时这可能是预期的效果(请看像Ikaruga这样的老式学校射击游戏,在击败老板后大量爆炸会导致游戏速度下降)。可变的时间步长可以使事物在时间上以正确的速度移动,但是您可能会看到位置的突然变化等,这可能会使玩家难以准确地执行动作。

我真的看不到固定时间的步骤可以使事情在网络上更容易实现,在一台机器上开始和放慢速度时它们都将略有不同步,而在另一台机器上却不会使事情更加不同步。

我一直个人倾向于可变方法,但是那些文章有一些有趣的事情要考虑。不过,我仍然发现固定步骤相当普遍,尤其是在控制台上,人们认为帧速率是恒定的60fps,而PC上却可以达到很高的速率。


5
您一定应该阅读原始帖子中的Gaffer on games链接。我认为这本身并不是一个不好的答案,因此我不会对此投反对票,但是我不同意您的任何论点
falstro,2010年

我认为固定时间步长不会导致游戏速度下降是故意的,因为这是因为缺乏控制力。严格意义上讲,缺乏控制就是屈服于机会,因此不是故意的。可能恰好是您的想法,但这是我想继续做的。至于网络中的固定时间步长,肯定有一个加法,因为在没有相同时间步长的情况下使两台不同机器上的物理引擎保持同步是不可能的。由于同步的唯一选择就是发送所有实体转换,因此带宽占用过多。
Kaj 2010年

0

使用Gaffer的“固定时间步长”方法。与选项1一样,以固定的步骤更新游戏,但是每渲染一帧就要进行多次(基于经过的时间),以使游戏逻辑与实时保持同步,同时保持离散的步骤。这样,易于实现的游戏逻辑(如Euler积分器)和简单的碰撞检测仍然有效。您还可以选择基于增量时间插值图形动画,但这仅用于视觉效果,而不会影响您的核心游戏逻辑。如果更新非常密集,则可能会遇到麻烦-如果更新落后,您将需要越来越多的更新来跟上进度,这可能使游戏的响应速度降低。

就个人而言,当我可以选择它时,我喜欢选项1;当我需要实时同步时,我喜欢选项3。我尊重选项2在您知道自己在做什么的情况下可能是一个不错的选择,但我知道我的局限性足以使其远离


如果您要窃取答案,请至少将其归功于此人!
PrimRock

0

我发现同步到60fps的固定时间步长可以使镜像平滑动画。这对于VR应用程序尤其重要。其他任何事情都在令人恶心。

可变的时间步不适合VR。看看一些使用可变时间步长的Unity VR示例。真不愉快

规则是,如果您的3D游戏在VR模式下是流畅的,那么在非VR模式下则是出色的。

比较这两个(Cardboard VR应用)

(可变的时间步长)

(固定的时间步长)

您的游戏必须是多线程的,以实现一致的时间步长/帧速率。物理,UI和渲染必须分为专用线程。同步它们是可怕的PITA,但结果是可以镜像所需的平滑渲染(特别是对于VR)。

手机游戏尤其如此。挑战,因为嵌入式CPU和GPU性能有限。尽量少用GLSL(s语),以从CPU上分担尽可能多的工作。请注意,将参数传递给GPU会消耗总线资源。

在开发过程中始终保持显示帧率。真正的游戏是将其固定为60fps。这是大多数屏幕和大多数眼球的本机同步速率。

您使用的框架应该能够通知您同步请求或使用计时器。不要插入睡眠/等待延迟来实现此目的-即使轻微的变化也很明显。


0

可变时间步骤用于应尽可能频繁运行的过程:渲染周期,事件处理,网络内容等。

固定时间步长用于需要可预测和稳定的事物。这包括但不限于物理和碰撞检测。

实际上,应该将物理和碰撞检测与其他所有因素分离开来,这是自己的时间步长。之所以要在固定的较小时间步长执行此类程序,是为了保持其准确性。脉冲大小高度依赖于时间,并且如果间隔太大,则模拟变得不稳定,并且疯狂的事情会发生,如弹跳的球相位穿过地面或弹跳出游戏世界,这都不是可取的。

其他所有内容都可以在可变的时间步长上运行(尽管从专业角度来讲,允许将渲染锁定到固定的时间步长通常是一个好主意)。为了使游戏引擎能够响应,应尽快处理网络消息和用户输入之类的问题,这意味着轮询之间的间隔在理想情况下应尽可能短。这通常意味着可变。

其他所有内容都可以异步处理,使计时成为一个争论点。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.