Learning

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

  1. Find the first stack-trace line naming one of your files.
  2. Open that line and the few lines above it.
  3. 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

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:

  1. Is love.update running?
  2. Does the key name match LÖVE’s name?
  3. Is speed nonzero?
  4. Is dt included exactly once?
  5. Does another line reset position later in the frame?
  6. 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

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:

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

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

  1. State one concrete expectation: “lives changes from 3 to 2 once.”
  2. Identify the smallest code path responsible.
  3. Observe the inputs and outputs of that path.
  4. Change one cause, not several symptoms.
  5. 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.