Fixing Love2D’s Variable Timestep with an Accumulator Loop
Learn how to decouple simulation from rendering in Love2D using a fixed‑timestep accumulator, avoid tunneling after hitches, and add interpolation for smooth frames.
11 Jan 2026, 14:56 UTC

The problem: variable dt can break your game after a hitch
Love2D’s default loop calls love.update(dt) each frame, where dt is the elapsed seconds since the last update. When the game runs smoothly, dt is small and movement looks fine. If the OS, a background task, or a vsync stall causes a single frame to take much longer (a “hitch”), dt spikes. Using that large dt directly to move objects can tunnel them through colliders or cause physics instability.
Useful takeaway: by accumulating dt and stepping the simulation at a fixed rate, you keep updates deterministic regardless of render‑frame timing.
Fixed‑timestep accumulator inside love.update
You do not need to replace love.run for most projects. Keep the default loop and place the accumulator logic in love.update. The pattern is:
- Add the frame’s
dtto an accumulator. - While the accumulator exceeds a fixed step size (e.g.,
1/60s), subtract the step size and advance the simulation. - Optionally clamp the accumulator to prevent a spiral of catch‑up steps after a long pause.
- Store the previous and current simulation states for interpolation during rendering.
-- main.lua
local FIXED_DT = 1/60 -- simulation step in seconds
local MAX_ACCUM = 0.25 -- clamp to avoid spiral of death
local accumulator = 0
local prevState = {} -- previous physics / game state
local currState = {} -- current state after each fixed step
function love.load()
-- initialize your game objects and copy them into prevState & currState
prevState = {x = 100, y = 100}
currState = {x = 100, y = 100}
end
function love.update(dt)
-- 1. accumulate time
accumulator = accumulator + dt
if accumulator > MAX_ACCUM then
accumulator = MAX_ACCUM
end
-- 2. fixed‑step simulation
while accumulator >= FIXED_DT do
-- store state before stepping
prevState.x = currState.x
prevState.y = currState.y
-- example: simple constant velocity movement
local speed = 200 -- pixels per second
currState.x = currState.x + speed * FIXED_DT
currState.y = currState.y + speed * FIXED_DT
accumulator = accumulator - FIXED_DT
end
-- 3. interpolation factor for rendering
-- alpha = how far we are between prevState and currState
local alpha = accumulator / FIXED_DT -- 0 .. 1
love.interpAlpha = alpha -- make it available to love.draw
end
function love.draw()
local alpha = love.interpAlpha or 0
-- interpolated render position
local renderX = prevState.x + (currState.x - prevState.x) * alpha
local renderY = prevState.y + (currState.y - prevState.y) * alpha
love.graphics.setColor(1, 1, 1)
love.graphics.circle('fill', renderX, renderY, 16)
-- optional diagnostics
love.graphics.setColor(1, 1, 0)
love.graphics.print('FPS: ' .. love.timer.getFPS(), 10, 10)
love.graphics.print('Delta: ' .. string.format('%.4f', love.timer.getDelta()), 10, 30)
end
Verifying the fixed step works
You can confirm that simulation steps occur at the intended rate regardless of render speed:
- Run the game and watch the console (or add
printinside thewhile accumulator >= FIXED_DTblock). The print should appear roughly 60 times per second, even if you artificially slow rendering withlove.timer.sleep(0.05)inlove.draw. - Toggle vsync in
love.conf(t.window.vsync = trueorfalse) and observe that the reported FPS changes while the simulation step rate (the prints) stays constant. - To test the dt‑clamp, insert a brief
love.timer.sleep(0.5)inlove.updateonce every few seconds. Without theMAX_ACCUMclamp you would see a burst of simulation steps; with the clamp the accumulator never exceeds the set limit, preventing a spiral of catch‑up steps.
Trade‑offs and limitations
While the accumulator loop gives deterministic updates, it introduces a few considerations:
- Latency: Interpolation adds up to one frame of input lag because the rendered state lags behind the latest simulation step. For fast‑reaction games you may choose to render the current state directly (
alpha = 1) and accept occasional micro‑stutter. - Complexity: You must keep a previous copy of every piece of state you want to interpolate (positions, rotations, animation frames). For very simple games where objects move linearly and tunneling is rare, using raw
dtmay be acceptable. - Clamp choice: The value of
MAX_ACCUMbalances safety against responsiveness. Too low and the game may appear to pause during a hitch; too high and you risk many catch‑up steps after a long pause. A common starting point is0.25s (four fixed steps at 60 Hz).
Practical way to check the result: after implementing the loop, run the game on a machine where you can temporarily throttle the CPU (e.g., using a performance‑profile tool) and verify that object positions continue to advance smoothly without sudden jumps.
Actionable closing
Start with the accumulator inside love.update as shown above. If you notice visible stutter when your monitor’s refresh rate does not divide evenly into your fixed step, add the interpolation factor (alpha) to your draw calls. Keep an eye on the accumulator clamp and adjust MAX_ACCUM based on the longest pause you expect in your target environment. With these steps your Love2D game will have stable physics and predictable movement, regardless of frame‑rate fluctuations.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.