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

游戏循环应该基于固定的还是可变的时间步骤? 是一个总是优越的,还是正确的select因游戏而异?

可变时间步长

物理更新被传递“自从上次更新以来的时间”参数,并因此依赖于帧率。 这可能意味着做计算为position += distancePerSecond * timeElapsed

优点 :stream畅,更易于编码
缺点 :非确定性,不可预知的非常小或大的步骤

deWiTTERS例子:

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

固定的时间步骤

更新可能甚至不接受“时间stream逝”,因为他们假设每个更新都是固定的时间段。 计算可以作为position += distancePerUpdate 。 该示例在渲染过程中包含插值。

优点 :可预测,确定性(更容易networking同步?),更清晰的计算代码
缺点 :没有同步监视v-sync(除非你插入,导致抖动graphics),有限的最大帧速率(除非你插入),很难在假定可变时间步骤的框架内工作(如Pyglet或Flixel )

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 ); } 

一些资源

  • 玩游戏:修复你的时间步!
  • deWitter的游戏循环文章
  • 雷神之锤3的FPS影响跳跃物理 – 一个原因是Doom 3被帧locking到60fps?
  • Flixel需要一个可变的时间步(我认为这是由Flash决定的),而Flashpunk则允许这两种types。
  • Box2D的手册§ 模拟 Box2D的世界表明它使用恒定的时间步骤。

有两个问题与这个问题有关。

  • 应该把物理速率和帧率绑在一起吗?
  • 物理学应该不断增长吗?

在Glen fielder's修复你的时间步骤,他说“释放物理学”。 这意味着你的物理更新速率应该与你的帧速率相关联。

例如,如果显示帧速率为50fps,仿真devise为以100fps运行,那么我们需要在每次显示更新时采取两个物理步骤来保持物理同步。

在Erin Catto对Box2D的build议中,他也提倡这一点。

所以,不要把时间步长与你的帧速率联系起来(除非你确实需要)。

物理步速是否应该与您的帧速率相关联? 没有。


艾琳对固定步长和变步进的想法:

Box2D使用称为积分器的计算algorithm。 积分器模拟离散时间点的物理方程。 …我们也不喜欢时间步伐变化很大。 可变的时间步长会产生可变的结果,这使得debugging变得困难。

格伦对固定与可变步进的想法:

修复你的时间步或爆炸

…如果在汽车模拟中有一系列对减震器有严格的弹簧约束,那么dt的微小变化实际上可以使模拟爆炸。 …

物理学应该不断增长吗? 是。


使用恒定增量步进物理的方法并不是将物理更新速率与帧速率绑定,而是使用时间累加器。 在我的游戏中,我更进一步。 我将平滑function应用于传入时间。 那样的话,大的FPS尖峰不会导致物理学跳得太远,而是在一两帧的时间内更快地模拟它们。

你提到一个固定的速度,物理不会与显示同步。 如果目标物理速率接近目标帧速率,则这是正确的。 帧速率比物理速率更糟。 一般来说,如果你能负担得起,最好把目标FPS的两倍的物理更新率作为目标。

如果你不能承受大的物理更新速率,考虑在帧之间插入graphics的位置,使绘制的graphics看起来比物理实际移动更平滑。

我认为真的有3个选项,但你只列出2个:

选项1

没做什么。 尝试以一定的间隔更新和渲染,例如每秒60次。 如果落后了,放手吧,别担心。 如果CPU不能跟上你的游戏,那么游戏会慢下来变成生涩的慢动作。 这个选项对于实时的多用户游戏根本不起作用,但是对于单人游戏来说很好,并且已经在许多游戏中成功地使用了。

选项2

使用每次更新之间的增量时间来改变对象的移动。 理论上很好,特别是如果游戏中没有任何东西加速或减速,而是以恒定的速度移动。 在实践中,许多开发人员实施这个很糟糕,并且可能导致碰撞检测和物理学不一致。 似乎有些开发人员认为这种方法比现在更容易。 如果你想使用这个选项,你需要大幅提高你的游戏速度,并拿出一些大型的math和algorithm,例如使用Verlet物理积分器(而不是大多数人使用的标准Euler),并使用射线进行碰撞检测而不是简单的毕达哥拉斯距离检查。 我在Stack Overflow上问了一个关于这个问题,并得到了一些很好的答案:

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

选项3

