Lua + LÖVE: Make a Game · Reference
Debugging field guide
A symptom-first guide for finding Lua and LÖVE bugs with evidence instead of guesswork.
Use the branch matching what you can observe. Change one thing, rerun, and keep evidence from each check.
LÖVE shows an error screen
- Find the first stack-trace line naming one of your files.
- Open that line and the few lines above it.
- Translate the error category below.
| Message pattern | Check |
|---|---|
syntax error near ... |
unmatched quote/parenthesis, missing comma, wrong operator |
'end' expected |
unclosed if, loop, or function above the reported line |
unexpected symbol near ... |
an extra token or syntax borrowed from another language |
attempt to index ... nil |
missing table, wrong field, constructor not called, wrong update order |
attempt to call ... nil |
misspelled function, module did not export it, dot/colon mismatch |
attempt to perform arithmetic ... nil |
uninitialized number or misspelled numeric field |
bad argument #n |
wrong value type or argument position in an API call |
module ... not found |
require name, capitalization, or folder path mismatch |
Print the suspicious value and its type immediately before the failing line:
print("player", player, type(player))
print("player.x", player and player.x, type(player and player.x))
The and expression safely avoids indexing a missing player during the print itself.
The window opens but is blank
- Confirm the callback is named exactly
love.draw. - Set a loud background color in
love.loadto prove the correct project runs. - Draw fixed test text first:
love.graphics.print("draw runs", 20, 20). - Reset color to white and full alpha.
- Print object coordinates; check for
nan, huge values, or off-screen positions. - Check drawing order: an opaque full-screen rectangle drawn last hides everything.
- Balance every
love.graphics.push()withlove.graphics.pop().
Bisect drawing:
function love.draw()
drawBackdrop()
drawWorld()
-- drawEffects()
-- drawUI()
end
Restore one section at a time until the blank screen returns.
An object does not move
Instrument the full chain:
print(
"right", love.keyboard.isDown("right"),
"dt", dt,
"x", player.x,
"speed", player.speed
)
Check:
- Is
love.updaterunning? - Does the key name match LÖVE’s name?
- Is speed nonzero?
- Is
dtincluded exactly once? - Does another line reset position later in the frame?
- Is draw reading the same player table update modifies?
Movement speed changes with frame rate
Every rate should be multiplied by dt:
position = position + pixelsPerSecond * dt
angle = angle + radiansPerSecond * dt
timer = timer - dt
Do not multiply one-time quantities by dt: score awards, life loss, teleport distance, and array indexes.
Diagonal movement should normalize direction before multiplying by speed.
A collection skips or duplicates objects
- Use a backward numeric loop when removal is possible.
- Use
if ... elseif, or return/continue structurally, to avoid removing one index twice. - Do not insert into the same array while iterating it unless you have designed for that.
- Print index and length around mutation:
print("remove", index, "length before", #meteors)
table.remove(meteors, index)
print("length after", #meteors)
Collision feels wrong
Draw the exact shapes used by collision:
love.graphics.setColor(0.2, 1, 0.4)
love.graphics.circle("line", player.x, player.y, player.radius)
Then check:
- both objects use the same coordinate space;
- radius is not being mistaken for diameter;
- image draw origin matches collision center;
- collision happens after positions update;
- invulnerability is counted down and checked in seconds;
- the collided object is removed or separated.
Restart contains old objects or values
List every value owned by one run:
score, lives, player, entity arrays, spawn timers,
invulnerability, particles, shake, elapsed difficulty time
Initialize all of them in one resetGame function. Keep application-lifetime resources—fonts, images, sounds, backdrop, loaded save data—outside it.
Sound does not play
- Create the Source once and verify it is not nil.
- Start with
source:stop(); source:play()for repeatable short effects. - Check source and master volume.
- Trigger it directly from a debug key to separate audio from collision logic.
- For generated SoundData, keep samples in
-1through1. - Do not create Sources every frame.
The source folder runs but the .love file does not
Inspect the archive root. It must contain:
main.lua
conf.lua
game.lua
...
It must not contain a wrapping project folder:
orbit-runner/main.lua ← wrong
Also check capitalization. A require path that works on a case-insensitive filesystem may fail on a case-sensitive one.
A disciplined five-minute loop
- State one concrete expectation: “lives changes from 3 to 2 once.”
- Identify the smallest code path responsible.
- Observe the inputs and outputs of that path.
- Change one cause, not several symptoms.
- Reproduce the original case and one neighboring case.
If you cannot say what evidence would prove a theory wrong, the theory is too vague. Narrow it before editing.