Game Dev

The Mental Model

How to think about building Love2D games — the conceptual framework the Parallax agent uses.

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.