Separate Fixed-Rate Gameplay Simulation from Rendering in LÖVE 2D
Use love.fixedUpdate for deterministic simulation at a fixed timestep while rendering independently in love.draw; avoid double-updating state and handle input latching for precision.
05 Nov 2025, 20:26 UTC

Useful Answer
Place all gameplay logic — movement, collision, rule evaluation — inside love.fixedUpdate(dt). Render the resulting state from love.draw(). Use love.update(dt) only for per-frame work such as UI animation or audio that does not advance the simulation.
Mechanism
love.timer.step(1/60) tells the engine to advance the simulation in 1/60‑second increments. The callback love.fixedUpdate(dt) receives that fixed dt each step, while love.update(dt) receives the actual elapsed time since the last rendered frame. This decouples the deterministic simulation rate from the variable render rate.
Worked Example
function love.load()
love.timer.step(1/60) -- fixed 60 Hz simulation
player = {x = 0, y = 0, speed = 100}
end
function love.fixedUpdate(dt)
-- dt is always 1/60 here
local dx = (love.keyboard.isDown('right') and 1 or 0)
- (love.keyboard.isDown('left') and 1 or 0)
player.x = player.x + dx * player.speed * dt
end
function love.update(dt)
-- frame‑local work only (e.g. particle timers)
end
function love.draw()
love.graphics.rectangle('fill', player.x, player.y, 20, 20)
end
Multiplying speed by the supplied dt guarantees the same distance per simulated second regardless of the display’s refresh rate.
Limits and Common Mistakes
- Fixed updates ≠ one per frame. A stalled host can trigger catch‑up steps, so gameplay may advance in bursts.
- Input sampling. Reading
love.keyboard.isDownonly infixedUpdatecan miss very short presses; latch key events inlove.keypressedwhen precision matters. - Version sensitivity. Older LÖVE releases lack
love.fixedUpdateorlove.timer.step; fall back to a manual accumulator or require a compatible version. - Double‑counting dt. Applying movement in both
fixedUpdateandupdate(or multiplying by both fixed and framedt) makes motion frame‑rate dependent. - Mutating state in
love.draw. Its call frequency follows rendering, breaking determinism.
Verification
- In a minimal project, increment counters in
fixedUpdateandupdateand print them once per second. The fixed counter should stay near 60 Hz while the frame counter follows the monitor refresh. - Insert a deliberate
love.timer.sleep(0.1)inlove.updatefor a few seconds, then compareplayer.xwith the expected distance (speed * simulated_seconds). The result should match within normal floating‑point tolerance. - Consult the reference bundled with your installed LÖVE release for
love.timer.step,love.fixedUpdate, andlove.update; confirm behavior under pause, focus loss, and a long stall. - Run a rapid key‑tap test at 30 Hz, 60 Hz, and 144 Hz render rates to verify whether your input strategy misses presses.
These checks give a practical way to confirm that the simulation stays deterministic while rendering remains smooth.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.