Managing Game State and Framerate Independence in Love2D
Learn how to implement framerate-independent movement and a basic state machine in Love2D to prevent jittery gameplay and messy code.
12 Feb 2026, 18:30 UTC

The Problem: Jittery Movement and State Chaos
When building a game in Love2D, two common pitfalls emerge early: objects moving faster on high-refresh-rate monitors and a main.lua file that becomes an unmanageable nest of if/else statements as you add menus, gameplay, and pause screens.
The solution lies in leveraging the dt (delta time) parameter for all movement and implementing a lightweight state manager to decouple your game logic from your rendering code.
Framerate Independence with Delta Time
Love2D uses a callback architecture. The love.update(dt) function runs as often as the hardware allows. If one player has a 60Hz monitor and another has a 144Hz monitor, the 144Hz player's love.update will trigger more frequently.
If you move a character by 5 pixels every frame, the 144Hz player will move more than twice as fast. To fix this, use dt—the time elapsed since the last frame in seconds.
The Calculation
Instead of position = position + speed, use position = position + (speed * dt). This changes the unit of speed from "pixels per frame" to "pixels per second." Regardless of the framerate, the object will cover the same distance in one real-world second.
Separating Logic from Rendering
A critical engineering rule in Love2D is that love.draw is for rendering only. You should never mutate game state (like updating a timer or moving a player) inside love.draw. Because the framework may skip draw calls or call them at different intervals than update calls, putting logic here leads to inconsistent game behavior and "teleporting" objects.
Implementing a Simple State Machine
Since Love2D doesn't provide a built-in scene manager, you can implement one using a variable to track the current state. This allows you to route the update and draw calls to different logic blocks based on whether the player is in the menu or the game.
Worked Example: State-Based Movement
The following example demonstrates a basic state switch and framerate-independent movement. Save this as main.lua and run it with the Love2D executable.
-- Global state variable
local gameState = "menu"
local player = { x = 100, y = 100, speed = 200 }
function love.load()
-- Initialization happens once here
end
function love.update(dt)
if gameState == "game" then
-- Move right at 200 pixels per second
player.x = player.x + (player.speed * dt)
-- Reset position if it goes off screen
if player.x > love.graphics.getWidth() then
player.x = 0
end
end
end
function love.draw()
if gameState == "menu" then
love.graphics.print("Press 'Enter' to Start", 10, 10)
elseif gameState == "game" then
love.graphics.rectangle("fill", player.x, player.y, 50, 50)
love.graphics.print("Game Running - Press 'Escape' for Menu", 10, 10)
end
end
function love.keypressed(key)
if key == "return" then
gameState = "game"
elseif key == "escape" then
gameState = "menu"
end
end
Trade-offs and Limitations
Using a simple string or integer for gameState is efficient for small projects. However, as your game grows, this approach leads to a massive main.lua. For larger projects, consider moving each state into its own Lua table or module with its own update and draw functions, which you can then call from the main callbacks.
Additionally, be aware that love.update is not a "fixed timestep" by default. For physics-heavy games where precision is vital (like platformers), you may need to implement a custom accumulator to ensure physics updates happen at a constant rate regardless of the rendering speed.
Verification
To verify your implementation:
- Movement: Change the
player.speedvalue. If the object moves consistently across different hardware or when you artificially cap the framerate,dtis working correctly. - State: Press the designated keys to toggle states. Ensure that the player object stops moving (the
updatelogic stops) when the state is switched back to "menu".
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.