# a better lerp for smooth movement
Game developers often use linear interpolation (Lerp) as a very easy way to get smoother movement. However, the traditional `Mathf.Lerp` can lead to framerate-dependent behavior. Enter "Zeno's lerp" - a frame-rate independent alternative.
## The Problem with Traditional usage of Lerp
Consider this common Unity code:
```csharp
void Update()
{
transform.position = Vector3.Lerp(transform.position, targetPosition, 0.1f);
}
```
This approach has two main issues:
1. **Framerate-dependence**: movement speed changes with framerate.
You can help a little by replacing `0.1f` with `0.1f * Time.deltaTime`, but the results are still somewhat framerate-dependent.
As a simple proof, compare calling it once with `Time.deltaTime = 10`, which results in `transform.position` going all the way to `targetPosition`, and calling it 10 times with `Time.deltaTime = 1`, which results in `transform.position` going only 65% of the way to `transform.position`.
2. The constant factor (`0.1f` here) is not very intuitive and hard to tune IMO.
## Introducing Zeno's Lerp
What I call Zeno's lerp, solves these issues:
```csharp
public static class ZenoExtensions
{
public static Vector3 Zeno(this Vector3 current, Vector3 target, float timeConstant)
{
float alpha = 1f - Mathf.Exp(-Time.deltaTime / timeConstant);
return current + alpha * (target - current);
}
}
// Usage
void Update()
{
transform.position = transform.position.Zeno(targetPosition, 0.1f);
}
```
## Why Zeno's Lerp is Better
1. **Framerate Independence**: Regardless of how frequently it is called, `Zeno` will result in the same value for the same period of time.
2. **Intuitive**: The `timeConstant` parameter represents the time it takes to close about 63% of the distance to the target, making it easier to tune. Just set it to about how long you want it to take for it to cross half the distance.
3. **Built in Time.deltaTime**: I don't like having to write `Time.deltaTime` every time, so I just built it in to this implementation on the assumption that you'll basically always want it.
It's not as nice in the sense that it's not built-in to Unity or any other game engine I know of, but it's easy to implement for various types (Vector2, float, etc.) as extension methods.
## Comparison
Interactive comparison: [Traditional VS Zeno](https://observablehq.com/d/135d95440bd1b265).
Here the solid lines represent the behavior of `Zeno` and the dashed lines represent the traditional behavior of `Lerp`.
![[lerps.png]]
You can see the behavior of `Zeno` (solid lines) is totally framerate-independent. Even at 1fps (solid red line), it ends up at the same place after its singular frame as the 5fps and 30fps cases do after 1 second.
The behavior of `Lerp` (dashed lines) is not at all framerate-independent. At 5fps (dashed blue line), it shoots out above the the behavior at 30fps (dashed green) and it never goes back.
As the framerate gets lower it gets even worse. I didn't even bother graphing Lerp at 1fps, because it's so bad it ruins the whole graph. (Although if you use a clamped Lerp it is not completely disastrous.)
However, you can see that at 30fps (dashed green), `Lerp` approximates `Zeno` quite well. In fact, in the limit the two modes of interpolation are identical. So `Zeno` essentially gives you what the traditional `Lerp` would have given you if you had infinite FPS.
#### Reproduce this graph
```python
import numpy as np
import matplotlib.pyplot as plt
# Constants
time_constant = 0.3
total_time = 1 # Total time to simulate
initial_position = 0
target_position = 1
# Time arrays for different frame rates
time_1fps = np.linspace(0, total_time, 2) # 1 update per second
time_5fps = np.linspace(0, total_time, 6) # 10 updates per second
time_30fps = np.linspace(0, total_time, 31) # 30 updates per second
def zeno_lerp(current, target, delta_time):
alpha = 1 - np.exp(-delta_time / time_constant)
return current + alpha * (target - current)
def traditional_lerp(current, target, t):
return current + t * (target - current)
# Simulate movement for different frame rates using Zeno's lerp
positions_zeno_1fps = [initial_position]
positions_zeno_5fps = [initial_position]
positions_zeno_30fps = [initial_position]
for dt in np.diff(time_1fps):
positions_zeno_1fps.append(zeno_lerp(positions_zeno_1fps[-1], target_position, dt))
for dt in np.diff(time_5fps):
positions_zeno_5fps.append(zeno_lerp(positions_zeno_5fps[-1], target_position, dt))
for dt in np.diff(time_30fps):
positions_zeno_30fps.append(
zeno_lerp(positions_zeno_30fps[-1], target_position, dt)
)
# Simulate movement for different frame rates using traditional lerp with t=0.1
positions_traditional_1fps = [initial_position]
positions_traditional_5fps = [initial_position]
positions_traditional_30fps = [initial_position]
positions_traditional_144fps = [initial_position]
for _ in range(1, len(time_1fps)):
positions_traditional_1fps.append(
traditional_lerp(
positions_traditional_1fps[-1], target_position, 1 * (1 / time_constant)
)
)
for _ in range(1, len(time_5fps)):
positions_traditional_5fps.append(
traditional_lerp(
positions_traditional_5fps[-1],
target_position,
1 / 5 * (1 / time_constant),
)
)
for _ in range(1, len(time_30fps)):
positions_traditional_30fps.append(
traditional_lerp(
positions_traditional_30fps[-1],
target_position,
1 / 30 * (1 / time_constant),
)
)
# Plotting
plt.figure(figsize=(12, 8))
plt.plot(time_1fps, positions_zeno_1fps, "ro-", label="Zeno Lerp 1 FPS")
plt.plot(time_5fps, positions_zeno_5fps, "bo-", label="Zeno Lerp 5 FPS")
plt.plot(time_30fps, positions_zeno_30fps, "go-", label="Zeno Lerp 30 FPS")
# plt.plot(time_1fps, positions_traditional_1fps, "r:", label="Traditional Lerp 1 FPS")
plt.plot(time_5fps, positions_traditional_5fps, "b:", label="Traditional Lerp 5 FPS")
plt.plot(time_30fps, positions_traditional_30fps, "g:", label="Traditional Lerp 30 FPS")
plt.title("Comparison of Zeno's Lerp vs Traditional Lerp at Different Frame Rates")
plt.xlabel("Time (s)")
plt.ylabel("Position")
plt.legend()
plt.grid(True)
plt.show()
```