1
\$\begingroup\$

I am decreasing my velocity by 50% every second using a guide I found online. I am using the code here and it gives the right result but only for very small values for dt:

float dt, velocity, position;

dt = 0.002f;
velocity = 1.0f;
position = 0.0f;

for (float t = 0.0f; t <= 1.0f; t += dt) {
    position += velocity * dt;
    velocity = lerp(velocity, 0.0f, 1.0f - std::pow(0.5f, dt));
}

printf("dt = %f, p = %f, v = %f\n", dt, position, velocity);

dt = 0.09f;
velocity = 1.0f;
position = 0.0f;

for (float t = 0.0f; t <= 1.0f; t += dt) {
    position += velocity * dt;
    velocity = lerp(velocity, 0.0f, 1.0f - std::pow(0.5f, dt));
}

printf("dt = %f, p = %f, v = %f\n", dt, position, velocity);

return 0;

But I am getting roughly the right velocity BUT two different final positions back when I use two different delta times (not due to floating point issues):

dt = 0.002000, p = 0.722848, v = 0.499308
dt = 0.090000, p = 0.784219, v = 0.473029

How can I fix this difference inside of my update loop?

How do I account for the extra acceleration between timesteps?

\$\endgroup\$
0
\$\begingroup\$

Although you're correcting your velocity, your position update:

position += velocity * dt;

...is still treating velocity as though it's a constant over the frame duration. So, the smaller your timestep, the more accurate this simplification is, but with longer timesteps, it gives too much weight to the high velocity early in the frame, without accounting for how it diminishes later in the frame.

To get this right, we'll need to integrate velocity over the frame duration with some calculus.

If your velocity is equal to...

$$\begin{align}\vec v_{\Delta T} &= \vec v_0 \cdot r^{\Delta T} \\&=\vec v_0 \cdot e^{\Delta T \ln r}\end{align}$$

Then your position is the integral of this value over your timestep:

$$\begin{align} \vec p_{\Delta T} &= \vec p_0 + \int_0^{\Delta T} {\vec v_0 \cdot e^{t\ln r}\ dt} \\&= \vec p_0 + \vec v_0 \cdot \left(\frac {e^{t \ln r}} {\ln r} \right) \Bigg|_0^{\Delta T} \\&= \vec p_0 + \vec v_0 \cdot \left(\frac {r^{\Delta T} - 1} {\ln r} \right) \end{align}$$

In code:

position += velocity * (std::pow(r, dt) - 1.0f)/std::log(r);
velocity *= std::pow(r, dt);
\$\endgroup\$
  • \$\begingroup\$ Is there a way to do this without knowing the initial v_0 - purely using the previous timestep's velocity? I want to constantly apply this damping over the entire game runtime. \$\endgroup\$ – user126218 Apr 1 at 12:43
  • \$\begingroup\$ Yes, here p_0 and v_0 are "position & velocity at the start of this timestep" as you can see in the code example at the bottom. We do not need to know initial conditions earlier than that — we can just treat the beginning of this timestep as time zero for our present purposes. \$\endgroup\$ – DMGregory Apr 1 at 12:49
  • \$\begingroup\$ Thanks so much for this. It works perfectly. I was looking in to semi-implicit euler and runge-kutta 4 as well. Is it any help using that to improve accuracy or will this equation always give a roughly exact answer? \$\endgroup\$ – user126218 Apr 1 at 13:12
  • \$\begingroup\$ This equation is exact to within the precision of your numbers. Note that any iterated accumulation like this is going to diverge eventually due to differences in rounding. A more general-purpose integrator could help you integrate this physics effect with other influences on the object though. \$\endgroup\$ – DMGregory Apr 1 at 13:16
  • \$\begingroup\$ Is it not always possible to create an exact solution using an equation such as this? \$\endgroup\$ – user126218 Apr 1 at 13:17
1
\$\begingroup\$

The solution given by DMGregory is the right solution, however, in game loops concerning physics, it's usually advised to use fixed time steps.

Having fixed time steps helps having consistent results across devices with different speeds/capabilities, see this answer for details.

such game loop should look something like this:

while (!exited)
{
    deltaTime = time - lastTime;
    timeSinceLastFixedUpdate += deltaTime;

    int fixedFramesCount = (int)(timeSinceLastFixedUpdate / fixedDeltaTime);
    if (fixedFramesCount > maxFixedFrames)
    {
        timeToLose = (maxFixedFrames - timeSinceLastFixedUpdate) * fixedDeltaTime;
        timeSinceLastFixedUpdate -= timeToLose;
        deltaTime -= timeToLose;
    }

    while (timeSinceLastFixedUpdate >= fixedDeltaTime)
    {
        FixedUpdate(fixedDeltaTime);
        timeSinceLastFixedUpdate  -= fixedDeltaTime;
    }

    Update(deltaTime);
    Render(deltaTime);

    lastTime = time;
}

Please note that engines like Unity have the two kinds of update (fixed time step and variable time step), that's because it updates the physics engine at fixed time step, and then it can interpolate/extrapolate physics results inside the variable time step Update calls in order to keep everything smooth at high frame-rates, updating particles, animation, etc. also happens in varying time steps.

\$\endgroup\$
  • 1
    \$\begingroup\$ Fixed Timesteps are a huge win for consistency. I'd still recommend using the correct integration formula for deltaTime though, even inside a fixed timestep. The reason being that if you as the developer later change your fixed timestep (say you developed at 50 Hz, but need to drop to 30 Hz for a mobile version), then you won't have to re-tune all your physics parameters to get back to the same feel on this version of the game. \$\endgroup\$ – DMGregory Apr 1 at 11:32
  • \$\begingroup\$ Thanks for the answer! I am already using a fixed timestep of dt but, for performance reasons, I am changing dt to a higher value when it lags behind instead of calling my update multiple times. \$\endgroup\$ – user126218 Apr 1 at 12:47
  • \$\begingroup\$ @DMGregory Agreed. \$\endgroup\$ – Esam Bustaty Apr 1 at 13:02
  • 1
    \$\begingroup\$ @LillyJonas In this case you're still forsaking consistency. I don't have an all-around solution for you but maybe you can try another compromise, the code I just edited-in solves the problem of lagging behing indefinitely, but on a slow device 1 second of simulation would take more than 1 second in realtime, if that's unacceptable then the only thing that's left is varying timestep, using the integration described in DMGregory's answer of course. \$\endgroup\$ – Esam Bustaty Apr 1 at 13:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy