Stabilize LÖVE Physics with a Fixed-Timestep Accumulator
Variable dt in love.update makes physics frame-rate dependent. Learn how an accumulator-based fixed timestep clamps spikes and keeps simulation deterministic.
26 Jun 2026, 09:06 UTC

The problem: dt is not a steady heartbeat
You drag the LÖVE window to another monitor, and your player suddenly tunnels through a wall. Nothing in your game logic changed, but dt did. LÖVE passes dt to love.update(dt) as the wall-clock seconds since the last frame. Lua garbage collection, OS scheduling, and window dragging can make that value spike or drop to zero. If you move bodies or resolve collisions using that raw number, your simulation becomes frame-rate dependent and non-deterministic.
The useful takeaway is to simulate in constant-size steps, accumulate the leftover time, and clamp large dt so a pause never turns into an ever-growing backlog of catch-up frames.
Why raw dt breaks physics
The default LÖVE loop calls love.update(dt), then love.draw(). Under the hood, love.timer.step() advances the clock and returns the elapsed time. Because love.graphics.present() usually blocks on vsync, the loop is paced by your display. Disable vsync in conf.lua and the loop runs uncapped, dt shrinks, and your fixed steps fire less often. On a 144 Hz monitor, a 60 Hz simulation without interpolation can show subtle judder.
Variable timestep is simpler and fine for cosmetic animation, but it makes collision tunneling vary with frame rate and prevents replays from matching across machines.
The accumulator pattern
A fixed-timestep accumulator keeps simulation deterministic. You add incoming dt to an accumulator, then run your step logic in a loop while the accumulator holds at least one full step. Each iteration subtracts the step size and carries the remainder forward. The remainder doubles as a render interpolation factor.
To prevent the "spiral of death"—where falling behind forces longer frames, which creates more backlog—clamp both the incoming dt and the number of catch-up steps per frame.
| Approach | Deterministic? | Best for |
|---|---|---|
Raw dt in love.update |
No | Simple visual effects, non-physics games |
| Fixed timestep accumulator | Yes | Physics, replays, networked games |
Worked example in Lua
Save this as main.lua in your project folder. Run it with love . from that directory on macOS or Linux, or drag the folder onto love.exe on Windows. No special permissions are required.
-- conf.lua
function love.conf(t)
t.window.vsync = true -- display-paced by default; false runs uncapped
end
local STEP = 1 / 60
local MAX_DT = 0.1
local MAX_STEPS = 5
local accumulator = 0
local render_alpha = 0
function love.update(dt)
-- Guard against debugger breaks, alt-tabbing, and window drags
dt = math.min(dt, MAX_DT)
accumulator = math.min(accumulator + dt, MAX_DT * 2)
local steps = 0
while accumulator >= STEP and steps < MAX_STEPS do
update_physics(STEP) -- implement your constant-step logic here
accumulator = accumulator - STEP
steps = steps + 1
end
-- alpha is the fraction of a step left over, used for render interpolation
render_alpha = accumulator / STEP
store_render_state(render_alpha) -- implement this to blend previous and current state
end
function love.draw()
-- draw using the interpolated state prepared in love.update
end
update_physics and store_render_state are placeholders for your own code. The accumulator ceiling prevents a single huge dt from queuing dozens of steps. The step cap guarantees a frame never spends too long catching up.
Expected check: open the LÖVE console, drag the window around, and watch the logged dt values. The clamp should engage during spikes, and the accumulator should stay bounded.
Interpolation and its trade-off
love.draw receives no dt, so interpolation must happen in love.update. Store the previous and current simulation states, then blend them using alpha at draw time. This removes judder on high-refresh displays, but it costs memory and complexity because you must keep two states in sync.
If you do not need interpolation, you can draw the latest state directly. The simulation remains deterministic; you may just see slight stutter when the display refresh rate does not divide evenly into your step rate.
Limitations and practical verification
A fixed step stabilizes simulation timing, but it does not remove stutter caused by Lua allocation churn. Garbage-collection pauses still hitch the frame. Vsync and timer behavior also differ across Windows, macOS, and Linux GPU drivers, so do not assume 60 Hz on every machine.
To verify the pattern, create a minimal project that logs dt and accumulator length each frame. Run a physics-heavy scene with many moving bodies, then compare fixed-step behavior against raw dt. Collisions should no longer depend on frame rate. Check love --version and test on each target OS before shipping.
Closing
Replace raw dt movement with an accumulator-based fixed timestep when physics consistency matters. Clamp dt and step count, interpolate for rendering if your display refresh justifies it, and verify with raw dt logs rather than averaged FPS.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.