Best Practices
Best Practices
These are the patterns the Parallax agent applies by default. Understanding them makes you a better collaborator with the agent — and a better Love2D developer.
Always use dt for movement
Never move objects by a fixed pixel amount per frame. Frame rate varies — fixed movement causes different speeds on different machines:
-- Bad
self.x = self.x + 5
-- Good
self.x = self.x + self.speed * dt
dt is the time (in seconds) since the last frame. Multiply any per-frame delta by dt to get frame-rate-independent movement.
Separate update and draw concerns
love.update handles logic. love.draw handles rendering. Never modify game state inside love.draw:
-- Bad — side effect inside draw
function love.draw()
score = score + 1 -- don't do this
love.graphics.print(score, 10, 10)
end
-- Good
function love.update(dt)
if coinCollected then score = score + 10 end
end
function love.draw()
love.graphics.print(score, 10, 10)
end
Use love.graphics.push/pop for transforms
When drawing entities with offsets, rotations, or scale, wrap in push/pop to avoid transform bleed:
function Player:draw()
love.graphics.push()
love.graphics.translate(self.x + self.w/2, self.y + self.h/2)
love.graphics.rotate(self.angle)
love.graphics.draw(sprites.player, -self.w/2, -self.h/2)
love.graphics.pop()
end
Preload all assets
Load images, audio, and fonts in love.load. Loading inside love.update or love.draw causes hitches:
function love.load()
-- All assets here
sprites.tileset = love.graphics.newImage('assets/images/tileset.png')
music.theme = love.audio.newSource('assets/audio/theme.ogg', 'stream')
end
Use 'stream' for long audio (music) and 'static' for short audio (sound effects).
Use quads for sprite sheets
Don't use one image per frame. Pack sprites into a sheet and use love.graphics.newQuad:
local sheet = love.graphics.newImage('assets/images/player-sheet.png')
local frameW, frameH = 16, 24
local frames = {}
for i = 0, 3 do -- 4 walk frames
frames[i+1] = love.graphics.newQuad(i * frameW, 0, frameW, frameH, sheet)
end
-- Draw frame 2
love.graphics.draw(sheet, frames[2], player.x, player.y)
The anim8 library automates this — the agent will suggest it for animated characters.
Keep conf.lua explicit
Always have a conf.lua that sets your window size, title, and disables unused modules:
function love.conf(t)
t.window.title = 'Dragon Dash'
t.window.width = 1280
t.window.height = 720
t.window.resizable = false
-- Disable modules you don't use (faster startup)
t.modules.joystick = false
t.modules.video = false
end
Error handling with love.errorhandler
Override the default error screen for production builds:
function love.errorhandler(err)
-- Log the error, show a friendly message
print('Error: ' .. tostring(err))
-- Optionally restart or show a custom screen
end
Structure your require paths
Keep your module paths consistent. All game modules live in the project root (or named subdirectories) and are required without the ./ prefix:
local Player = require('player')
local Tilemap = require('world.tilemap')
local bump = require('lib.bump')
The Parallax agent follows this convention by default.
Camera pattern
For scrolling worlds, use a simple camera offset rather than a full camera library for small games:
local cam = { x = 0, y = 0 }
function love.draw()
love.graphics.push()
love.graphics.translate(-cam.x, -cam.y)
-- draw world, entities...
love.graphics.pop()
-- draw HUD (not affected by camera)
end
function love.update(dt)
-- Follow player
cam.x = player.x - love.graphics.getWidth() / 2
cam.y = player.y - love.graphics.getHeight() / 2
end
For complex camera behaviour (lerp, screenshake, zoom), the agent will suggest the hump.camera module.