使用Gaffer的“修复你的时间步骤”的方法。 按照选项1以固定的步骤更新游戏,但是每帧渲染多次 – 基于经过了多less时间 – 以便游戏逻辑保持实时,而保持离散的步骤。 这样,易于实现像欧拉积分器和简单的碰撞检测游戏逻辑仍然工作。 您也可以select基于增量时间插补graphicsanimation,但这仅仅是视觉效果,并不影响您的核心游戏逻辑。 如果更新非常密集,则可能会遇到麻烦 – 如果更新落后,则需要越来越多的更新以保持更新,从而使您的游戏的响应速度降低。

就我个人而言,我喜欢选项1,当我需要实时同步时,我喜欢选项3。 当你知道自己在做什么的时候,我尊重scheme2可以是一个很好的select,但是我知道我的局限性足以远离它。

我非常喜欢XNA Framework实现固定时间步骤的方式。 如果一个给定的平局呼叫需要太长时间,它会反复更新直到它“赶上”。 肖恩·哈格里夫斯在这里描述:
http://blogs.msdn.com/b/shawnhar/archive/2007/11/23/game-timing-in-xna-game-studio-2-0.aspx

在2.0中,Draw的行为已经改变:

  • 根据需要多次呼叫更新以赶上当前时间
  • 调用Draw一次
  • 等到下一次更新的时候

在我看来,对此最大的赞成是你提到的,它使得所有的游戏代码计算变得如此简单,因为你不必在整个地方包含那个时间variables。

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

还有另一种select – 分离游戏更新和物理更新。 试图定制物理引擎到游戏时间步骤会导致问题,如果你解决你的时间步(失控的问题,因为集成需要更多的时间步,需要更多的时间需要更多的时间步),或使其变得不可思议,并得到不可思议的物理。

我看到很多解决scheme是让物理运行在一个固定的时间步长上,在不同的线程上(在不同的核心上)。 游戏内插或推断给定两个最新的有效帧,它可以抓住。 插值增加了一些滞后,外推增加了一些不确定性,但是你的物理将是稳定的,不会让你的时间步失去控制。

这不是微不足道的实施,但可能certificate自己未来的证据。

就我个人而言,我使用variables时间步长的变化(这是我认为的固定和variables的混合)。 我强调以几种方式testing这个计时系统,我发现自己在很多项目中都使用它。 我推荐它的一切? 可能不会。

我的游戏循环计算要更新的帧的数量(我们称之为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() 

这种更新并不适合所有的事情,但是对于街机风格的游戏,我宁愿看到游戏速度减慢,因为有很多事情比错过帧和失去玩家控制。 我也更喜欢这种其他可变时间步骤方法,最终导致帧丢失造成不可重现的故障。

这个解决scheme并不适用于所有的东西,但是还有另一个层次的可变时间步长 – 世界上每个物体的时间步长可变。

这看起来很复杂,也可能是,但是把它想象成一个离散的事件模拟。 每个玩家的移动可以表示为一个事件,当动作开始时开始,当动作结束时结束。 如果有任何要求事件被拆分的交互(例如冲突),事件被取消,另一个事件被推入事件队列(可能是按事件结束时间sorting的优先级队列)。

渲染完全脱离事件队列。 显示引擎根据需要在事件开始/结束时间之间插入点,并且可以根据需要在该估计中精确或者马虎。

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

 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倍的加速选项也是不够的。 对于一个可用的sim,你需要超过1,000,000倍的加速度,这在时间片模型中很难做到。 使用基于事件的模型,我们得到从1 s = 7 ms到1 s = 1 yr的任意时间速率。

改变时间速率并不会改变SIM的行为,这是一个重要的特征。 如果没有足够的CPU功率来以所需的速率运行模拟器,则事件将叠加,我们可能会限制UI刷新,直到事件队列被清除。 同样,我们可以尽可能多地快速转发sim,确保我们既不浪费CPU也不牺牲准确性。

所以总结一下:我们可以在一个长而悠闲的轨道上(使用Runge-Kutta积分)模拟一辆车,并且另一辆车同时在地面上弹跳 – 由于我们没有全球时间步,所以车辆将以适当的精度被模拟。

缺点:复杂性,缺乏任何支持这种模式的现成的物理引擎:)

考虑到浮点精度并使更新保持一致,固定时间步骤非常有用。

