Skip to content
Published August 30, 2021

Today, we’ll be looking at a more advanced programming concepts: data structures. Intelligent usage of data structures will help to drastically cut down on your program’s size as well as limit the number of bugs in your project. For Pico-8 in particular, keeping your file size small is imperative since you have hard limits to work with.

Tables

In Lua, tables are the only data structure that can act as a container, storing multiple pieces of data. Because of this, you can use Lua tables to build all sorts of data structures, such as arrays, objects, associative arrays, linked lists, trees, etc. Unfortunately, this means you need to be careful, since Lua won’t categorize these different datatypes for you; you must keep track of them yourself. This is one of the biggest pros/cons of Lua in my opinion; it lets you create all sorts of things easily, but the developer needs to pay attention to what they create since Lua doesn’t check if your program syntax makes sense until you run it.

Let’s start out with arrays, a commonly used and simple to understand data structure.

Arrays

Arrays are a simple data structure; all they are is a contiguous storage of data or memory. Here is a basic example.

Let’s say that you want to store the health of five different enemies. One solution is to create a variable for each enemy, e.g. enemy1health, enemy2health, enemy3health, etc. Afterwards, you can set their health values. Here’s how it could look:

enemy1health = 3
enemy2health = 3
enemy3health = 3
enemy4health = 3
enemy5health = 3

While there’s nothing to prevent you from writing the data out this way, you may realize that this will quickly become tedious. Not to mention, if you need to build, say, 100 enemies at the same time, copying and pasting this code over and over will become infeasible before long.

The solution is to use an array. First, declare your array as a table.

enemy_health = {}

In Lua, tables are defined using curly braces {}. If you don’t put anything in between the braces, the table starts out empty.

Now we need to populate the array. Pico-8’s function for adding onto an array is a little different than pure Lua. Most of Lua’s functions work the same in Pico-8, but there are some exceptions.

In Pico-8, use the add( tbl, v ) function. tbl is the name of your table (array), and v is the value you want to add. v can be any value, including numbers, strings, booleans, and even other tables and functions.

add(enemy_health, 3)

This will add the value 3 to the enemy_health array. Technically, the value will be added to the end of your array, but since the array was empty, 3 becomes the only value in the array.

To access the value later, use the [x] syntax where x is the index of the array that you want to access.

enemy_health[1] -- value is 3
enemy_health[2] -- value is nil since we haven't stored a value here yet

Lua uses a 1-based array system, so the first value in an array is accessed as array[1]. Most other languages use a 0-based array system, where array[0] is the first value in the array. I’m not sure why the creator of Lua chose something different, so keep this in mind if you’ve ever coded in other languages (or switch to another programming language after Lua).

Here is an alternative way to write an array if you already know what you want to populate it with. Using the previous example of enemy health values, you write out however many values you want as a comma separate list:

enemy_health = {3, 9, 12, 8, 2, 25}

You can delete elements from an array using one of two functions. Function one is del(array, val), where you delete an element by the first instance of val:

numbers = {1, 8, 5, -6, 8}
del(numbers,8) -- the array now reads: {1, 5, -6, 8}

You can also delete an element based on where in the array it is located using deli(array, [i]) to delete the ith element. Example:

numbers = {1, 8, 5, -6, 8} -- 5 is the 3rd element
del(numbers,3) -- the array now reads: {1, 8, -6, 8}

An array always has the whole numbers (indices) to reference the array’s values in numerical order. So if your array foo has 4 contiguous elements, the array values are foo[1], foo[2], foo[3], foo[4]. If you want to loop through your array and you don’t care about each value’s index, you can use all(array) in a for loop like so:

for v in all(array) do:
    -- do something with each value v
end

The all(array) syntax works for numerical arrays but not key-value arrays (as we’ll see in a bit). When you use all(array), you traverse the array in order of the elements every time.

If you need the indices, you can use a more traditional for loop:

for i=1,#array do:
    -- i is the index in this case, so you can use
    -- array[i]
end

#array gives you the array length. Note that #array will only work with standard arrays, and not with the associative arrays that I will be going over in the next section.

Associative Array

An associative array is an array that uses keys to access values instead of numbers. Basically, this means that you reference array values with strings instead of numbers. For example, here is a hypothetical list of enemies and how many of them are in the game currently:

enemies = {"minotaur": 5, "cockatrice": 2}

