LOVE2D Cheat Sheet
Overview
LOVE (also written as Love2D) is a free, open-source 2D game framework that uses the Lua programming language. It provides a simple yet powerful set of APIs for creating 2D games, covering graphics rendering, audio playback, physics simulation (via Box2D), input handling, file system access, and window management. LOVE is designed for rapid prototyping and game development, with a philosophy of keeping things simple and getting out of the developer’s way.
LOVE runs on Windows, macOS, Linux, Android, and iOS. It uses OpenGL for hardware-accelerated rendering and supports shaders written in GLSL. The framework is particularly popular in game jam communities due to its minimal setup requirements and fast iteration cycle — you can start coding a game with just a text editor and the LOVE executable. The distribution model is simple: package your Lua files with the LOVE runtime to create standalone executables.
Installation
# macOS
brew install love
# Ubuntu/Debian
sudo add-apt-repository ppa:bartbes/love-stable
sudo apt update
sudo apt install love
# Arch Linux
sudo pacman -S love
# Windows
# Download from https://love2d.org
# Add to PATH or use installer
# Verify installation
love --version
# Run a game
love /path/to/game-directory
love game.love # Packaged game
# Create project structure
mkdir mygame
cd mygame
touch main.lua conf.lua
Basic Game Structure
-- main.lua
function love.load()
-- Called once at startup
player = {
x = 400,
y = 300,
speed = 200,
width = 32,
height = 32
}
score = 0
love.window.setTitle("My Game")
end
function love.update(dt)
-- Called every frame (dt = delta time in seconds)
if love.keyboard.isDown("left") or love.keyboard.isDown("a") then
player.x = player.x - player.speed * dt
end
if love.keyboard.isDown("right") or love.keyboard.isDown("d") then
player.x = player.x + player.speed * dt
end
if love.keyboard.isDown("up") or love.keyboard.isDown("w") then
player.y = player.y - player.speed * dt
end
if love.keyboard.isDown("down") or love.keyboard.isDown("s") then
player.y = player.y + player.speed * dt
end
end
function love.draw()
-- Called every frame for rendering
love.graphics.setColor(0, 0.5, 1)
love.graphics.rectangle("fill", player.x, player.y, player.width, player.height)
love.graphics.setColor(1, 1, 1)
love.graphics.print("Score: " .. score, 10, 10)
end
Configuration
-- conf.lua (loaded before main.lua)
function love.conf(t)
t.title = "My Game"
t.version = "11.5" -- LOVE version compatibility
t.window.width = 800
t.window.height = 600
t.window.resizable = true
t.window.vsync = 1
t.window.msaa = 4 -- Anti-aliasing samples
t.window.fullscreen = false
t.window.fullscreentype = "desktop"
-- Modules (disable unused ones for performance)
t.modules.joystick = true
t.modules.physics = true
t.modules.audio = true
t.modules.video = false -- Disable if not needed
end
Graphics
-- Drawing primitives
function love.draw()
-- Colors (0-1 range)
love.graphics.setColor(1, 0, 0) -- Red
love.graphics.setColor(0, 1, 0, 0.5) -- Semi-transparent green
-- Shapes
love.graphics.rectangle("fill", 100, 100, 50, 30)
love.graphics.rectangle("line", 200, 100, 50, 30)
love.graphics.rectangle("fill", 300, 100, 50, 30, 8, 8) -- Rounded
love.graphics.circle("fill", 400, 300, 25)
love.graphics.circle("line", 500, 300, 25)
love.graphics.ellipse("fill", 600, 300, 40, 20)
love.graphics.line(0, 0, 800, 600)
love.graphics.polygon("fill", 300, 50, 350, 150, 250, 150)
love.graphics.arc("fill", 400, 400, 50, 0, math.pi)
love.graphics.points(100, 200, 150, 250, 200, 200)
-- Reset color
love.graphics.setColor(1, 1, 1)
end
-- Images
function love.load()
playerImg = love.graphics.newImage("sprites/player.png")
playerImg:setFilter("nearest", "nearest") -- Pixel art
end
function love.draw()
love.graphics.draw(playerImg, x, y)
-- With rotation, scale, origin
love.graphics.draw(playerImg, x, y, rotation, scaleX, scaleY, originX, originY)
end
-- Sprite sheets / Quads
function love.load()
spritesheet = love.graphics.newImage("spritesheet.png")
quads = {}
local frameW, frameH = 32, 32
for y = 0, spritesheet:getHeight() - frameH, frameH do
for x = 0, spritesheet:getWidth() - frameW, frameW do
table.insert(quads, love.graphics.newQuad(
x, y, frameW, frameH,
spritesheet:getDimensions()
))
end
end
end
function love.draw()
love.graphics.draw(spritesheet, quads[currentFrame], x, y)
end
Input Handling
-- Keyboard (event-based)
function love.keypressed(key, scancode, isrepeat)
if key == "space" then
player:jump()
elseif key == "escape" then
love.event.quit()
end
end
function love.keyreleased(key)
if key == "space" then
player:stopJumping()
end
end
-- Keyboard (polling in update)
function love.update(dt)
if love.keyboard.isDown("left") then
player.x = player.x - speed * dt
end
end
-- Mouse
function love.mousepressed(x, y, button, istouch)
if button == 1 then -- Left click
shoot(x, y)
end
end
function love.mousemoved(x, y, dx, dy)
-- dx, dy are delta movement
crosshair.x = x
crosshair.y = y
end
function love.wheelmoved(x, y)
if y > 0 then zoomIn()
elseif y < 0 then zoomOut() end
end
-- Touch (mobile)
function love.touchpressed(id, x, y, dx, dy, pressure)
touches[id] = { x = x, y = y }
end
-- Gamepad
function love.gamepadpressed(joystick, button)
if button == "a" then player:jump() end
end
function love.update(dt)
local lx = love.joystick.getGamepadAxis(joystick, "leftx")
local ly = love.joystick.getGamepadAxis(joystick, "lefty")
if math.abs(lx) > 0.2 then player.x = player.x + lx * speed * dt end
end
Audio
function love.load()
-- Sound effects (decode on load, low latency)
jumpSound = love.audio.newSource("sounds/jump.wav", "static")
hitSound = love.audio.newSource("sounds/hit.wav", "static")
-- Music (stream from disk, saves memory)
music = love.audio.newSource("music/background.ogg", "stream")
music:setLooping(true)
music:setVolume(0.5)
music:play()
end
function playSound(sound)
sound:stop() -- Reset if already playing
sound:play()
end
-- Or clone for overlapping sounds
function playSFX(source)
local clone = source:clone()
clone:play()
end
Physics (Box2D)
function love.load()
world = love.physics.newWorld(0, 9.81 * 64, true) -- Gravity
-- Ground (static body)
ground = {}
ground.body = love.physics.newBody(world, 400, 580, "static")
ground.shape = love.physics.newRectangleShape(800, 40)
ground.fixture = love.physics.newFixture(ground.body, ground.shape)
-- Player (dynamic body)
ball = {}
ball.body = love.physics.newBody(world, 400, 100, "dynamic")
ball.shape = love.physics.newCircleShape(20)
ball.fixture = love.physics.newFixture(ball.body, ball.shape, 1)
ball.fixture:setRestitution(0.5) -- Bounciness
ball.fixture:setFriction(0.3)
-- Collision callbacks
world:setCallbacks(beginContact, endContact, preSolve, postSolve)
end
function beginContact(a, b, contact)
local userDataA = a:getUserData()
local userDataB = b:getUserData()
-- Handle collision
end
function love.update(dt)
world:update(dt)
end
function love.draw()
love.graphics.circle("fill", ball.body:getX(), ball.body:getY(), ball.shape:getRadius())
love.graphics.rectangle("fill",
ground.body:getX() - 400, ground.body:getY() - 20, 800, 40)
end
Camera System
-- Simple camera
local camera = { x = 0, y = 0, scale = 1, rotation = 0 }
function love.draw()
love.graphics.push()
love.graphics.translate(-camera.x + love.graphics.getWidth()/2,
-camera.y + love.graphics.getHeight()/2)
love.graphics.scale(camera.scale)
love.graphics.rotate(camera.rotation)
-- Draw world objects here
drawWorld()
love.graphics.pop()
-- Draw UI (not affected by camera)
drawUI()
end
function love.update(dt)
-- Follow player with lerp
local lerpSpeed = 5 * dt
camera.x = camera.x + (player.x - camera.x) * lerpSpeed
camera.y = camera.y + (player.y - camera.y) * lerpSpeed
end
-- Screen shake
local shake = { duration = 0, magnitude = 0 }
function screenShake(duration, magnitude)
shake.duration = duration
shake.magnitude = magnitude
end
function love.update(dt)
if shake.duration > 0 then
shake.duration = shake.duration - dt
camera.x = camera.x + love.math.random(-shake.magnitude, shake.magnitude)
camera.y = camera.y + love.math.random(-shake.magnitude, shake.magnitude)
end
end
Animation System
local Animation = {}
Animation.__index = Animation
function Animation.new(image, frameWidth, frameHeight, duration)
local anim = setmetatable({}, Animation)
anim.image = image
anim.frames = {}
anim.duration = duration or 0.1
anim.currentTime = 0
anim.currentFrame = 1
local imgW = image:getWidth()
local imgH = image:getHeight()
for y = 0, imgH - frameHeight, frameHeight do
for x = 0, imgW - frameWidth, frameWidth do
table.insert(anim.frames, love.graphics.newQuad(
x, y, frameWidth, frameHeight, imgW, imgH
))
end
end
return anim
end
function Animation:update(dt)
self.currentTime = self.currentTime + dt
if self.currentTime >= self.duration then
self.currentTime = self.currentTime - self.duration
self.currentFrame = self.currentFrame % #self.frames + 1
end
end
function Animation:draw(x, y, r, sx, sy)
love.graphics.draw(self.image, self.frames[self.currentFrame], x, y, r or 0, sx or 1, sy or 1)
end
-- Usage
function love.load()
local sheet = love.graphics.newImage("player_walk.png")
walkAnim = Animation.new(sheet, 32, 32, 0.1)
end
function love.update(dt)
walkAnim:update(dt)
end
function love.draw()
walkAnim:draw(player.x, player.y)
end
Advanced Usage
-- Shaders (GLSL)
local shader = love.graphics.newShader([[
extern number time;
vec4 effect(vec4 color, Image texture, vec2 texture_coords, vec2 screen_coords) {
vec4 pixel = Texel(texture, texture_coords);
pixel.r = pixel.r + sin(time + screen_coords.x * 0.01) * 0.3;
return pixel * color;
}
]])
function love.draw()
shader:send("time", love.timer.getTime())
love.graphics.setShader(shader)
love.graphics.draw(image, 0, 0)
love.graphics.setShader() -- Reset
end
-- Canvas (render to texture)
local canvas = love.graphics.newCanvas(800, 600)
love.graphics.setCanvas(canvas)
love.graphics.clear()
-- Draw to canvas
love.graphics.setCanvas()
-- Draw canvas to screen
love.graphics.draw(canvas, 0, 0)
-- Threads for heavy computation
local thread = love.thread.newThread("worker.lua")
local channel = love.thread.getChannel("results")
thread:start()
local result = channel:pop() -- Non-blocking
local result = channel:demand() -- Blocking
-- Save/Load data
local saveData = { score = 100, level = 3 }
local serialized = "return " .. serialize(saveData)
love.filesystem.write("save.dat", serialized)
local data = love.filesystem.load("save.dat")()
Building and Distribution
# Create .love file (zip renamed)
cd mygame
zip -r ../mygame.love .
# Windows executable
cat love.exe mygame.love > mygame_win.exe
# Then distribute with love DLLs
# macOS app bundle
cp -r love.app mygame.app
cp mygame.love mygame.app/Contents/Resources/
# Use love-release tool for automated builds
luarocks install love-release
love-release -W -M -L # Build for Windows, macOS, Linux
# Android (via love-android)
git clone https://github.com/love2d/love-android.git
# Place game in app/src/embed/assets/
./gradlew assembleRelease
Troubleshooting
| Issue | Solution |
|---|---|
| ”Module not found” | Check file paths; use require("folder.module") with dots |
| Blurry pixel art | Set filter to nearest: image:setFilter("nearest", "nearest") |
| Game runs too fast/slow | Always multiply movement by dt (delta time) |
| Physics bodies fly apart | Scale down; use pixels-per-meter (64 pixels = 1 meter) |
| Audio crackling | Use “static” source type for short sounds; check buffer sizes |
| Out of memory | Reuse objects; call collectgarbage() periodically |
| No gamepad input | Check love.joystick.getJoysticks(); verify gamepad mapping |
| Window not resizing | Set t.window.resizable = true in conf.lua |
| Drawing order wrong | LOVE draws in order of calls; draw background first |
| Save file not found | Use love.filesystem.getSaveDirectory() to check location |