(翻译gafferongames) 固定时间步长 Fix Your Timestep
原文:https://gafferongames.com/post/fix_your_timestep/
这篇文章主要说了:怎么正确的实现一个 固定时常的Tick方式,有些类似于unity的 fixedupdate的实现。
实现方式:1.时间的累计,并且一帧可能会执行多次 fixedTick 2.针对Render渲染帧,进行时间的插值,确保和fixedTick里的逻辑同步。
比较适合在fixedTick里实现的逻辑。最常见的是物理模拟。
我只翻译两部分:
Semi-fixed timestep 半固定时间步长
It’s much more realistic to say that your simulation is well behaved only if delta time is less than or equal to some maximum value. This is usually significantly easier in practice than attempting to make your simulation bulletproof at a wide range of delta time values.
With this knowledge at hand, here’s a simple trick to ensure that you never pass in a delta time greater than the maximum value, while still running at the correct speed on different machines:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | double t = 0.0; double dt = 1 / 60.0; double currentTime = hires_time_in_seconds(); while ( !quit ) { double newTime = hires_time_in_seconds(); double frameTime = newTime - currentTime; currentTime = newTime; while ( frameTime > 0.0 ) { float deltaTime = min( frameTime, dt ); integrate( state, t, deltaTime ); frameTime -= deltaTime; t += deltaTime; } render( state ); } |
The benefit of this approach is that we now have an upper bound on delta time. It’s never larger than this value because if it is we subdivide the timestep. The disadvantage is that we’re now taking multiple steps per-display update including one additional step to consume any the remainder of frame time not divisible by dt. This is no problem if you are render bound, but if your simulation is the most expensive part of your frame you could run into the so called “spiral of death”.
What is the spiral of death? It’s what happens when your physics simulation can’t keep up with the steps it’s asked to take. For example, if your simulation is told: “OK, please simulate X seconds worth of physics” and if it takes Y seconds of real time to do so where Y > X, then it doesn’t take Einstein to realize that over time your simulation falls behind. It’s called the spiral of death because being behind causes your update to simulate more steps to catch up, which causes you to fall further behind, which causes you to simulate more steps…
如果你的物理模拟无法跟上所需的计算步数,你的程序就会陷入一个恶性循环。例如:
- 你的游戏需要模拟
X
秒的物理。 - 但物理计算需要
Y
秒(Y > X
)。 - 由于
Y > X
,你的模拟会逐渐落后。 - 为了追赶进度,模拟需要执行更多的步骤……
- 但这又导致它变得更慢,最终进入无限循环。
So how do we avoid this? In order to ensure a stable update I recommend leaving some headroom. You really need to ensure that it takes significantly less than X seconds of real time to update X seconds worth of physics simulation. If you can do this then your physics engine can “catch up” from any temporary spike by simulating more frames. Alternatively you can clamp at a maximum # of steps per-frame and the simulation will appear to slow down under heavy load. Arguably this is better than spiraling to death, especially if the heavy load is just a temporary spike.
Free the physics 解耦物理更新和渲染
Now let’s take it one step further. What if you want exact reproducibility from one run to the next given the same inputs? This comes in handy when trying to network your physics simulation using deterministic lockstep, but it’s also generally a nice thing to know that your simulation behaves exactly the same from one run to the next without any potential for different behavior depending on the render framerate.
为了确保物理模拟在相同输入下,每次运行的结果都完全一致,我们需要采用完全固定的时间步长,你的模拟从一次运行到下一次运行的行为完全相同而没有任何可能因渲染帧率而不同的行为也是件好事。
But you ask why is it necessary to have fully fixed delta time to do this? Surely the semi-fixed delta time with the small remainder step is “good enough”? And yes, you are right. It is good enough in most cases but it is not exactly the same due to to the limited precision of floating point arithmetic.
但是你会问为什么需要完全固定的时间来做这个?半固定的时间和小的剩余步长肯定是“足够好”吗?是的,你是对的。在大多数情况下,它已经足够好了,但由于浮点运算的精度有限,它并不完全相同。
What we want then is the best of both worlds: a fixed delta time value for the simulation plus the ability to render at different framerates. These two things seem completely at odds, and they are - unless we can find a way to decouple the simulation and rendering framerates.
解耦simulation帧和render帧
Here’s how to do it. Advance the physics simulation ahead in fixed dt time steps while also making sure that it keeps up with the timer values coming from the renderer so that the simulation advances at the correct rate. For example, if the display framerate is 50fps and the simulation runs at 100fps then we need to take two physics steps every display update. Easy.
What if the display framerate is 200fps? Well in this case it we need to take half a physics step each display update, but we can’t do that, we must advance with constant dt. So we take one physics step every two display updates.
Even trickier, what if the display framerate is 60fps, but we want our simulation to run at 100fps? There is no easy multiple. What if VSYNC is disabled and the display frame rate fluctuates from frame to frame?
If you head just exploded don’t worry, all that is needed to solve this is to change your point of view. Instead of thinking that you have a certain amount of frame time you must simulate before rendering, flip your viewpoint upside down and think of it like this: the renderer produces time and the simulation consumes it in discrete dt sized steps.
Notice that unlike the semi-fixed timestep we only ever integrate with steps sized dt so it follows that in the common case we have some unsimulated time left over at the end of each frame. This left over time is passed on to the next frame via the accumulator variable and is not thrown away.
注意,与半固定时间步长不同的是,我们只对步长dt进行积分,因此,在一般情况下,在每一帧结束时,我们会留下一些未模拟的时间。剩余的时间通过累加器变量传递到下一帧,不会被丢弃。
The final touch
But what do to with this remaining time? It seems incorrect doesn’t it?
To understand what is going on consider a situation where the display framerate is 60fps and the physics is running at 50fps. There is no nice multiple so the accumulator causes the simulation to alternate between mostly taking one and occasionally two physics steps per-frame when the remainders “accumulate” above dt.
Now consider that the majority of render frames will have some small remainder of frame time left in the accumulator that cannot be simulated because it is less than dt. This means we’re displaying the state of the physics simulation at a time slightly different from the render time, causing a subtle but visually unpleasant stuttering of the physics simulation on the screen.
One solution to this problem is to interpolate between the previous and current physics state based on how much time is left in the accumulator:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | double t = 0.0; double dt = 0.01; double currentTime = hires_time_in_seconds(); double accumulator = 0.0; State previous; State current; while ( !quit ) { double newTime = time (); double frameTime = newTime - currentTime; if ( frameTime > 0.25 ) frameTime = 0.25; currentTime = newTime; <strong>accumulator</strong> += frameTime; while ( accumulator >= dt ) { previousState = currentState; integrate( currentState, t, dt ); t += dt; accumulator -= dt; } const double <strong>alpha</strong> = accumulator / dt; State state = currentState * alpha + previousState * ( 1.0 - alpha ); render( state ); } |
This looks complicated but here is a simple way to think about it. Any remainder in the accumulator is effectively a measure of just how much more time is required before another whole physics step can be taken. For example, a remainder of dt/2 means that we are currently halfway between the current physics step and the next. A remainder of dt*0.1 means that the update is 1/10th of the way between the current and the next state.
We can use this remainder value to get a blending factor between the previous and current physics state simply by dividing by dt. This gives an alpha value in the range [0,1] which is used to perform a linear interpolation between the two physics states to get the current state to render. This interpolation is easy to do for single values and for vector state values. You can even use it with full 3D rigid body dynamics if you store your orientation as a quaternion and use a spherical linear interpolation (slerp) to blend between the previous and current orientations.
https://docs.unity3d.com/6000.0/Documentation/Manual/fixed-updates.html
我们看untity文档里对于FixedUpdate的描述,基本和上面的实现类似。
The fixed update loop simulates code running at fixed time intervals but in practice the interval between fixed updates isn’t fixed. This is because a fixed update always needs a frame to run in and the duration of a frame and the length of the fixed time step are not in perfect sync. If a fixed time step completes during the current frame, the associated fixed update can’t run until the next frame. When frame rates are low, a single frame might span several fixed time steps. In this case a backlog of fixed updates accumulates during the current frame and Unity executes all of them in the next frame to catch up.
An example showing FixedUpdate running at 50 updates per second (0.02s per fixed update) and the Player Loop running at approximately 80 frames per second. Some frame updates (marked in yellow) have a corresponding FixedUpdate (marked in green) if a new complete fixed timestep has elapsed by the start of the frame.
An example showing Update running at 25 FPS and FixedUpdate running at 100 updates per second. You can see there are four occurrences of a FixedUpdate during one frame, marked in yellow.
Note: A lower timestep value means more frequent physics updates and more precise simulations, which leads to higher CPU load.
低帧率情况下,fixedupdate执行频率变高,CPU压力变得更大了。。
也就是说 如果设备由于各种Profile原因导致FPS变低,如果一直保持一个很低的水平,导致FixedUpdated同一帧执行次数变多,那CPU就会更低了。。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!