0
\$\begingroup\$

I'm implementing friction into my game, and I'd like it to be framerate-independent. Here's my first solution, in Lua style pseudo-code:

local pos = 0
local vel = 100
local deceleration = 80

function Game:update(dt)
    vel = move_toward(vel, 0.0, deceleration * dt)
    pos = pos + vel * dt
end

with move_toward being identical to the way it works in engines like Godot. Here's the implementation just in case but it's not that relevant.

function move_toward(from, to, delta)
    if math.abs(to - from) <= delta then
        return to
    else
        return from + sign(to - from) * delta
    end
end

Now, this seems to work fine but I noticed that there's still some very small disparities when I try to run this at different framerates: video link (top is 60 FPS, middle is 30 FPS, bottom is 15 FPS). I can't seem to figure out why that is.

I'm not trying to make a perfect or realistic simulation, just one that's good enough for games. I'm familiar with algebra and analysis, but I'd like something that is simple to implement, it's better if I can avoid complicated formulas or bits of code.

\$\endgroup\$
1
  • \$\begingroup\$ Here is some past Q&A about solving similar problems with a different friction formula: option 1 & option 2. \$\endgroup\$
    – DMGregory
    Commented Mar 5 at 22:37

3 Answers 3

1
\$\begingroup\$

The problem you're running into is that you're performing numeric integration with a varying step size. In all but the simplest case (movement at a constant speed), this produces results that vary depending on the step size. There are two ways to fix this:

  1. The most common (and usually the easiest) is to decouple your physics framerate from your drawing framerate. In a single-threaded program like you appear to have, that means figuring out how many physics frames have taken place since you last drew the screen, and running the physics loop that many times. Keep in mind that this is more than just "divide delta by 1/framerate", because you might have a partial physics frame left over from a previous drawing frame.

    In a multithreaded situation, you'd have a dedicated physics thread running at a constant framerate, and the rendering thread would pick up the results of the most recent physics frame whenever it needed to draw something.

  2. The less common option is to symbolically integrate, giving a formula that is independent of framerate. For example, deceleration would have the formula "position = initial_position + initial_speed * t - 0.5 * deceleration * t * t", where "t" is the time since deceleration began. This is usually harder than separating physics from drawing, and for more complex motion, isn't even always possible -- but when it is, it's more accurate than going with option 1.

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

This is my best guess on how you would achieve something like this:

Since code is run frame by frame there is no way to create a truly independent loop that runs separately from the regular loop (that I know of). However, you can use a sneaky trick to calculate how many of these passes have happened each frame, here is an example:

Lets say we want the code to run 100 times a second, we need to calculate the distance in fixed delta time between each seperate pass.

// This is in C# by the way so use whatever you would use for a floating point number
float fixedDeltaTime = 1/100;

Now we would need to calculate how many of these passes have happened using normal delta time, or the time between the current frame and the previous frame. For our example we're updating fixed timestep 100 times a second, or every 0.01 seconds. So lets say it has taken 4 seconds to render, that means that 400 fixed time loops have passed. So we can multiply this by whatever you normally use in the fixed timestep.

Lets say you want to move a player using this "framerate independent" timestep, instead of player.x += 1 * deltaTime you would use player.x += 1 * deltaTime / fixedDeltaTime

Hopefully this helps you to create a fixed timestep.

\$\endgroup\$
4
  • \$\begingroup\$ This doesn't work as the velocity varies over the duration of the timestep itself (due to friction), rather than remaining constant for the whole deltaTime. \$\endgroup\$
    – Basic
    Commented Mar 6 at 2:52
  • \$\begingroup\$ @Basic ah, I dont really know about any of that, I thought he was just asking how to implement a fixed timestep. \$\endgroup\$
    – Pow
    Commented Mar 6 at 9:48
  • 1
    \$\begingroup\$ Tip: we try not to assume everyone posting here uses "he" pronouns unless they tell us so. After all, lots of women and non-binary folks make games too. 😉 When in doubt, click on a user's ID to check whether they specify a name or pronoun to use in their profile. If not, "they" makes a pretty decent fallback option. \$\endgroup\$
    – DMGregory
    Commented Mar 6 at 12:18
  • \$\begingroup\$ @DMGregory my mistake. \$\endgroup\$
    – Pow
    Commented Mar 6 at 20:18
0
\$\begingroup\$

Imagine a graph of velocity over time, descending toward zero in a linear ramp.

In any given time slice, the area under that curve is the resulting position displacement over that interval.

In the bulk of the ramp, this is just the average of the velocity at the start and end of the interval times the duration.

At the toe of the ramp where it hits zero, we just need to make sure we're not counting the plateau at the end after the object stopped moving. So we can calculate time the object is actually decelerating in the interval, and use that.

Here's what that would look like in code:

decelTime = min(abs(vel)/deceleration, dt)
vel_end = move_toward(vel, 0.0, deceleration * dt)

pos = pos + 0.5 * (vel + vel_end) * decelTime
vel = vel_end

Note that while this will look smoother, it still doesn't guarantee perfect framerate independence, because we're working with finite precision numbers: rounding and accumulating errors with each operation. So running at a high framerate, you'll take smaller steps and encounter more of this rounding error, while a low framerate will move in larger chunks, and round less often.

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .