Game Dev

Best Practices

Proven patterns for Love2D games — what works, what to avoid, and why. Used by the Parallax agent.

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.