Learning

Lua + LÖVE: Make a Game · Lesson 03 · 45 min

Build with functions, tables, and loops

Group behavior into functions and model collections of game objects with Lua tables.

Learn

Functions, table fields, arrays, and loops.

Build

A tiny signal-collection simulation.

Prove it

Remove expired objects safely.

Lua has one general-purpose data structure: the table. A table can act like a list, a dictionary, a record, an object, or some mixture of them. Combined with functions and loops, it is enough to model most of a small game.

Functions turn a recipe into a tool

Define a function with parameters, then return a result:

local function distance(x1, y1, x2, y2)
  local dx = x2 - x1
  local dy = y2 - y1
  return math.sqrt(dx * dx + dy * dy)
end

local gap = distance(10, 20, 13, 24)
print(gap) -- 5

Parameters and variables declared inside the function are local to that call. A function stops at return and can return more than one value:

local function direction(fromX, fromY, toX, toY)
  return toX - fromX, toY - fromY
end

local dx, dy = direction(10, 20, 16, 17)
print(dx, dy) -- 6  -3

Functions are values in Lua. This syntax:

local function greet(name)
  return "Hello, " .. name
end

is convenient shorthand for assigning an anonymous function:

local greet = function(name)
  return "Hello, " .. name
end

Tables as records

Group related player values in one table:

local player = {
  x = 480,
  y = 270,
  radius = 14,
  speed = 260,
  lives = 3,
}

Read and change named fields with dot syntax:

print(player.x)             -- 480
player.x = player.x + 20
player.lives = player.lives - 1

player.x is shorthand for player["x"]. Bracket syntax lets you use a field name stored in a variable:

local field = "lives"
print(player[field]) -- 2

A constructor function makes fresh tables with the same shape:

local function newSignal(x, y)
  return {
    x = x,
    y = y,
    radius = 8,
    life = 5,
  }
end

local first = newSignal(120, 90)
local second = newSignal(640, 320)

first and second refer to different tables. Changing first.life does not change second.life.

Tables as arrays

An array-style table stores an ordered collection:

local signals = {
  newSignal(120, 90),
  newSignal(640, 320),
}

Lua arrays conventionally start at 1, not 0:

print(signals[1].x) -- 120
print(#signals)     -- 2, the array length

Add and remove values with the table library:

table.insert(signals, newSignal(300, 200))
table.remove(signals, 2)

Assigning nil to a field removes that field. Avoid leaving holes in the middle of array-style tables; # and ipairs are designed for a contiguous sequence.

Repeat with loops

A numeric for loop knows its index:

for index = 1, 5 do
  print(index)
end

Use ipairs when order matters and the table is an array:

for index, signal in ipairs(signals) do
  print(index, signal.x, signal.y)
end

Use pairs for named fields where order is not meaningful:

for key, value in pairs(player) do
  print(key, value)
end

A while loop repeats as long as its condition remains true:

local countdown = 3
while countdown > 0 do
  print(countdown)
  countdown = countdown - 1
end

Make sure a while loop changes something relevant to its condition, or it can run forever.

Update a collection

Game objects often have a remaining lifetime. An update function can subtract elapsed time from each one:

local function updateSignals(signals, dt)
  for _, signal in ipairs(signals) do
    signal.life = signal.life - dt
  end
end

The underscore is an ordinary variable name used by convention to mean “I will not use this value.” Here we ignore the index.

Removing items while walking forward through an array is dangerous. Removal shifts later items left, so the loop can skip one:

-- Fragile: a removal changes the indexes still ahead of us.
for index, signal in ipairs(signals) do
  if signal.life <= 0 then
    table.remove(signals, index)
  end
end

Walk backward when removal is possible:

local function removeExpired(signals)
  for index = #signals, 1, -1 do
    if signals[index].life <= 0 then
      table.remove(signals, index)
    end
  end
end

Removing index 4 cannot change indexes 1 through 3, so the unvisited part stays stable.

The collection pattern

Small LÖVE games use this constantly: store similar entities in an array, insert new tables when they spawn, update or draw every item in a loop, and loop backward when deleting.

Build a tiny simulation

This program uses no LÖVE features, so it can run with lua practice.lua:

local function newSignal(name, life)
  return {name = name, life = life}
end

local signals = {
  newSignal("alpha", 1.0),
  newSignal("beta", 2.5),
  newSignal("gamma", 0.5),
}

local function step(seconds)
  print("Advancing " .. seconds .. " seconds")

  for index = #signals, 1, -1 do
    local signal = signals[index]
    signal.life = signal.life - seconds

    if signal.life <= 0 then
      print("  expired: " .. signal.name)
      table.remove(signals, index)
    else
      print("  active: " .. signal.name .. " (" .. signal.life .. ")")
    end
  end
end

step(0.75)
step(0.75)
print("Signals left: " .. #signals)

Predict which signals remain after each step, then run it.

Tables are references

Variables hold references to tables rather than copying their contents:

local a = {score = 0}
local b = a
b.score = 10
print(a.score) -- 10

Both names point to the same table. This is useful when a function receives a player and changes it:

local function heal(entity)
  entity.lives = entity.lives + 1
end

heal(player)

The function does not need to return the table because it mutates the shared object. Mutation is convenient, but make it obvious through names and structure.

Challenge: cap the collection

Change the simulation so a spawnSignal(name, life) function inserts a signal only when fewer than five are active. Return true when it spawns and false when the collection is full.

One solution
local function spawnSignal(name, life)
  if #signals >= 5 then
    return false
  end

  table.insert(signals, newSignal(name, life))
  return true
end

Checkpoint

You are ready when you can create a table with named fields, add it to an array, loop over the array, and explain why removal loops usually run backward.

Keep the Lua quick reference nearby. In the next lesson, these language pieces become a real-time scene.