Using lerp with delta-time

14
Official Construct Team Post
Ashley's avatar
Ashley
  • 25 Jan, 2015
  • 1,412 words
  • ~6-9 mins
  • 24,970 visits
  • 15 favourites

Recently on the forums came up the question of how to use dt (delta time) correctly with lerp. This turned out to be a surprisingly tricky question, and is a good demonstration of how knowledge of maths comes in handy when designing games.

Just to recap, lerp(a, b, x) is a system expression that calculates a + x * (b - a). In other words, it returns the number x% of the way from a to b, e.g. lerp(100, 200, 0.25) gives the number 25% of the way from 100 to 200, which is 125. This is called linear interpolation (lerp for short) and is often very useful in game design. dt is short for "delta time", or the time in seconds since the last frame, covered in more detail in the tutorial delta-time and framerate independence.

A common trick to smoothly make an object move to a destination is to lerp its current position to its destination. To just take the X axis, this might look like "Every tick, set X to lerp(X, end, factor)". The tricky part about this is since it runs every tick, the result will be dependent on the framerate unless it takes in to account delta-time. So the question is: how should dt be used in the factor?

Without delta-time

If you don't use delta-time, the result is framerate dependent (i.e. may run faster or slower than intended depending on the framerate). One example of doing this is something like:

a = lerp(a, b, 0.1)

This means every tick it goes 10% of the way to the destination. Notice this amount changes every tick since the result is assigned back to a. So using the X position example again, if the object has 100 pixels to cover, the first tick it covers 10 pixels and has 90 remaining. The next tick, it covers another 9 pixels and has 81 remaining. Next it covers 8.1 pixels, and so on. Since the remaining distance reduces, so does the distance it covers, making it appear to slow down as it gets closer. The object may appear to stop, but in fact it never exactly arrives - it just keeps getting exponentially closer and closer.

This can be proved to be framerate dependent by comparing the results at two different framerates. The easiest way to consider the problem is to think about the distance remaining, instead of the distance covered. If the distance covered every frame is 0.1 (10%), then every frame the remaining distance is 0.9 (90%) of what it was previously. So over 3 frames, the remaining distance is 0.9 x 0.9 x 0.9, or 0.9^3. More generally, the distance remaining m over n frames is m^n.

To keep the numbers reasonable, let's use low example framerates. With m = 0.9, at 5 FPS it will step 5 times and have 0.9^5 remaining, which is about 0.59. At 10 FPS it will step 10 times and have 0.9^10 remaining, which is about 0.35. So the object covers a different distance in one second depending on the framerate. Oops!

A good approximation

The most obvious way to use delta-time - which turns out to work well in practice, but is actually not mathematically exact - is to simply put dt as the factor:

a = lerp(a, b, dt)

Often here dt is multiplied by a number to change the speed, e.g. 0.5 * dt, but to keep the math simpler let's assume it's just dt on its own (the same as 1 * dt).

This is a very good approximation, but is not actually exactly correct. More worryingly, in some cases it can overshoot its destination.

This time, the distance remaining m after one tick is 1 - dt. Over n frames, the distance remaining is (1 - dt)^n. Expanding this formula quickly gets complicated. Even with the example of n = 5 (for 5 FPS) you get a long expansion (check it on Wolfram Alpha), and comparing it with n = 10 is even harder. The easiest way to prove it isn't exact is a counter-example.

Remember that dt is the time since the last tick, which assuming a steady framerate is 1 divided by the FPS. Therefore to work out the remaining distance after one second we can substitute dt with 1 / n, giving m = (1 - (1 / n))^n.

At 30 FPS, that works out as (1 - (1 / 30))^30 which is about 0.36166. However at 60 FPS, that works out as (1 - (1 / 60))^60, which is about 0.36479. It's different! It's very, very close - so close you'll probably never notice the difference - but the difference shows it's not exactly correct.

The bigger problem is the possibility of overshoot. Since dt is often multiplied by a number to change the rate it approaches the target, it's possible to multiply dt to be larger than 1. For example if you use a = lerp(a, b, 50 * dt), at 40 FPS dt is 0.025, which multiplied by 50 is 1.25, or 125% of the way to the target. This means it overshoots by 25%! Next frame it will come back the other way, and end up doing a crazy yoyo back and forth over the target. If you use a low multiple it should be OK though.

Realistically if you use this formula in your project, it is probably just fine. It's unlikely to be necessary to go through all your work updating this formula. But what is the exact answer?

Perfect accuracy

The answer is not entirely obvious. It's actually:

a = lerp(a, b, 1 - f ^ dt)

...where f is the factor between 0 and 1 deciding how quickly it catches up, e.g. 0.25. In fact it works out that this is the remaining factor per second, so if it's 0.25 it means it covers 75% of the remaining distance every second - independent of the framerate!

So why does this work? Since we cover 1 - f^dt, the remaining distance is f^dt. Over n frames, the remaining distance is thus (f^dt)^n. Again we can assume a steady framerate and that dt is equal to 1 / n. This gives the remaining distance over one second at n frames per second as (f^(1 / n))^n. If you know your powers, you know that (x^a)^b = x^ab. So we can simplify and see that the remaining distance after one second is f^1 = f. So it's covered the same distance in one second regardless of the framerate!

This has the nice quality of being able to easily decide the percentage of the distance it covers every second. As before using f = 0.25 means it covers 75% of the distance regardless of the framerate, whereas trying to figure out the multiplier when just using f * dt to get it to cover 75% of the distance per second is very tricky (actually mathematically impossible, since it depends on the framerate), and probably just means guessing a few numbers and eyeballing it. Also as long as 0 < f < 1, it will never overshoot.

Why doesn't Construct 2 automatically apply dt?

This question comes up from time to time, and is probably now on the mind of anyone who would prefer to avoid the maths. The answer is: it does, at least for the built-in behaviors which all use dt. However in the event system, Construct 2 simply cannot tell when dt should be applied or not. For example if an event runs for a one-off tick to rotate an object 90 degrees, it should not apply dt - but if the event runs every tick and is intended to rotate the object, it should apply dt. There's no way of automatically telling whether events run every tick or not, and anyway it's probably also impossible to automatically tell where dt ought to be inserted in to an arbitrary formula even if it does run every tick. So it's something you have to deal with yourself.

Conclusion

So there you go - the correct factor to use with lerp is 1 - f ^ dt. But if you're using f * dt, you'll probably get away with it. However it serves to show how knowledge of maths and how game engines really work is still useful when designing games, even if you're using a tool like Construct 2 which is doing a lot behind the scenes for you.

Subscribe

Get emailed when there are new posts!