Game Dev

Love2D Patterns

Common reusable patterns for Love2D games — copy, adapt, use as agent prompts.

Love2D Patterns

These are copy-paste-ready patterns for common game mechanics. Each one is also a good starting point for an agent prompt — reference the pattern name and the agent will know what you mean.

Input handling

Centralise input into an input table updated each frame. Avoids scattered love.keyboard.isDown calls:

-- input.lua
local input = {}

function input.update()
  input.left   = love.keyboard.isDown('left', 'a')
  input.right  = love.keyboard.isDown('right', 'd')
  input.jump   = love.keyboard.isDown('space', 'w', 'up')
  input.attack = love.keyboard.isDown('z', 'x')
end

return input
-- player.lua
function Player:update(dt)
  if input.right then self.x = self.x + self.speed * dt end
  if input.left  then self.x = self.x - self.speed * dt end
  if input.jump and self.grounded then self:jump() end
end

Timer / tween system

Use flux for tweens and timers:

local flux = require('lib.flux')

-- In love.update
flux.update(dt)

-- Tween a value
flux.to(self, 0.3, { x = targetX }):ease('quadout')

-- One-shot timer
flux.to({}, 2.0, {}):oncomplete(function() spawnEnemy() end)

Simple pooling for bullets / particles

Object pools prevent garbage collection spikes:

local BulletPool = {}
BulletPool.__index = BulletPool

function BulletPool.new(size)
  local pool = setmetatable({ active = {}, inactive = {} }, BulletPool)
  for i = 1, size do
    table.insert(pool.inactive, { x=0, y=0, vx=0, vy=0, alive=false })
  end
  return pool
end

function BulletPool:spawn(x, y, vx, vy)
  local b = table.remove(self.inactive) or { alive=false }
  b.x, b.y, b.vx, b.vy, b.alive = x, y, vx, vy, true
  table.insert(self.active, b)
end

function BulletPool:update(dt)
  for i = #self.active, 1, -1 do
    local b = self.active[i]
    b.x = b.x + b.vx * dt
    b.y = b.y + b.vy * dt
    if outOfBounds(b) then
      b.alive = false
      table.remove(self.active, i)
      table.insert(self.inactive, b)
    end
  end
end

Screen shake

local shake = { duration = 0, intensity = 0 }

function triggerShake(duration, intensity)
  shake.duration  = duration
  shake.intensity = intensity
end

function love.update(dt)
  if shake.duration > 0 then shake.duration = shake.duration - dt end
end

function love.draw()
  local ox, oy = 0, 0
  if shake.duration > 0 then
    ox = (math.random() * 2 - 1) * shake.intensity
    oy = (math.random() * 2 - 1) * shake.intensity
  end
  love.graphics.push()
  love.graphics.translate(ox, oy)
  -- draw world
  love.graphics.pop()
end

Save / load with love.filesystem

local function saveGame(data)
  local encoded = require('lib.json').encode(data)
  love.filesystem.write('save.json', encoded)
end

local function loadGame()
  if love.filesystem.getInfo('save.json') then
    local content = love.filesystem.read('save.json')
    return require('lib.json').decode(content)
  end
  return nil
end

Sound manager

-- sounds.lua
local S = {}
local sources = {}

function S.load()
  sources.jump   = love.audio.newSource('assets/audio/jump.ogg',   'static')
  sources.coin   = love.audio.newSource('assets/audio/coin.ogg',   'static')
  sources.death  = love.audio.newSource('assets/audio/death.ogg',  'static')
  sources.music  = love.audio.newSource('assets/audio/theme.ogg',  'stream')
  sources.music:setLooping(true)
end

function S.play(name)
  if sources[name] then
    local clone = sources[name]:clone()
    clone:play()
  end
end

function S.playMusic()
  sources.music:play()
end

return S

Have a pattern that worked well in your game? Submit it as a docs contribution — it may end up here and in the agent's training data.