Since an associative array uses keys (strings) to access values, it is sometimes known as key-value pairs. One thing to note is that the keys for your values are not guaranteed in any order, unlike arrays with strict numbering. The table will not auto-sort your keys for you in, say, alphanumeric order. This means it’s a little trickier to go through all the elements of your array, since you can’t reference each data cell by number.

You can access an element in a key-value table in the same way as a standard array, using the braces format. Make sure to use quotes around the key since it is a string:

enemies["minotaur"] -- value is 5

There is another way to access elements specifically for keys: instead of using quotes or braces, simply use dot notation:

 enemies.minotaur -- value is 5 

An associative array goes by other names too, such as a dictionary, map or symbol table. The Python language for example uses the term dictionary.

A Word of Caution

Lua does not readily distinguish between normal arrays and associative arrays until you actually run your code, the reason being that the table is the only data structure. In Pico-8, this means when your start your game, the Lua interpreter will check your tables and determine whether they are arrays or associative arrays. The way it does this is it figures out whether the keys to your table are whole numbers in order starting with one. As in, “1”, “2”, “3”, etc. If there are any gaps in the table, like if the value at key “2” is nil and key “3” has some other value, Lua won’t treat it like a standard array but instead like a key-value table.

Technically arrays are a special case of associative arrays in Lua and not their own unique data type. In Lua, all arrays are associative arrays, but not all associative arrays are arrays, similar to how all squares are rectangles but not all rectangles are squares. Under the hood, all tables are considered associative arrays. It’s just that Lua allows for some code shorthand for the “normal” arrays.

Because Lua doesn’t check your code until you run it, it is very easy to accidentally add a key-value pair to a table that you intend to be a standard array. Let’s take a look at an example of what I mean. Assume that you have defined a function that creates a new enemy randomly on the screen called create_enemy()

-- declare a new table called enemies
enemies = {}

-- create 5 new enemies randomly on-screen using the create_enemy() function
for i=1,5 do
    add(enemies, create_enemy())
end

So far so good. We now have a table enemies that has 5 enemies in it. So it would look like:

{enemy1, enemy2, enemy3, enemy4, enemy5}

in the table. Enemy 1 is at position 1 of the array, enemy 2 is at position 2 of the array, etc.

When looking at a standard array, there is another way to look at the array, that being an associative array with numerical keys like so:

{[1] = enemy1,  [2] = enemy2,  [3] = enemy3,  [4] = enemy4,  [5] = enemy5} 

As long as the table starts with the first key being number one and the following keys increase in value by one per key, this works. However, let’s say now that you accidentally add a sixth value in, but the key is not “6”. Rather, the key is the string value “six”:

 enemies["six"] = create_enemy()

Now the table is no longer an array since a non-standard key value was added to it. It has instead become a key-value table.

Why does this matter?

There are a couple issues here. One is that you can’t iterate over the table using numbers as the index anymore. So the numerical for loops

for i=1,#array do:
    -- do something
end 

and

for i=1,all(array) do:
    -- do something
end 

won’t work anymore, since keys other than numbers in order starting with “1” have been added to the key-value table. This means any code that you use later that iterates over the array in this fashion will break.

This for loop iteration works with key-value tables, which means any table will work with it:

for k,v in pairs(array) do:
    -- do something
end

The first variable (in this case “k”) lets you access the key in the loop, while the second variable (in this case “v”) gives you the value in the loop. So in some instances, you may want the key and/or the value.

There is another for loop for tables, but it’s not something I use much personally. Your mileage may vary, however:

foreach(array, func)

The foreach function iterates over a table like pairs, but it calls a function to act on each value in the table. For example, if func = print, you could print each element of the table.

The Other Problem

The other issue is that an associative array does not have an order to it. Unlike standard arrays that are in order from 1 to some bigger whole number, key-value tables do not have a guaranteed order for their indices. This is an issue if you want a table to be sorted for some reason, e.g. if you want all your in-game sprites to be placed into a table and drawn in a specific order.

Sometimes you will care about the order, and other times you won’t. It’s up to you to decide when order is important, and when it’s irrelevant.

For Further Reference

For additonal reading on Lua tables, check out http://lua-users.org/wiki/TablesTutorial

And the pico-8 manual has more specific details about pico-8 tables: https://www.lexaloffle.com/dl/docs/pico-8_manual.html

Next time…

I’ve been mentioning functions for a bit in the past few tutorials. Next time, I will cover more about how functions work and how to write your own custom functions that will save you time and headache ( I was originally planning to cover them in this tutorial, but then the table tutorial ended up being longer than I anticipated -_- )