这是一个简单的代码,所以试试看看它是否适用于你的游戏是有用的。

 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或animation小精灵)和30fps来更新非平凡的系统(比如物理和),甚至更慢的定时器来做后台pipe理,比如删除未使用的对象,资源等。

除了你已经说过,它可能会降低到你想要的游戏的感觉。 除非你可以保证你总是有一个固定的帧速率,那么你可能会在某个地方有所放缓,固定和可变的时间步骤看起来会非常不同。 固定会影响你的游戏进行一段时间的慢镜头,有时候可能会产生预期的效果(看看像Ikaruga这样的老派风格的射手,大爆炸在击败老板之后引起放缓)。 可变的时间步长会使物体在时间上以正确的速度移动,但是您可能会看到位置的突然变化等,这可能使玩家难以准确地进行动作。

我无法真正看到一个固定的时间步骤将使networking上的事情更容易,他们都会稍微不同步,在一台机器上开始和减速,而不是另一个会推动更多的不同步。

我一直倾向于个人可变的方法,但这些文章有一些有趣的事情要考虑。 尽pipe如此,我仍然发现固定的步骤相当普遍,特别是在人们认为帧速率恒定在60fps的情况下,与PC上可实现的非常高的速率相比。

使用Gaffer的“修复你的时间步骤”的方法。 按照选项1以固定的步骤更新游戏,但是每帧渲染多次 – 基于经过了多less时间 – 以便游戏逻辑保持实时,而保持离散的步骤。 这样,易于实现像欧拉积分器和简单的碰撞检测游戏逻辑仍然工作。 您也可以select基于增量时间插补graphicsanimation,但这仅仅是视觉效果,并不影响您的核心游戏逻辑。 如果更新非常密集,则可能会遇到麻烦 – 如果更新落后,则需要越来越多的更新以保持更新,从而使您的游戏的响应速度降低。

就我个人而言,我喜欢选项1,当我需要实时同步时,我喜欢选项3。 当你知道自己在做什么的时候,我尊重scheme2是一个很好的select,但是我知道我的局限性足以远离它

我发现固定的时间步长同步到60fps可以产生平滑的animation效果。 这对VR应用程序尤其重要。 其他任何事情都是肉体上的恶心。

可变时间步长不适用于VR。 看看一些使用可变时间步长的Unity VR例子。 这是不愉快的。

规则是如果你的3D游戏在VR模式下是平滑的,那么在非VR模式下会非常出色。

比较这两个(纸板VR应用程序)

(可变时间步长)

(固定时间步)

你的游戏必须是multithreading的,以达到一致的时间步长/帧率。 物理,用户界面和渲染必须分成专用线程。 同步它们是可怕的PITA,但结果是你想要的镜像平滑渲染(尤其是VR)。

手机游戏尤其如此。 具有挑战性,因为embedded式CPU和GPU的性能有限。 尽量less用GLSL(俚语),尽可能减lessCPU的工作量。 请注意,将parameter passing给GPU会消耗总线资源。

在开发过程中始终保持显示帧率。 真正的游戏是保持固定在60fps。 这是大多数屏幕的本机同步速率,对于大多数眼球也是如此。

您正在使用的框架应该能够通知您一个同步请求,或者使用一个计时器。 不要插入睡眠/等待延迟来实现这一点 – 即使是轻微的变化也是显而易见的。

可变的时间步骤是应该尽可能经常运行的程序:渲染周期,事件处理,networking等等。

固定的时间步骤是当你需要一些可预测和稳定的时候。 这包括但不限于物理和碰撞检测。

实际上,物理和碰撞检测应该与其他所有事物分离开来,在它自己的时间步骤上。 在一个小的固定的时间步骤执行这样的程序的原因是保持它们的准确性。 冲动的大小高度依赖于时间,如果时间间隔太大,模拟变得不稳定,疯狂的东西就像弹跳的球体穿过地面,或者跳出游戏世界,这两者都不是理想的。

其他的东西都可以在一个可变的时间步上运行(尽pipe从专业的angular度来说,允许locking渲染到一个固定的时间步是个好主意)。 为了使游戏引擎具有响应能力,应尽快处理networking消息和用户input等内容,这意味着轮询之间的间隔应尽可能短。 这通常意味着variables。

其他一切都可以asynchronous处理,使时间成为一个有争议的问题。