4. Tables

A table holds several values together. It is the only data structure Lua has, and it does two jobs: a list of things in order, and a record of named things.

A list

Write the values between curly brackets:

local inventory = { "sword", "shield", "potion" }

print(inventory[1])    --> sword
print(inventory[3])    --> potion
print(#inventory)      --> 3

The number in square brackets is the position, and # gives how many there are.

Lists start at 1 in Lua. The first item is inventory[1]. In most other languages it would be 0, and this is the mistake people from those languages make most.

A position that has nothing is nil:

local inventory = { "sword", "shield", "potion" }
print(inventory[4])    --> nil

Changing a list

local inventory = { "sword", "shield" }

table.insert(inventory, "potion")       -- add at the end
inventory[1] = "axe"                    -- replace the first
table.remove(inventory, 2)              -- take out the second; the rest move up

print(inventory[1], inventory[2])       --> axe   potion
print(#inventory)                       --> 2

Going through a list

local enemies = { "slime", "bat", "ghost" }

for index, name in ipairs(enemies) do
	print(index .. ": " .. name)
end
--> 1: slime
--> 2: bat
--> 3: ghost

ipairs gives each position and its value, in order. When you do not need the position, the habit is to call it _:

local scores = { 40, 25, 35 }
local total = 0

for _, score in ipairs(scores) do
	total = total + score
end
print(total)    --> 100

A record

A table can also hold values under names:

local player = {
	name = "Mira",
	health = 100,
	level = 3,
}

print(player.name)      --> Mira
print(player.health)    --> 100

Read and change a named value with a dot. Assigning to a name that is not there yet adds it:

local player = { name = "Mira", health = 100 }

player.health = player.health - 30
player.gold = 50

print(player.health, player.gold)    --> 70   50

A name that is not there reads as nil. That is not an error, which is handy and also hides typing mistakes: player.helth is quietly nil.

Going through a record

pairs gives every name and its value:

local prices = { sword = 120, shield = 80, potion = 15 }

for item, price in pairs(prices) do
	print(item .. " costs " .. price)
end

The order pairs goes in is not fixed. Use a list and ipairs when order matters.

Names in square brackets

player.name is short for player["name"]. The long form is for when the name is in a variable:

local prices = { sword = 120, shield = 80 }
local wanted = "shield"

print(prices[wanted])    --> 80

That makes a table a lookup: give it a name and get the value back. Removing an entry is assigning nil to it.

Tables inside tables

A value in a table can be another table. A list of records is the shape most game data has:

local waves = {
	{ enemy = "slime", count = 5 },
	{ enemy = "bat",   count = 8 },
	{ enemy = "ghost", count = 2 },
}

for number, wave in ipairs(waves) do
	print("Wave " .. number .. ": " .. wave.count .. " x " .. wave.enemy)
end
--> Wave 1: 5 x slime
--> Wave 2: 8 x bat
--> Wave 3: 2 x ghost

print(waves[2].enemy)    --> bat

A variable holds the table, not a copy

This surprises everyone once:

local a = { health = 100 }
local b = a

b.health = 50
print(a.health)    --> 50

b = a does not copy the table. Both names now refer to the same one, so a change through either shows through the other.

The same goes for passing a table to a function: the function gets the same table and can change it. Numbers, strings and booleans are copied. Tables are shared.

For the same reason, two tables are equal only when they are the same table:

print({ 1, 2 } == { 1, 2 })    --> false: two different tables

In a game

Functions that find several things return a list of them:

-- file: example
for _, coin in ipairs(Scene.FindByTag("Coin")) do
	coin:Destroy()
end

print(#Scene.FindByTag("Enemy") .. " enemies left")

A script's Fields is a record, and so is the script itself:

-- file: example
Player.Fields = {
	WalkSpeed = 4.5,
	RunSpeed = 8,
}

A saved game is a table too. SaveData.Save("Slot1", { Level = 3, Gold = 250 }) writes it and SaveData.Load("Slot1") gives it back.

One thing differs: a Vec3 is not a table. Reading entity.Position gives you a copy, so changing the copy does not move the entity. Assign it back:

-- file: example
local position = self.Entity.Position
position.y = position.y + 1
self.Entity.Position = position

Try it

  1. Make a list of five scores. Go through it and print the highest.
  2. Make a record for a weapon with a name, a damage and a range. Write a function that takes a weapon and prints a line describing it.
  3. Make a list of three weapons like that, and print the name of the one with the most damage.

Next: objects and scripts.