The Mental Model
The Mental Model
Before writing a line of Lua, it helps to have a clear mental model for how Love2D games are structured. This is the same model the Parallax agent uses internally — so the more you think in these terms, the better your collaboration with the agent will be.
The game loop
Love2D exposes three core callbacks. Everything in your game flows through them:
function love.load()
-- Run once at startup: load assets, initialise state
end
function love.update(dt)
-- Run every frame: advance game state, handle physics, check input
-- dt = time since last frame in seconds (always use this for movement)
end
function love.draw()
-- Run every frame: render the current state to the screen
-- Never modify game state here — only read it
end
The golden rule: love.update changes state. love.draw reads state and renders it. Never cross these concerns.
Entities as tables
In Love2D, the natural way to represent game objects (player, enemies, bullets, items) is as Lua tables with method-like functions:
local Player = {}
Player.__index = Player
function Player.new(x, y)
return setmetatable({ x = x, y = y, speed = 200, vy = 0 }, Player)
end
function Player:update(dt)
-- movement, gravity, collision
end
function Player:draw()
love.graphics.rectangle('fill', self.x, self.y, 16, 24)
end
return Player
Every entity has :update(dt) and :draw(). The game loop calls both for every active entity. This pattern scales from 3 entities to 300.
State machines for game flow
Use a simple state machine to manage high-level game states — menus, gameplay, pause, game over:
local state = 'menu' -- 'menu' | 'playing' | 'paused' | 'game_over'
function love.update(dt)
if state == 'playing' then
player:update(dt)
for _, e in ipairs(enemies) do e:update(dt) end
end
end
function love.draw()
if state == 'menu' then drawMenu()
elseif state == 'playing' then drawGame()
elseif state == 'paused' then drawPause()
end
end
The Parallax agent will suggest this pattern automatically for any project that involves multiple game screens.
Collision via bump.lua
Love2D has no built-in collision resolution. The standard choice is bump.lua — a simple AABB library that handles tunnelling, slopes, and one-way platforms.
local bump = require('lib.bump')
local world = bump.newWorld(64) -- cell size
-- Add items to the world
world:add(player, player.x, player.y, player.w, player.h)
-- In player:update(dt)
local goalX = self.x + dx
local goalY = self.y + dy
local actualX, actualY, cols = world:move(player, goalX, goalY)
self.x, self.y = actualX, actualY
The agent uses bump.lua for all collision by default. If you want a different approach, tell it in .parallax/context.json.
Assets as module-level variables
Load assets in love.load and store them in module-level variables. Never load inside love.draw — that causes frame drops:
-- main.lua
local sprites = {}
local sounds = {}
function love.load()
sprites.player = love.graphics.newImage('assets/images/player.png')
sounds.jump = love.audio.newSource('assets/audio/jump.ogg', 'static')
end
Coordinate system
Love2D's origin (0, 0) is the top-left corner of the window. X increases to the right, Y increases downward. This trips up developers coming from other engines. The Parallax agent accounts for this automatically.
What this means for prompting
When you prompt the agent using these concepts — entities, state machines, bump.lua collisions — it produces cleaner, more idiomatic code with fewer revisions. The more your .parallax/context.json captures these conventions, the better.