luaForGameDevelopment
luaForGameDevelopment
Chapter 4: Variables 19
Additional Details (Variables) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
Case Sensitivity . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
Error Messages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
Naming Conventions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
Chapter 6: Operators 25
Arithmetic Operators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
Relational Operators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
Logical Operators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
AND Operator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
OR Operator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
NOT Operator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
Miscellaneous Operators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
Additional Details (Operators) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
Whitespace . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
Redundant Boolean Checks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
Valid Logical Operator Arguments . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
Operator Errors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
Assignment Operators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
Exercises: Operators 34
Exercise 1: Basic Arithmetic Operations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
Exercise 2: Comparing Numbers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
Exercise 3: Logical Operators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
Optional Operators Exercise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
(Solution) Exercise 1: . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
(Solution) Exercise 2: . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
2
(Solution) Exercise 3: . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
Chapter 7: Conditionals 36
Additional Details (Conditionals) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
Indent Size (Tabs vs Spaces) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
Conditional Errors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
Exercises: Conditionals 41
Exercise 1: Door Unlock Condition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
Exercise 2: Game Character Health Check . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
Exercise 3: Finding the Largest Number . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
Optional Conditionals Exercise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
(Solution) Exercise 1: . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
(Solution) Exercise 2: . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
(Solution) Exercise 3: . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
Chapter 8: Functions 44
Function Syntactic Sugar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
Returning Values . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
Additional Details (Functions) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
Function Indentation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
Code Flow with Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
Empty Function Return . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
Extra Function Properties . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
Function Errors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
Chapter 9: Scope 58
Additional Details (Scope) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
Local Function Parameters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
Local vs Global Performance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
File Scope . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
Global Variable Exceptions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
Local Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
Nested Scope . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
Multiple Assignment . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
Scope Errors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
Exercises: Functions 66
Exercise 1: Heal Player . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
Exercise 2: Heal Player (function return) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
Exercise 3: Weapon Stats (multiple return) . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
Optional Functions Exercise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
(Solution) Exercise 1: . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
(Solution) Exercise 2: . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
(Solution) Exercise 3: . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
3
Length of an Array . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
Table Errors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
Exercises: Tables 78
Exercise 1: Create and Access a Table (Array) . . . . . . . . . . . . . . . . . . . . . . . . . . 78
Exercise 2: Create and Access a Table (Dictionary) . . . . . . . . . . . . . . . . . . . . . . . . 78
Exercise 3: Check If a Value Exists . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
Optional Tables Exercise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
(Solution) Exercise 1: . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
(Solution) Exercise 2: . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
(Solution) Exercise 3: . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
Exercises: Loops 93
Exercise 1: Traverse Dictionary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
Exercise 2: Find the Largest Number . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
Exercise 3: Combine Tables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
Optional Loops Exercise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
(Solution) Exercise 1: . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
(Solution) Exercise 2: . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
(Solution) Exercise 3: . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
Conclusion 122
4
(Solution) Functions Exercise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130
Tables Exercise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132
(Solution) Tables Exercise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133
Loops Exercise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 135
(Solution) Loops Exercise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137
5
What This Book Is and Why I Wrote It
Hi! My name is Atsu, also known as SquidGod. At the start of 2022, when a small handheld console
called the Playdate came out, I began making some videos on Youtube to teach others how to make
games on the device, which uses Lua. As I was making tutorials, I started getting comments asking for
suggestions on resources to learn Lua. There were plenty of things for experienced programmers, but
for beginners, I looked around and didn’t find many great resources on the topic. The biggest issue was
that a lot of books and resources were aimed at those who already had programming knowledge and just
needed to learn how Lua differs from other programming languages. It didn’t seem like there were many
resources for complete beginners. The way I think about it, if you consider 10 to be a fairly competent
game developer, most tutorials start at level 2 or occasionally down to level 1, even though many people
are at level 0. Despite there being a wealth of educational programming resources out there, most
people are left on the sidelines because the initial learning curve is so steep and undocumented.
In response to what I found, I made a two-part video series to teach Lua from the basics, in an attempt
to get those who watched it to this “level 2” of understanding, so that they could start following along
with my tutorials, as well as other resources. However, as I was making the videos, there were a lot of
small intricacies that I wanted to add that didn’t quite fit in a video format. The result of that is this
book, where I go over all those things I wanted to add and more.
This book is written in a way where I assume you have no knowledge of programming. I start from
the absolute basics, like what a programming language or computer even is, and sequentially build
upon each previous topic until the final chapter, where you should have a good enough understanding
of the language to start following along with tutorials and resources that expect you to know how to
program - at least without being completely lost. While I don’t assume any prior knowledge and write
in consideration of that (e.g. explaining every single new term), I do frequently start immediately using
concepts that were just taught, sort of like a college course. It’s expected that there are things that
won’t click immediately, so don’t be discouraged by that! Feel free to go back, reread sections, and test
out the concepts yourself to cement your understanding.
To help, I have a few interactive exercises throughout the book that you can try solving to test your
knowledge. Also, if you have any questions, don’t hesitate to reach out - my contact information is at
the end of the book. I love answering questions! That’s why I wrote this book and started my Youtube
channel :)
The readers I had in mind when I wrote this book are those who are trying to get into game development
specifically for the Playdate, since that’s my main audience, but for the most part, the basics apply to
other game development environments that use Lua, whether that’s using Love2D, Roblox, PICO-8,
Defold, or anything else. If there is anything Playdate development-specific, I’ll be sure to point it out.
I’d like to take this chance to really clarify - this book is not going to teach you how to make a game.
My hope is that it will help lay a good foundation for learning game development, so that in any
videos you watch, courses you take, blog posts you read, or tutorials you follow, you can just focus
on the game development side, as opposed to having to go through the frustrating and overwhelming
experience of learning programming at the same time.
While the focus is on the Lua programming language, I also cover a lot of general topics related to
writing code in general, like tips for organizing code and best practices. These come from my own
experiences from making several games in Lua and also my full-time job as a software engineer. In
many cases, the use case for these tips and nuggets of knowledge may go over your head since you
probably wouldn’t have gotten far enough along to bump into the relevant situations. However, my
hope is that you can use this book as a resource to come back to, reread, and get some new insights
you didn’t get before as you gain more and more experience. Alright - let’s jump into it!
6
Chapter 1: What is Lua?
Before we get into anything about how to program, how to write code, or even what Lua is - let me ask
you this question: what even is programming? Maybe you know vaguely what it is, but how does it
work? Many people have been able to learn how to program without knowing any of the mechanics
behind how code works, but I think if we can have even a cursory knowledge of what’s happening
behind the scenes, it’ll help lay a good, foundational level of understanding on why things are the way
they are in programming.
What is a Computer?
Let’s begin our exploration starting from the very basics - what do programs run on? That might be
obvious - they run on computers. But, what even is a computer? In our modern day, the computer
seems like a magical box that can do anything, but, at its core, it’s actually much simpler than you
think. In 1936, Alan Turing invented the Turing machine - a theoretical idea of a machine that can
compute anything we would imagine a modern computer can (he also helped crack the German ciphers
in WW2 - see the dramatic interpretation in The Imitation Game).
The interesting thing is that the “Turing Machine” is very simple. It has a tape of paper split up into
cells. At any point, the machine has a “head” that’s pointed at one of these cells and can read or
write a number into one of these cells. The machine also has its own “state”, which is just another
number. Every step of the machine’s operation, the number in the current cell is read, and based on a
combination of that number, the machine state, and a table of rules, the machine can either 1. write a
new number into that cell 2. move one cell to the left or right or 3. halt its own execution. An example
of a rule is something simple like “If the cell has a value of 1 and the machine’s state is 42 , move one
cell to the right”.
Despite the relative simplicity of the idea, the theory behind it is that everything that a modern-day
computer does can somehow be boiled down to this basic system, in a somewhat similar way to how
all the intricacies and complexity of a human brain can somehow be boiled down to much simpler
interactions between neurons.
We can think of these cells as “memory”, as they can store information. Before the machine starts
7
operating, you can pre-fill some of the cells with numbers, and that would be your program. And lastly,
the set of rules is the programming language.
Every piece of software you’ve interacted with is basically just a series of instructions to read and write
data to memory and simple operations on that data. Displaying an image on a screen is just writing
data to cells that correspond to a specific pixel. Advanced games can be boiled down to just bits of data
(health, damage, score, velocity, etc) and how that data is displayed. The complexity of programming
really comes from the combination of simple rules and bits of data, like DNA or Conway’s Game of Life.
Above is a diagram of a simple CPU. The black arrow labeled “instructions” from the block “main
memory” to the block “control unit” is like the Turing Machine reading numbers from the tape. An
instruction is just a number, and based on the rules of the computer, can mean something like adding
two numbers together, or storing a number in memory to retrieve later. The instructions of your
program are stored in memory, and the CPU goes line by line, memory cell by memory cell, executing
each instruction, like the head of the Turing Machine going cell by cell.
Every part of the CPU is connected to a central clock that sends a signal to advance its internal electrical
components. This drives the operation of the computer. Like how each step of the Turing Machine
consists of reading the cell and then performing some operation, each clock signal also corresponds to
reading an instruction from memory and performing some operation. However, this happens extremely
quickly. If you’ve wondered what the x GHz processing speed on your computer means, that is directly
related to how fast that clock sends out its signal. For example, 2 GHz equals 2,000,000,000 cycles per
second. Theoretically, that means up to 2 billion instructions can be handled per second. Insane, right?
Of course, the actual number of instructions processed per second depends on a multitude of factors,
but that should give you some sense of the speed and scale of these machines.
Speaking of electrical components, the diagram is just an abstraction of what a CPU actually is. In
reality, each block is its own electrical unit.
8
Figure 3: Diagram of an Arithmetic Logic Unit (Wikipedia)
The diagram above shows a wiring diagram of what the “Combinational Logic” unit, or “Arithmetic
Logic Unit”, might look like, which performs simple operations like addition and multiplication. However,
how do we perform addition with electronic components? Notice how we have a bunch of little shapes
that have wires going in and out of them? These represent a logic gate, which performs a logical
operation that are easier to implement with electronics. For example, the AND gate below takes in A
and B and outputs Q . If A and B are 1, then it outputs 1. If either A or B are 0, then it outputs 0.
With a combination of these logic gates, we can actually create the logic to make operations like
addition. If we want to drill down even more, these logic gates are themselves made up of transistors, a
small electronic component that can take an electronic signal and operate like a switch or amplify the
signal. They look like the photo below. The biggest one here is about the size of a dime.
9
Figure 5: A photo of some transistors (Wikipedia)
However, with advances in technology, we’ve managed to make transistors smaller and smaller, to the
point where we actually use special processes to etch, or even grow, millions of transistors and those
wiring connections onto silicon wafers like the photo below.
Today, the transistors are so small they can be measured in lengths of literal atoms.
10
Is it running any of those?
Not quite. The language it’s using is something the CPU hardware can understand. Remember how
the “Turing Machine” had a set of rules that defined its behavior when it read a number from a
cell? Well, how these rules are defined for a computer is that they are directly hard-wired into the
connections between the electronic components. Remember that each instruction is just a number,
and for computers, numbers are represented in binary, with ones or zeros. These ones and zeros are
directly connected to specific wires, which turn on with a one, and off with a zero. Those wires are
then connected to various hardware components in the CPU, which do different operations based on
which wires are on or off. Here’s an example of an instruction:
This instruction is written in what we would call binary code, specifically using the x86 instruction set,
designed originally for Intel processors. The first 5 digits map to 5 wires connected to the Arithmetic
Logic Unit, which in this case, executes a subtraction when it sees that the wires are set to 00101 .
The next 3 digits specify the register (a place to store data) to subtract from ( AX in this case), and
the last 8 digits is the number that is being subtracted (which is 2 in binary in this case). While this
format is perfect for a computer since it maps exactly to wires on hardware, it is very difficult for
humans to read. This is why the concept of a programming language was invented. Basically, it allows
us to write instructions for computers that are more understandable to us. Of course, we can’t just
write a sentence like “Create a platformer character” and expect the computer to be able to process
that, so there are essentially levels of translation that need to occur between human speech and this
binary machine code, which we call layers of abstraction. An example of something that is one step up
from these binary instructions is one of the first programming languages that humans created, called
“assembly”. Here’s that same subtraction command, but in assembly.
Much easier to understand, right? The keyword SUB is simply mapped to the number 00101 to indicate
subtraction. We specified register AX with just the letters, which get translated to the binary value of
100 , and we specified the number 2 using the much easier-to-read hexadecimal format. However, we
eventually found out that while this was much easier to understand, it was still very tedious to write
since we have to do everything step by step.
So, the solution was to add another layer of abstraction by creating more programming languages. One
very popular language that is one step more abstracted than assembly is the C programming language.
Here’s an example function in C that calculates the square of a number.
No need to know exactly what it means, but you can sort of tell what it’s doing right? Here’s that
same function in assembly.
11
square:
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], edi
mov eax, DWORD PTR [rbp-4]
imul eax, eax
pop rbp
ret
Notice how it’s many more lines, and it also isn’t very obvious what it’s doing? The C function
has human-readable words, but assembly is more cryptic. How this works is that there’s another
program, called a compiler, that takes this C code and translates it into assembly. We call this process
“compilation”, and this translation process must happen for every single programming language. This
is awesome, and a lot of code is still being written in C today, but there’s one big thing that people
thought would be nicer if we didn’t have to manage.
Take a look at this next code snippet written in C. In it, we’re simply getting the user’s name and
returning “Hello [User’s Name]!”.
#include <stdio.h>
int main()
{
// Create a string
char name[30];
return 0;
}
In the last code snippet, there’s this line near the top: char name[30] . What we’re doing here is
reserving 30 pieces of memory for our data (in this case, 30 characters). This is one small example in a
larger suite of things called “memory management”.
A possible issue comes from memory access. For our char name[30] example, what happens if you
insert a name that is longer than 30 characters? Contrary to what you expect, it doesn’t result in an
error. What you end up doing is putting some unknown piece of data that exists sequentially after your
allocated data. Not only can this cause some problems with your program if you do this accidentally,
but it can be used maliciously. Hackers can target and rewrite parts of your computer system by
precisely using this out-of-bounds access (see buffer overflow).
In a language like C, you are also in charge of manually allocating how much memory you need. There
are a few complications with this. One such complication is you are also in charge of telling the
computer when you are done with that memory, thereby “freeing” it to the system for other programs
12
to use. What happens if you forget to free it? That’s called a “memory leak” and, eventually, your
system might run out of memory and crash. There are many other aspects of memory management,
but these are some of the big considerations. So, what is the logical next step? That’s automatic
memory management.
Languages like Python, Javascript, and yes, Lua, all have automatic memory management (commonly
through something called garbage collection). We call languages like these “high-level” programming
languages. This is because the lowest level is the actual hardware circuitry, and the more abstracted
we are from that level, the higher up we go. Of course, there are a lot of other differences with these
different programming languages, but I find memory management to be an obvious abstraction from
lower levels. A language like C is also considered high-level, but you could consider it slightly lower
than the aforementioned languages. In reality, the highest level you could get to is human speech,
so you can think about these levels of abstraction as closing the gap between our intentions and the
computer hardware.
What is Lua?
Finally, we get to the question: what is Lua? Well, Lua was created in 1993 in Brazil to match a growing
need. This need was for a higher-level language that met two qualifications. The first qualification was
that it had to be easier to use than slightly lower-level languages like C. The second qualification was
that it could be used in an embedded context, also known as scripting. What that means is you’re
kind of writing a program within another program or context. Think of stuff like Excel macros or
code instructions on a robot. At the time of Lua’s creation, Brazil had strong trade barriers in place.
These barriers made it prohibitively expensive to buy customized software, so the inventors (Roberto
Ierusalimschy, Luiz Henrique de Figueiredo, and Waldemar Celes) created their own language in order
to create software from scratch. You can read more on the Wikipedia article about Lua.
A key trait for scripting languages like Lua is that it needs to be lightweight, meaning that it’s simple,
small in size, and can easily be ported to different contexts. Python and Javascript are also scripting
languages, but since they’re used in so many different contexts, features were continuously added which
bloated the language to the point where they’re not that lightweight anymore. On the contrary, Lua has
remained extremely lean, very simple (easier to pick up for beginners), and easy to embed into other
13
programs. That’s why you see it in a lot of game development contexts like Roblox, LÖVE, Defold,
and more. Roblox is a complex game engine, but is designed to have users define their own behavior on
top of it - a perfect space for Lua to occupy. An additional benefit is that since it’s lightweight, it goes
great on hardware (e.g. PICO-8, Playdate, and LÖVR).
Now that we have some context on what Lua is, are you ready to learn it? Hope so! Let’s get to it.
14
Chapter 2: Setting Up Your Coding Environment
To write Lua code, you’re going to need a programming environment to code it in. The most basic
programming environment would be a text editor and a way to compile any Lua code you write in that
text editor down to something your machine can understand. The text editor could literally Notepad or
TextEdit or something simple like that, since code is always just plain text, though there are dedicated
text editors, called code editors, that have features that make writing code much easier.
To compile Lua, you could install something called the “Lua Interpreter”. However, for game development,
there are usually additional things that need to be handled, like how things are drawn to the screen
and how inputs from the user are received. That’s where the specific platform you’re targeting comes
into play. For example, the Playdate can take a crank input and only needs to draw to a 1-bit screen
that is 400x240 pixels, while LÖVE can take controller and keyboard input, and can also draw any
color to any screen dimension. So, each platform usually has its own compiler built on top of Lua that
takes those things into consideration. In addition, some platforms have other features, like Roblox has
a 3D environment editor and PICO-8 has a 2D map editor, and both have built-in code editors.
You should be able to find how to set up your programming environment of choice online, but to make
this easy for the purposes of this book, I won’t be using anything platform-specific - just basic Lua.
Instead, we can use an online programming environment that combines a simple code editor and a
built-in compiler. For the purposes of this book, I recommend the OneCompiler online Lua coding
environment (onecompiler.com/lua). Write your code on the left-hand side, click the “Run” button on
the top right to run your code, and anything that you print using the print command will appear in
the “Output” window in the bottom right.
Figure 8: OneCompiler
15
written some example solutions as well to check your work. At the very end of the book, there are
more difficult optional exercises as well for you to complete at your discretion.
Official Documentation
If you ever want to get some more information about something I cover, Lua has some documentation
in the form of a Reference Manual, which you can find at: www.lua.org/manual/5.0/manual.html. The
reference manual is quite technical, however, so there’s also a companion book written by the creators
of Lua called “Programming in Lua”, which you can find at: www.lua.org/pil/contents.html. But,
that book is written assuming you already have some background programming knowledge, which is
why you’re here. However, once you’re finished reading this book, you should be equipped to start
understanding what they’re talking about over there. Those resources should start to become the first
place you look to once you’re out there coding Lua in the wild since while I’ll cover a lot of things
about Lua, it won’t be completely comprehensive.
Every programming language has documentation, and so will the game development environment
you’re using (e.g. Playdate SDK documentation, PICO-8 manual, etc.). Since a lot of answers to basic
questions can just be found in the manual, it’s good to get in the habit of referencing documentation
first when you have a question.
16
Chapter 3: Writing Code vs Natural Language
What a lot of beginner programming resources fail to cover is how different writing code is from
writing in natural language. In English, if you spell something wrong, use incorrect grammar, or
omit some spacing/punctuation, the idea of your sentence can still generally be understood by your
reader. However, with code, you are writing very specific instructions to a machine. If the grammar or
punctuation is incorrect, the compiler will not be able to infer your intent. To really understand this
difference, let’s take a look into the rules that we’ve put into just plain written English to eliminate
ambiguity.
Take a look at this sentence: “Let’s eat, grandma!” With our use of punctuation, we’ve made it clear
that we’re just inviting our sweet grandma to eat at the table.
However, what if we remove punctuation? It becomes: “Lets eat grandma” - now, it’s not so obvious,
right? This sentence could potentially mean that we’re planning on eating our poor grandma. With
code, it’s like every single sentence is one of these ambiguous phrases. We have forms of punctuation
in code, but because of this ambiguity, the correct punctuation is not optional. If you use incorrect
code punctuation, your code will not compile and it will give you an error. Luckily, in most cases, the
compiler will tell you where your punctuation mistake is. We’ll learn about what these punctuation
rules are as we go.
In natural language, from context (e.g. from the previous sentence), it’s generally pretty obvious what
the writer is implying, even if incorrect punctuation is used in an ambiguous sentence. However, context
is a very nebulous concept. Modern LLMs like ChatGPT can have some understanding of context, but
programming language compilers are relatively stupid compared to those fancy models. They cannot
infer anything from context, so again, you need to write things properly according to the grammar
defined by the programming language. We refer to this concept of programming language grammar as
“syntax”.
Spaces also play an important role in both natural language and programming languages. Take the
phrase “We are now here”. This implies that the group has arrived at a specified location. But if we
remove the space between “now” and “here” - “We are nowhere” - it takes on a whole new meaning.
Likewise, in Lua, there are specific keywords that have special meanings. There must be a space
between keywords and surrounding elements so that they can be understood by the compiler, with one
exception: when there is some punctuation. It’s the same in written natural language - if we add some
punctuation to the sentence - “We are,now,here” - it retains the original meaning despite the lack of
spaces. So, in this context, the spaces are optional, but you want to keep them for legibility. Likewise,
the spaces would be optional around keywords in code if there are punctuation symbols around them,
but you keep them for legibility.
Similar to spaces, pressing enter to create a new line is also a tool that we use to organize our writing.
In this section, I’ve separated my text into multiple paragraphs to make it more legible and to divide
my text into related segments. However, I could put everything into one paragraph and it would
technically be the same. But, splitting it into different paragraphs makes it much easier to read. In the
same way, we split up code into different lines, with each line usually doing one main operation. You
could technically put everything on one line, but it makes it extremely hard to read. It is common to
refer to code by the line number that it is on. Just like you say “turn the pamphlet to page 5” or “look
at the third paragraph”, you would frequently hear programmers say “go to line 256”. Code editors will
typically have the line numbers written on the side for this reason.
Let’s take a look at some examples of Lua code to illustrate some of these concepts. Here is a snippet
of code that checks some health value and prints You're alive if it’s greater than 0, and prints
You're dead if it is less than or equal to 0. Of course, you might not understand the specifics of how
it’s written since I haven’t taught you anything about the syntax of Lua, but you should be able to
pick out that there seem to be some certain keywords and punctuation.
17
local health = 25
If you notice, some words and symbols are different colors. This is because a code editor recognizes the
syntax of a language and can color-code things accordingly. We refer to this as “syntax highlighting”.
You’ll get used to this coloring and can use it to do a quick sanity check to confirm if you are writing
the syntax properly. For example, if you misspell a keyword, it won’t turn into the color you expect.
The code is currently organized in a way that a typical programmer would come to expect - with things
spaced out and on different lines to make it legible. You might find some differences in spacing, like
removing or adding some spaces/newlines between different symbols and keywords, like so:
local health=25
if health>0 then
print ( "You're alive" )
else
print ( "You're dead" )
end
However, it’s actually not the most compressed it can be. We can put everything on one line and
remove a bunch of spaces until only the necessary spaces are left, and we would get left with this
monstrosity:
This is still able to compile without any complaints from the Lua interpreter. Notice that there are
only spaces between words and numbers that are not separated by a symbol. It works, but it’s hard
to read. When you are working on your game, it’s not just about making your code work, but also
writing it in a way that is easy to understand for you. Your code constantly evolves as you add more
things to your game, so making readable code helps you when you go back to it and make changes.
This extends not just to the formatting of your code, but also to the flow of your logic and the way you
label and name certain things. We call this “code quality”. Code quality is not something typically
taught to beginners, but I think it’s an important concept to understand, so I will be taking the liberty
to touch on it throughout this book.
Anyway, I found that beginners are frequently confused about spacing and when different things should
be on different lines. Because almost all code is written with things on different lines and no one
explains the reasoning behind it, beginners are led to believe that things need to be on different lines and
that it is part of the language syntax. Apart from a few exceptions (namely the Python programming
language), this is not true. As we learn things throughout the book, I will point out what the common
practice is. If why we’re even talking about this is going over your head, it should make more sense as
we learn some actual code. Now that this is out of the way, let’s actually get started!
18
Chapter 4: Variables
Games are all about data. When you get damaged, your health goes from 100 to 80. When you create
a character, it saves the name you gave it. When you open a door, it stays open. What keeps track of
all these values? This is all handled by a fundamental concept of programming, which is the concept of
variables. A variable is nothing more than a container to store a piece of data. Think about common
game-related data like health, a score, or your inventory. Those need to be kept track of somehow, and
we can do so with the help of variables.
Every variable has a name that we decide on. We can create a variable by simply typing out its name
and giving it a value. To give a variable a value, we use the equals sign ( = ). In the example below, I
create a variable called myVariable and give it a value of 1 . We call this assignment.
myVariable = 1
The equals sign here is not quite the same as the equals sign that you might be used to in math. In
math, the equals sign means that whatever is on the left side and the right side are the same value.
This equals sign means that we take whatever is on the right side and put it into any variable on the
left. The equals sign is an example of the syntax we mentioned earlier - the special characters or words
that are reserved and have a special function. You can think of it as punctuation or grammar for code.
If you try running this code in a code environment, you won’t see anything appear because we haven’t
used this data for anything. For testing purposes, we can use a special command called print that
takes a value and outputs it.
myVariable = 1
print(myVariable)
Go ahead and try that in your code environment of choice. If you’re using the OneCompiler one I’ve
suggested (onecompiler.com/lua/), the printed value will appear in the “Output” window in the bottom
right. Try creating other variables with different names and different values. You should see that these
variables are essentially named references to values.
Variables can be reassigned as well. Let’s look at another short example.
health = 30
print(health)
health = 10
print(health)
If you run this, you should see that it first prints 30, but then it prints 10. This is because the underlying
value that the health variable contains has changed. In this example, we can see that code executes
sequentially, from top to bottom. In most cases, you can think about it like the code is executing line
by line. While this is not strictly true in all cases, it’s how we usually think about it. Pretty simple,
right? We’ll see more of why this is useful later.
19
variables, we will be talking about how they are case sensitive, what error messages are, and how to
properly name your variables.
Case Sensitivity
Note that variables are CASE-SENSITIVE. Don’t get stuck when you’re trying to access hasKey , but
you wrote haskey instead! In fact, assume everything is case-sensitive. This is an extremely common
beginner mistake, so always make sure to check your spelling and capitalization if something isn’t
working!
Error Messages
As I mentioned earlier, there’s a sort of grammar to a programming language known as “syntax”. If we
make mistakes, the program will fail to compile and will not run. However, luckily, the compiler will
give us details of what went wrong in the form of an “Error Message”. Usually, these errors say exactly
what the issue is. I find that a lot of beginners ask me “What’s wrong - my code isn’t working”, when
usually the answer is just in the error message, but I think that’s just because there is a bit of a learning
curve to understanding what the messages are saying. In each section, I will go over some common
errors and what the corresponding error message looks like so we can slowly learn them throughout the
book.
For this first section, let’s just take a look at the anatomy of an error message. In the code below, we
accidentally typed == instead of = on line 3.
a = 1
b = 2
c == 3
d = 4
e = 5
Because of this, the compiler will give something like main.lua:3: syntax error near '==' as an error.
The main.lua is just the file the error occurred in. We’re only working in one file throughout this book,
but the file name becomes very important once you start working on a larger project. The :3 at the
end of main.lua:3 indicates that the error is on line 3. This is part of why we split code into different
lines - errors are easier to track down. syntax error just tells us that we typed something that doesn’t
match the grammar of the language. near '==' tells us generally where the error is around. Now, in
this case, it points exactly to what the error is, but sometimes, it might tell us roughly where the error
is.
If you notice, the error message doesn’t tell us how to fix the issue. However, it greatly narrows down
where and what the issue is, so you can save yourself a lot of headache by just looking carefully at what
the error message says.
Note that if we misspell a variable, even just using different capitalization, the code will happily run
without spitting out an error. This is because the code thinks you’re referencing another new variable,
and this variable has a value of nothing, or nil . We’ll cover what nil is in the next section, but just
keep this in mind, because error messages won’t save you from everything.
myVariable = 10
print(myvariable) -- Prints "nil"
20
Naming Conventions
This is something you might not see in a lot of tutorials or books, but it’s something you’ll come across
often. The topic of naming conventions touches on how best to name your variables. Something new
developers typically don’t realize is the importance of what you name your variables. Naming variables
sequentially or random combinations of letters like a1 , a2 , a3 , or someVariable might seem fine,
but the very moment you need to use a bunch of variables together or you leave your code and need to
come back to it, it will be very hard to keep track of what’s going on. Instead of l , maybe something
like len or length might be more appropriate. You can even go as far as collisionBoxLength or even
playerCollisionBoxLength . There’s nothing wrong with creating longer, descriptive variable names.
In fact, it might be preferable since you can easily understand your code at a glance. As you may recall,
a programming language is an abstraction between our high-level human intentions and the computer,
so it should be used in a way that is maximally understandable by you, the human. Spending a little
extra time naming your variables will save a lot of future time and headaches.
Now, you may have also noticed a curious convention I used when I wrote the example variable names
in the last paragraph. I wrote them starting with a lowercase and then capitalized the start of every
next word. This is referred to as camel case (camelCase) since the capital letter looks like a hump in a
camel. The reason for this choice is that variables cannot have spaces in them, so with camel case, it’s
easier to tell where the word breaks are. Another similar convention is snake case (snake_case), with
words separated by an underscore, or kebab case (kebab-case).
You may wonder why we don’t capitalize the first letter. Well, the answer to that is just that it’s
convention, but conventions can be helpful if anyone else ever takes a look at your code or if you work
on a team. The main point I want to make, however, is that the best naming conventions are the
ones you stick to. This way, naming conventions can help you quickly determine information about a
variable. For example, I use all capital letters in snake case to dictate one specific type of unchanging
variable, like PLAYER_JUMP_HEIGHT = 10 , which is a pretty common naming convention. For most other
variables, I do camel case with the first letter being lowercase (e.g. thisIsMyVariable ). These are the
conventions I have chosen, and by sticking to them I can immediately tell what kind of variable I’m
working with. However, if I’m working in some sort of shared codebase, like on an enterprise application
at my software engineering job, I comply with the naming conventions that are used in that codebase.
Just something to keep in mind.
21
Chapter 5: Data Types
In the last section, I showed you how we can create variables and store basic integers in them. However,
variables can hold a number of different things. We call these different things “Data Types”. The
different data types are Numbers, Strings, Booleans, and Nil.
Numbers
We’ve seen that we can store numbers already. However, it’s not just limited to integers. Numbers
can be decimals, negative numbers, and even scientific notation if you’re so inclined (though I’ve never
ever seen anyone use scientific notation in code before). Common numbers that can be a decimal or
negative are anything physics related. So, if you have a projectile flying through the air, the rate it’s
flying probably won’t be some nice round number. If it’s going left, it might be a negative velocity, and
positive when it’s going right.
a = 3.14
b = -200
c = 1.2345e5 -- equivalent to 123450
Strings
So far, we’ve been working with just numbers, but there’s more to games than just numbers. Frequently,
we’re working with text. Think character dialog, labels like Score: 500 , the settings menu, etc. So,
how do we store text into a variable?
You can’t just write out a word by itself, because that will be interpreted as a variable or a keyword.
In the example below, the compiler will think that you’re referencing the variable Hello and a special
! character.
introDialog = Hello!
We need to wrap the text we write with double quotes ( "" ). You can also use single quotes ( '' ) as
well (this is commonly used for single characters). This is what we call a string because it’s a string of
characters. In the example below, I’ve assigned a string to the variable introDialog . When you print
it, it’s as you expect.
As a quick note, you might notice the differently styled text that comes after the double hyphen ( -- ).
That’s referred to as a “comment”. Anything you append with a double hyphen will not be interpreted
as code, so you can use it to leave notes in your code, or temporarily disable code for testing, which is
quite useful.
Strings can range from being completely empty ( "" ), to a single character ( "a" ), or even up to an
entire book ( "insert entire text of War and Peace here" ).
Boolean
Another important data type is the boolean, which can take on the values of true or false . This data
type is important because there are many cases where you are considering the condition of something.
22
Does the player have a key to open the door? If true, then open the door. If false, then don’t open the
door. We can keep track of that using the boolean data type. Those can be explicitly assigned with
true and false , which we call “keywords”. Keywords are special words in a programming language
that have a specific purpose and are interpreted differently. Usually, they are highlighted in a different
color by your coding environment, so you should be able to tell pretty easily if something is a keyword.
Note that, like everything else, booleans are case-sensitive. True will be a variable, while true is the
boolean value.
hasKey = true
isDead = false
Nil
Another important data type to keep in mind is something called “Nil.” This implies the absence of data,
and it’s not the same as false or 0 . If you create a variable without assigning it to anything, the
default value will be nil . You can also assign something to nil manually by using the nil keyword.
player = nil
print(player) -- Prints out "nil"
The difference between nil and 0 is a bit subtle, so let’s think about a small example. Consider a
number variable accountBalance that represents some bank account balance. A value of 0 would
indicate that the account exists and has 0 dollars. However, a value of nil might mean that the
account doesn’t even exist.
Here’s another example to illustrate the difference between nil and false . Let’s say that we have
some boolean variable isOpen that’s true if a container is open and false if it’s closed. A nil
might represent there being no container, or possibly a container that cannot be opened or closed.
Escape Characters
What if you want to put a quotation mark in a string? If you try to do that normally ( """ ), you’ll
find that the first two quotation marks create a string, and the last quotation mark opens up a string
that never gets closed. In that case, you could mix and match quotation marks, with a double quote
surrounded by single quotes ( '"' ), or you can use something special called escape characters, which is
denoted by a backslash ( \ ). Simply append the quotation mark with a backslash ( "\"" ). And if you
want a backslash character, you guessed it, you write a double backslash ( \\ ). Escape characters are
useful in other scenarios when you need certain special characters. A common one is creating a new
line. That is created by using backslash n ( \n ).
You can learn more about strings in “Programming for Lua”: www.lua.org/pil/2.4.html
23
String Errors
If you miss a quotation mark, the error will simply be unfinished string near [something] . In the
example below, since it’s the only line in the file, we’ll get main.lua:1: unfinished string near <eof> .
eof just stands for “end of file”.
print("Hello World!)
The same thing happens if you have an extra quotation mark or if you forget to escape it using a
backslash.
24
Chapter 6: Operators
In games, we don’t want to just store data, but we want to do things with it. When you use a key on a
door, reduce your key count by one. Multiply your attack damage by two for a charged attack. Check
to see if your health is less than 0 to show a game over screen. We call these things that we do to data
operations, and we do so using operators. Operators are symbols that tell the code to perform specific
mathematical or logical operations. This is important, since in games, you’re frequently calculating or
comparing different bits of your game data to progress the game.
Arithmetic Operators
First, we have arithmetic operators, which are for basic math stuff. There are 7 arithmetic operators:
addition ( + ), subtraction ( - ), multiplication ( * ), division ( / ), exponent ( ˆ ), modulus ( % ), and
the negative sign ( - ).
The first is addition ( + ), which we can use to add two numbers together. Let’s take a look at this
addition operation.
print(1 + 2) -- 3
print(1 + 2 + 3) -- 6
Just like you’re used to with basic math, the plus sign ( + ) takes the number to the left and the right
and adds them together. We can store the result into a variable like so.
a = 1 + 2
print(a) -- 3
In this equation, the variable a gets set to the value of 3. One thing to note is the order of how the
operation and assignment is done. Everything to the right of the equals sign ( = ) is done first, and
then the value is assigned to variable on the left. So, we can read a = 1 + 2 as “compute 1 + 2 and
store the result in the variable a ”.
We can also use operators on variables as well, and it will compute the result based on the value stored
in the variable.
a = 1
b = 2
c = a + b
print(c) -- 3
a = 2
b = 7 + a
print(b) -- 9
The same things can be done with subtraction ( - ) as well. We also have multiplication, which is
denoted with an asterisk ( * ), division, denoted with a forward slash ( / ), and taking the exponent,
which is denoted with a caret ( ˆ ).
25
a = 3 - 9 -- -6
b = 8 * 7 -- 56
c = 3 / 20 -- 0.15
d = 2ˆ4 -- 16
You can make any single number or variable negative by appending a negative sign in front of it. Notice
here that I’m using an operator on the same variable as the one I’m assigning to. Because everything
on the right of the equals sign is done first, we first take the current value of a (5), then we negate it
(-5), and then we override the value of a with our new result.
a = 5
a = -a
print(a) -- -5
Lastly, we have something special called the modulus operator, denoted with the percent symbol ( % ).
This gives you the remainder after integer division. For example, 17 modulus 10 will give you 7. It’s
commonly referred to as “mod” (e.g. 17 mod 10 is 7). Surprisingly useful - for example, you can use it
to tell if something is even or odd if you mod it by 2. A result of 0 means that it’s even, and a result of
1 means that it’s odd. Anytime you mod something and it returns 0, you know that the number is a
multiple of whatever you modded it by.
b = 15 % 6
print(b) -- 3
print(8 % 2) -- 0, so 8 is even
print(23 % 2) -- 1, so 23 is odd
print(36 % 3) -- 0, so 36 is a multiple of 3
Relational Operators
In games, we don’t just want to do arithmetic operations on data - we also frequently want to compare
different bits of data. Is your score higher than the minimum required score to pass the level? Only
wizards can use wands - is the player a wizard class? To do so, we use something called relational
operators. There are six relational operators, which are less than ( < ), less than or equal to ( <= ),
greater than ( > ), greater than or equal to ( >= ), equals to ( == ), and not equals to ( ~= ).
You might have learned about some of these in your math classes, but they operate a little differently
here. In math, when you write 10 > 5 , you are positing a statement that is always true - “10 is greater
than 5”. However, in Lua, when you write 10 > 5 , you are instead doing a check - “Is 10 greater than
5?”. If the statement is true, then the > operator will return the boolean value true .
In math, the statement 5 > 10 is an illegal statement, because it simply isn’t true. However, in Lua,
when you write 5 > 10 , it will instead just return false . In this way, these relational operators are
much more useful because we can use it to check the ever-changing state of our game and use it to
make decisions.
Like arithmetic operators, we can use variables with relational operators as well. Let’s say you want to
check if the player died - we can check if playerHealth is less than or equal to 0. Or, if we want to see
if the player has enough keys - we can check to see if keys is equal to keysRequired .
26
playerHealth = 20
print(playerHealth <= 0) -- false
keys = 5
keysRequired = 5
print(keys == keysRequired) -- true
We use the double equals sign ( == ) because the single equals sign ( = ) is already used for as-
signing a value to a variable. To clarify, let’s take a look at the example above. When we write
keys == keysRequired , we’re checking if what is to the left of == is the same value as what is to the
right. If it’s the same, then we return true. If not, then we return false.
With relational operators, we can only use them on numbers, with the exception of equal to ( == ) and
not equal to ( ~= ), which we can use on strings and booleans as well.
selectedItem = "dagger"
-- Is the selected item a hammer?
print(selectedItem == "hammer") -- false
isJumping = true
-- Is the player *not* jumping?
print(isJumping ~= true) -- false
Like with any other operator, we can store the result of relational operators into variables.
playerHealth = 20
playerIsDead = playerHealth <= 0
print(playerIsDead) -- false
It’s a little confusing when we write the variable assignments this way, so just to reiterate, the operations
on the right of the equals sign ( = ) are done first. In this example, playerIsDead = playerHealth <= 0 ,
the playerHealth <= 0 part happens first, and then the resulting value gets put into the playerIsDead
variable. Since English is read left to right, it might seem like playerIsDead is set to playerHealth
first, but there’s a subtle difference, so I just wanted to point that out. It reads “Is playerHealth
less than or equal to 0? If so, return true. Otherwise, return false. Store that result into the variable
playerIsDead . You can imagine a set of invisible parentheses around the right side if that makes it
easier to visualize: playerIsDead = (playerHealth <= 0) .
Now, as to what we can do with the boolean results of these relational operators and how we can use
them to make decisions, we’ll be covering that in the next chapter: “Conditionals”.
Logical Operators
Beyond just simply comparing two bits of data together, in games, we also need to compare the results
of these comparisons. Consider if you have two conditions to pass a level - enemies defeated and keys
acquired. What do we do if we need to check if the required enemies are defeated and we have enough
keys? Or, what we just want to check one or the other? To do those checks, we have the logical
operators, and , or , and not , which are used with boolean values.
27
AND Operator
The and operator takes two booleans. If the boolean to the left and the right are both true , then it
returns true. Otherwise, it returns false .
OR Operator
The or operator takes two booleans as well. If either the boolean to the left or the right are true ,
then it returns true. This includes if both are true as well. If both are false , then return false .
Let’s say you want to check the player input and swing the player’s sword if either the A button is
pressed OR the B button.
aButtonPressed = false
bButtonPressed = true
swingSword = aButtonPressed or bButtonPressed
print(swingSword) -- true
NOT Operator
The last logical operator is NOT. This simply turns a false condition true, or a true condition false. It
takes one boolean to the right. If it’s false , return true . If it’s true , return false .
28
print(not true) -- false
print(not false) -- true
Maybe the player can only attack when they’re not jumping.
isJumping = true
playerCanAttack = not isJumping
print(playerCanAttack) -- false
Miscellaneous Operators
Two more miscellaneous operators are the concatenation operator ( .. ) and the length operator ( # ).
The concatenation operator, consisting of two periods, concatenates (combines) two strings. The pound
sign returns the length of the string.
You can also group operations and affect the order of operations using parentheses.
isAlive = true
hasEnoughKeys = false
killedEnoughEnemies = true
I find that I use parentheses a lot for visual clarity, but also to make sure things are happening in the
order I expect since, in many cases, it’s required if you need to override the existing order of operations.
In Lua, here is the default order of precedence.
1. ˆ
2. not | - (negative sign on a number)
3. * | /
4. + | - (subtraction)
5. ..
6. < | > | <= | >= | ~= | ==
7. and
8. or
29
Whitespace
I think now is a good time to mention that I’ve been adding spaces around everything for readability,
but spaces have no impact on the code execution. It’s up to you on how you want to space it. All these
lines below are functionally equivalent.
playerIsJumping = true
However, if you think about it, the == true check is pretty redundant. playerIsJumping is already a
boolean, and if it’s true the check will return true , and if it’s false the check will return false . In
that case, can’t we just use the playerIsJumping variable itself? And, instead of checking == false
or ~= true , we can make it a little more concise by just using the not operator.
playerIsJumping = true
Of course, you can still write == true for clarity, but you’ll often see it dropped for brevity if you look
at code from others.
print(not "Hello")
print(not 100)
print(not 0)
print(not nil)
If you thought it would give an error, surprise! Interestingly, there are valid results printed out. This
is because each data type has a hidden boolean value associated with it. All strings and numbers
(including 0) are treated as true to any logical operation. Why? Well, there are certain conveniences
that come with that, but at the end of the day it was just a decision by the creators of Lua. We call
these “truthy” values. However, nil is an exception and is considered false by any logical operation.
30
We call this a “falsy” value. One reason this was done is so you can quickly tell if something is a valid
(non-nil) value. Try using different logical operators ( and / or ) on different values to see how they
react!
Operator Errors
Common operator errors usually involve performing operations on different data types. Take a look
at this next example - we’re trying to add a string to a number. The proper operation should be the
string concatenation operator .. , but instead, we’re using + . Luckily, the error messages are usually
pretty descriptive. In this case, it’s main.lua:3: attempt to add a 'string' with a 'number' . You’ll
also see some additional messages under something that says stack traceback: . Don’t worry about
that for now - we’ll cover it when we get to “Functions”.
name = "Squid"
score = 2750
print(name + score) -- Error!
You’ll get an error if you try to do most operations on nil values, which doesn’t seem hard to
avoid, but it’s quite easy to do unintentionally. The most common way is just by spelling a vari-
able wrong. In this next example, I’m adding a levelOneScore and a levelTwoScore , but I ac-
cidentally spell the second one as levelTwoscore with a lowercase s . Lua won’t tell you that
you’re using the wrong variable - instead, it will happily use the misspelled variable as a com-
pletely new variable. But, as we know, uninitialized variables are just nil . So, you’ll get an error
that says main.lua:3: attempt to perform arithmetic on a nil value (global 'levelTwoscore') . If
you’re getting errors, I want to encourage you again to look really closely at what it’s saying, because
it might be hard to spot stuff like this. In this case, it should set off a red flag in your head that you’re
getting a nil value since you just initialized it, so the first thing to check is probably spelling.
levelOneScore = 1250
levelTwoScore = 2750
print(levelOneScore + levelTwoscore) -- Error!
Since we use parentheses with operators a lot, you can get some issues with that as well. Sometimes,
you end up working with a lot of nested parentheses, and it gets hard to track which set you’re on and
whether you forgot a single parenthesis or not. In most code editors, you can put your text cursor next
to a parenthesis and it will highlight the matching one, which helps, but also, if you happen to forget
an opening or closing parenthesis, the compiler will throw an error. Sometimes, the error message will
be different based on which parenthesis you’re missing.
In this next example, there should be an extra opening parenthesis right before killedEnoughEnemies .
The error message given is main.lua:1: unexpected symbol near ')' . Unfortunately, it doesn’t really
say that it expected an opening parenthesis, just that it’s not expecting a ) symbol since an opening
one was never created. Also, it could be referring to any closing parenthesis, so the near ')' sometimes
isn’t very helpful. At least we get to know what line it’s on, which gets us close enough.
In this next example, we’re missing a closing parenthesis at the end. This time, the error is a bit more
descriptive, with the error main.lua:2: ')' expected (to close '(' at line 1) near 'print' . The
31
error is attributed to line 2, but it tells you that line 1 is where the opening parenthesis started. Of
course, it’s up to you to figure out where you want to put that closing parenthesis.
Assignment Operators
A common operation that you will find yourself doing is taking a variable, doing some sort of operation
on it, and then storing that new result back into the same variable. Here’s an example:
coins = 50
print(coins) -- 50
-- Picked up a coin!
coins = coins + 1
print(coins) -- 51!!!!
If you notice, we’re repeating the coins variable, so wouldn’t it be nice to have something that
combines both an assignment and an operation together? Unfortunately, Lua doesn’t have anything
like that, but in most other languages they do. For example, here’s what that looks like in the Python
programming language:
# a = a + b
a += b
# Output
print(a) # 8
However, in some runtimes of Lua, for example for Roblox and the Playdate, they’ve added assignment
operators. You can check if the platform you’re coding for has added this. This is what we would call
Syntactic Sugar because it doesn’t really add anything new, but it just looks nicer and makes it a little
“sweeter” for us to use since you don’t have to write the same variable again. Not the most important,
but I thought I would mention it since programmers love this sort of thing (me included), and we’ll see
other instances of syntactic sugar later.
32
coins = 50
print(coins) -- 50
-- Picked up a coin!
-- coins = coins + 1
coins += 1
print(coins) -- 51!!!!
33
Exercises: Operators
Below are a few exercises that you can complete to test your knowledge of operators. As mentioned
before, you can use the OneCompiler online Lua coding environment (onecompiler.com/lua) in order to
try writing some code. Click the “Run” button on the top right to run your code and anything that
you print using the print command will appear in the “Output” window in the bottom right.
Possible solutions to the exercises will be presented at the end of this section, however, I want to note
that programming is like a maze with multiple paths. There are many, many ways to the right answer.
If your code looks a bit different than mine, but works properly, I would consider that a correct solution.
isRaining = true
hasUmbrella = false
Write a script that uses logical operators ( and , or , not ) to print true if you can go outside without
getting wet and false otherwise. Try changing the value of isRaining and hasUmbrella to see if
you’re printing out the correct result. The output should match this table:
34
(Solution) Exercise 1:
(Solution) Exercise 2:
-- Define a and b
a = 5
b = 10
-- a is greater than b
print(a > b)
-- a is less than or equal to b
print(a <= b)
-- a is equal to b
print(a == b)
(Solution) Exercise 3:
Exercise 3 has many solutions. Below are two possible options. What happens when you change the
order or add/remove parenthesis?
isRaining = true
hasUmbrella = false
-- Option 1
print(not isRaining or hasUmbrella)
-- Option 2
print((isRaining and hasUmbrella) or not isRaining)
35
Chapter 7: Conditionals
A lot of the operators we covered deal with booleans, which probably isn’t something you come across
very commonly in day to day life, however, it’s very common in programming and game development.
The main reason is because of how decision-making in programs work. Check if the player has the
double jump ability - if so, allow the player the double jump. Check if the player is within a certain
radius - if so, spawn new enemies. We can check different conditions based on the state of a game,
and if the condition is met, we can execute a specific action. We do this by using something called
conditionals. Here’s an example of an “if statement” conditional.
hasKey = true
if hasKey then
print("Door unlocked!")
end
We can read this as “If hasKey is true, then print Door Unlocked”. The format is “if condition, then
do something”. Finally, you close it up with an “end”. The condition is whatever is in between the if
and the then . The condition is a single boolean value, and if it’s true, then whatever is between the
then and end occurs. Otherwise, it doesn’t. In the example above, the variable hasKey is true, so
Door unlocked! gets printed. If hasKey is false, then the code simply gets skipped.
Notice that the print statement is indented. This is to make it obvious that this code is within the if
statement. It’s not strictly necessary, but you would be hard-pressed to find any code that doesn’t
follow this convention. You can indent by pressing tab or using the spacebar, but pressing the tab
button is conventional. This is because if you want the indentation to be made up of spaces as opposed
to a single tab character, most modern code editors can convert your tab input automatically into the
equivalent number of spaces.
The previous example is a pretty simple one only involving one statement, but you can put anything
where the condition is, as long as the final result is a single boolean value. Here’s an example.
hasKey = true
enemiesKilled = 10
if hasKey and (enemiesKilled >= 5) then
print("Door unlocked!")
end
The statement hasKey and (enemiesKilled >= 5) resolves down to a single boolean value - in this case,
true . enemiesKilled >= 5 is true , so the statement becomes hasKey and true , which becomes
true since hasKey is true as well. Therefore, the if condition is satisfied, and Door unlocked is
printed to the console.
We’ve been using one-line conditional examples, but there can be any number of lines inside of a
conditional.
if true then
print("Line 1")
print("Line 2")
print("Line 3")
end
36
Sometimes we want to run some code if the condition has been met, but some other code if the condition
hasn’t been met. Of course, we can do so with two if statements by simply putting a not before one of
the conditions.
hasKey = false
if hasKey then
print("Door unlocked!")
end
if not hasKey then
print("Door locked!")
end
However, if you happen to change the first condition, you’ll need to remember to change the second
one as well. Luckily, we have something called the else statement that can follow any if statement
and gets run if the if condition is false.
hasKey = false
if hasKey then
print("Door unlocked!")
else
print("Door locked!")
end
This can be read as “If hasKey is true, then print Door Unlocked. Otherwise, print Door Locked”.
Here’s another example:
coins = 2
coinRequirement = 5
if (coins >= coinRequirement) then
print("Door unlocked!")
else
-- Prints "Get 3 more coins!"
print("Get " .. (coinRequirement - coins) .. " more coins!")
end
Sometimes, you want to check multiple conditions. Of course, we can do that by simply having multiple
if statements. Take a look at this next example.
37
level = 6
However, if you look closely, the rewards are mutually exclusive. If we get a Wooden Sword, we’re not
getting an Iron or Steel Sword. We manually create this exclusivity by checking if level > 5 for the
Iron Sword and level > 10 for the Steel Sword, but those levels should be dependent on the previous
conditional. This mutually exclusive conditional is a common pattern, so we have a keyword for it -
the elseif statement.
level = 6
This can be read as “If level is less than or equal to 5, then give a Wooden Sword reward. Otherwise,
if level is less than or equal to 10, give an Iron Sword reward. Otherwise, if level is less than or
equal to 15, then give a Steel Sword reward”. The conditions will be checked sequentially. First, the
if condition. Then, all the elseif conditions one by one. If any of the if or elseif statements
are true, it stops checking all the other conditions and runs the code inside the conditional block.
Notice that in this example, the order of the statements matter. If the level <= 15 check comes first,
then that will take precedent over the other conditions.
Another nice thing about these elseif statements is that you can have an else block as well that
encapsulates all the conditions and catches any case where none of them are met.
38
level = 20
Conditional Errors
With conditionals, we’re introduced to some syntax to create these conditional blocks, which means
more potential error messages. A common error is accidentally typing else if instead of elseif .
Below, we’ll get a main.lua:5: 'end' expected (to close 'if' at line 1) near <eof> error, which
seems weird since we already have an end . But this is because the compiler is reading the separated
if as its own conditional, so it’s expecting two end s for two conditionals.
if true then
print("Hello")
-- Should be "elseif"
else if false then
print("Bye")
end
Some programming languages, like Python, use elif instead of elseif , so if you accidentally type
that, it will just throw a syntax error. Below, we’ll get main.lua:3: syntax error near 'false' .
39
if true then
print("Hello")
elif false then
print("Bye")
end
For else statements, you don’t need to put a then or any condition. If you put any of those, you’ll
just get an unexpected symbol error.
40
Exercises: Conditionals
Below are a few exercises to test your understanding of conditionals. Feel free to use your coding
environment of choice or the OneCompiler online Lua coding environment (onecompiler.com/lua).
Possible solutions to the exercises will be presented at the end of this section.
41
(Solution) Exercise 1:
hasKey = true
if hasKey then
print("Door unlocked!")
else
print("Key required!")
end
(Solution) Exercise 2:
Exercise 2 has multiple solutions. The opposite of >= is < , and the opposite of <= is > , so you can
choose to rearrange logic in a way that makes the most sense to you.
health = 30
-- Option 1
if health < 20 then
print("Critical health!")
elseif health >= 80 then
print("Healthy!")
else
print("Moderate health")
end
-- Option 2
if health < 20 then
print("Critical health!")
elseif health < 80 then
print("Moderate health")
else
print("Healthy!")
end
(Solution) Exercise 3:
Exercise 3 has multiple possible solutions. Below are two possible solutions. The first one simply uses
nested conditionals to narrow down the result. The second option is a bit more concise by using a
temporary result variable to store a candidate for the largest number.
42
x = 20
y = 30
z = 10
-- Option 1
if x > y then
if x > z then
print(x)
else
print(z)
end
else
if y > z then
print(y)
else
print(z)
end
end
-- Option 2
result = x
print(result)
43
Chapter 8: Functions
With everything we’ve learned so far (Variables, Data Types, and Conditionals), you can actually
already make some functioning game logic. Technically, you can make any game with just those
fundamental concepts, but there are a lot of situations where it would be nice to have some additional
tools to make things easier and more organized. One of those tools is something called a function. Let’s
take a look at an example of why a function might be useful.
Here’s a snippet of code where we’re keeping track of a player’s health and an enemy the player has
collided with. With the power of imagination, we can imagine that we’re getting the collided enemy
using some sort of collision system, but here we’re just setting it directly as an example. We check
what type of enemy it is, and then “damage” the player by reducing their health by a set amount.
health = 100
collidedEnemy = "zombie"
if collidedEnemy == "slime" then
health = health - 3
elseif collidedEnemy == "ghost" then
health = health - 5
elseif collidedEnemy == "zombie" then
health = health - 10
elseif collidedEnemy == "skeleton" then
health = health - 15
end
print(health) -- 90
This looks fine, but imagine we wanted to add some things. What if we wanted to add some armor
value, that reduces the damage taken? Ok - let’s just add that everywhere.
health = 100
armor = 2
collidedEnemy = "zombie"
if collidedEnemy == "slime" then
health = health - (3 - armor)
elseif collidedEnemy == "ghost" then
health = health - (5 - armor)
elseif collidedEnemy == "zombie" then
health = health - (10 - armor)
elseif collidedEnemy == "skeleton" then
health = health - (15 - armor)
end
print(health) -- 92
Ok, that works, but what if the armor is greater than the damage taken? For example, if armor is 10 ,
then getting hit by a slime would deal -7 damage, which would heal the player. That doesn’t make
sense, so let’s add some checks to make the damage 0 if it goes negative.
44
health = 100
armor = 2
collidedEnemy = "zombie"
if collidedEnemy == "slime" then
damage = 3 - armor
if damage < 0 then
damage = 0
end
health = health - damage
elseif collidedEnemy == "ghost" then
damage = 5 - armor
if damage < 0 then
damage = 0
end
health = health - damage
elseif collidedEnemy == "zombie" then
damage = 10 - armor
if damage < 0 then
damage = 0
end
health = health - damage
elseif collidedEnemy == "skeleton" then
damage = 15 - armor
if damage < 0 then
damage = 0
end
health = health - damage
end
print(health) -- 92
Ok, I think you can sort of see how this might get a bit unsustainable. What if there’s a bunch of
enemies - do we just have to keep repeating the code for every single case? What if we’re taking damage
in another part of the code, like from environmental hazards? Do we have to copy it over there too?
What if I want to add something new, like a sound effect every time the player gets damaged? The
problem is that with every change, you have to hunt down the code, and it’s very easy to make a
mistake like forgetting to update one piece of code or typing it incorrectly. Wouldn’t it be nice if we
could package repeated pieces of code into one reusable component? That’s where functions come in!
Here’s an example of a simple function that simply prints “Hello friend!”
sayHello = function()
print("Hello friend!")
end
Let’s break down the syntax. Functions are stored into variables, so we’re first creating this sayHello
45
variable. Then, to create a function we use the function keyword, which must be followed by a pair
of opening and closing parenthesis () . Then, like conditionals, we have an indented block of code that
will be run when our function is invoked. Again, like conditionals, we close off our function with the
end keyword. We call this piece of code the function definition. Similar to when we put our other data
types into variables, functions are simply a container, but this time for a piece of code. We have now
given a label to a piece of code that we can reuse whenever we want. We call this defining a function.
In order to use a function, we invoke it by simply writing out the name of the function followed by a
pair of parenthesis again () . We refer to this as calling a function. In the example above, I call the
sayHello function three times, which runs the code inside of the function three times. Note that the
function code does not run when it’s being defined - only when it is being called.
Right now functions are a bit rigid, since they can only run the same piece of code each time. What if
most of the code you’re running is the same, but with only slight differences? Instead of saying “Hello
friend!”, what if we want it to say “Hello [name]”, with the name being different each time? For that,
we can use something called parameters. Parameters allow us to pass in custom values into a function.
Let’s take a look at the following example:
sayHello = function(name)
print("Hello " .. name .. "!")
end
Let’s break down the syntax. In the function definition, we are now creating a parameter called name .
This name parameter operates exactly like a variable and can be used as such. However, what is the
value of name set to? Well, if you see where sayHello is called, we are putting a string between the
parentheses. This value is called the argument. When we call a function with a parameter, we can pass
in an argument, and the parameter variable will get set to the value of the argument. This way, even
though we’re calling the same function three times, we get three different results because the arguments
were different.
Functions can be created with any number of parameters. To add more parameters, you simply separate
them by commas into a list. Then, when you call the function, you pass in the arguments in a comma
separated list as well.
addThreeNumbers = function(a, b, c)
print(a + b + c)
end
addThreeNumbers(1, 2, 3) -- 6
addThreeNumbers(3, 3, 3) -- 9
addThreeNumbers(7, 2, 1) -- 10
Let’s go back to enemy collision example at the start of this chapter and wrap our damage logic into a
function.
46
health = 100
armor = 2
collidedEnemy = "zombie"
if collidedEnemy == "slime" then
damagePlayer(3) -- Call damagePlayer function
elseif collidedEnemy == "ghost" then
damagePlayer(5) -- Call damagePlayer function
elseif collidedEnemy == "zombie" then
damagePlayer(10) -- Call damagePlayer function
elseif collidedEnemy == "skeleton" then
damagePlayer(15) -- Call damagePlayer function
end
print(health) -- 92
If you look at where we’re checking the collidedEnemy , instead of a big block of code, we’re simply
calling this damagePlayer function we’ve created. A lot cleaner, right? That’s another one of the
benefits of using functions - not only do we just have one place where we need to update the code, but
also it becomes a lot more readable for you, as the developer, since we’re basically putting a descriptive
label on some code. Again, we don’t just want to write code that works, but also something that can
be easily understood and worked with, so making changes and updates to our game will be easier and
less prone to introducing new bugs.
As a side note, we’ve actually already seen a function before. The print statement that we’ve been
using is a function that’s provided by the Lua language. Here’s a list of the existing basic functions in
the documentation (though you probably won’t use most of these).
47
-- Syntactic sugar: saved you from typing a single "=" - wow!!!
function myFunction()
print("Hello!")
end
Seems small, but there’s actually a good reason for this. First is that the function keyword is the
first word in the line, so you can quickly tell what’s supposed to be a function by scanning down the
left side of your code. Second, and probably the main reason, is that it matches how functions are
typically declared in other languages. Here are a few examples:
void myFunction() {
printf("Hello in C!");
}
def my_function():
print("Hello in Python!")
function myFunction() {
console.log("Hello in Javascript!");
}
Going forward, I’ll be writing all functions in this style, but just keep in mind what’s really happening
under the hood.
Returning Values
These functions are pretty useful, but there’s one more trick up their sleeve that makes them even
more useful - the ability to return values. We can return a value from a function by putting a value or
variable after a special return statement.
emerald = 5
ruby = 3
sapphire = 6
function getTotalGemCount()
totalGems = emerald + ruby + sapphire
-- Return total gem count
return totalGems
end
48
Here’s a small example to illustrate why this might be useful. Let’s say we’re creating some sort of
puzzle game that has the player making some number of discrete moves to complete the puzzle. We
split the puzzles by difficulty based on the number of moves required to complete the puzzle, and we
display that difficulty in different places in our game UI (user interface), like the puzzle selection screen,
in the puzzle itself, or the results screen after you finished the puzzle. Here’s what that might look like.
-- Puzzle Scene
movesRequired = 30
if movesRequired <= 20 then
print("Current Difficulty: Easy")
elseif movesRequired <= 50 then
print("Current Difficulty: Medium")
else
print("Current Difficulty: Hard")
end
You can probably see that each scene is repeating the same code, except the wording of the printed
statement is slightly different ( Selected , Current , Completed ). As we’ve seen from previous examples,
this would be the perfect place to use a function. However, since it’s not exactly the same, how do
we share the logic between all the different scenes? Well, the only thing that is really dependent on
the moves required is the specific difficulty string ( Easy , Medium , Hard ), so we could just return the
difficulty string out of the function, and then append it to our specific scene-based string.
49
function getPuzzleDifficulty(moves)
if moves <= 20 then
return "Easy" -- 1 - 20 moves
elseif moves <= 50 then
return "Medium" -- 21 -- 50 moves
else
return "Hard" -- 51+ moves
end
end
-- Puzzle Scene
movesRequired = 30
print("Current Difficulty: " .. getPuzzleDifficulty(movesRequired))
The getPuzzleDifficulty call effectively replaces where it’s written to be the value that is calculated
and returned from the function. So, when we see this:
When the game is running, getPuzzleDifficulty runs and returns the string Medium , and it gets
converted to:
This is just a simple example, but it shows that returning values from functions can be quite versatile
because of how we can use the function in a variety of contexts. In just this example, you can see
how it might be useful to have this code all in one place if you ever want to change how difficulty gets
calculated, add a new difficulty level, add localization to have different languages, etc. Even if you
were only using a function in one place, it might make sense to wrap it into a function because you can
effectively name an operation for organizational purposes, and if you ever need to use it in another
place, you can just use your ready-made function.
Function Indentation
It’s typical to indent the contents of the function definition to clarify what is part of the function, but
the following is technically valid, albeit a lot less readable:
50
damagePlayer = function(amount)
damage = amount - armor
if damage < 0 then
damage = 0
end
health = health - damage
end
And you could do the following if you really wanted to, but I’d recommend against it. As you can see,
it’s literally going off the screen. Usually, you want to try to keep each line of code within a reasonable
length, so if you’re quickly reading your code, it’s easier to quickly understand. Also, keeping things on
separate lines allows your error messages to be more helpful.
damagePlayer = function(amount) damage = amount - armor if damage < 0 then damage = 0 end health = h
myFunctionB() -- Line 9
print("In between") -- Line 10
myFunctionA() -- Line 11
When we call myFunctionB on Line 9 , what we’re really doing is hopping over to Line 6 . Then, we
go sequentially as normal to Line 7 , but then, once we hit the end on Line 8 , we go to where the
function was called on Line 9 . Then, we go sequentially as normal to Line 10 and then to Line 11 .
However, since we’re hitting another function, we step into the definition on Line 2 . Then to Line 3 ,
and then finally, once we hit the end on Line 4 , we step out of the function and go back to Line 11 .
51
function myFunction()
print("Some super cool game logic")
return -- Hidden at the end of this function
end
Technically, with this empty return, we could force the function to return at any point in the function
then, right? That’s right, but with some qualifications. Take a look at this use of return .
function myFunction()
a = 100
return
a = a + 50 -- Error
end
Since we’re always exiting out of the function, the code after the return statement will never run.
Because of this, the compiler will complain with an error in this situation. So, when does an empty
return make sense? It makes sense in situations where the return isn’t always called, based on some
condition. Ring a bell?
Empty returns are always paired with some sort of conditional. Otherwise, there would be no need for
a return since it would always hit the implicit one at the end of a function. Why would we do this?
Here’s an example of where this might be useful. Take a look at this example where we have some sort
of platformer character controller. In it, we have a doubleJump function where we want to make the
player double jump, but we want to first check if 1. the player isn’t on the ground 2. the player isn’t
touching a wall, and 3. the player still has jumps remaining. It might look something like this:
yVelocityPlayer = 0
isGrounded = false
isTouchingWall = false
remainingJumps = 1
function doubleJump()
if not isGrounded then
if not isTouchingWall then
if remainingJumps > 0 then
remainingJumps = remainingJumps - 1
yVelocityPlayer = yVelocityPlayer - 5
end
end
end
end
It’s a bit unruly, and we’re doing a lot of indenting. Of course, we can combine all the conditions into
one single if check using and between all the conditions, but then our line gets really long. However,
what if we use some return statements instead?
52
function doubleJump()
if isGrounded then
return
end
if isTouchingWall then
return
end
remainingJumps = remainingJumps - 1
yVelocityPlayer = yVelocityPlayer - 5
end
We’re sort of flipping the logic around a bit. Instead of checking if something is not true, we’re checking
if it is, and if it is, then we’re exiting out of the function so that nothing after it gets run. Technically,
there’s nothing wrong with either approach, but it’s nice being able to glance at all the conditions and
quickly confirm that we’ve covered all our bases. We call this paradigm an early return, and it’s one
way an empty return statement might be useful.
With conditionals, I’ve mentioned that the convention is to put the whole condition on one line and
indent anything inside the condition. However, in the case of these early returns, to make it look
cleaner, it’s not uncommon for some people to put the whole early return block onto one line since it’ll
usually be pretty short.
function doubleJump()
if isGrounded then return end
if isTouchingWall then return end
if remainingJumps <= 0 then return end
remainingJumps = remainingJumps - 1
yVelocityPlayer = yVelocityPlayer - 5
end
As mentioned before, the spacing and indentation don’t affect the compilation of the program, so it’s
technically up to you. In sort of the opposite way, there might be cases where you want to take a
single line and split it up into multiple lines if it’s really long. In this last example, if we went with the
conditional approach, some people might format it like:
53
function doubleJump()
if not isGrounded and
not isTouchingWall and
remainingJumps > 0
then
remainingJumps = remainingJumps - 1
yVelocityPlayer = yVelocityPlayer - 5
end
end
-- Converts from cm to m
function getDimensionsInMeters(width, height)
return width/100, height/100
end
The second property is that the functions are something called “first-class functions”, which differs from
many other programming languages. This means that functions can essentially be treated the same way
that variables can. Here’s an example. I have a function getSum that sums up two values. However, I
want to be able to convert the units in that function. I’ll take in 3 arguments: two arguments are the
values in centimeters, and a third argument is a function that converts centimeters to some other unit.
I can then pass in different functions to get different type conversions. Notice that when I pass in the
function, I pass it in without any parenthesis. This way, I’m passing in the function definition itself,
not calling the function and getting the value it returns. This is kind of an advanced concept, so don’t
worry if that didn’t make that much sense. Not something you use very often, but very useful when
you need it.
54
function cmToM(input)
return input / 100
end
function cmToMM(input)
return input * 10
end
The third and last interesting property I want to point out is the ability to have a variable number
of arguments. So, you could pass in no values or as many values as you want into a function. You
do so by using ... as the function parameter. I won’t go into it, but you can read more about it in
“Programming in Lua” (www.lua.org/pil/5.2.html).
Function Errors
With the introduction of functions, we can now talk about a very useful part of error messages called
the “Stack Trace”. Once you start making more complex games, you’ll eventually have functions,
calling functions, calling other functions, and so on. The stack trace allows you to see the exact
order of functions that were called in order to produce the error. Let’s take a look at an example. In
this code below, we have a simple type error happening in functionOne . We have another function,
functionTwo , that calls functionOne , and then we have another function, functionThree , that calls
functionTwo . Then, we call functionThree directly.
function functionOne()
print("hello" + 100) -- Error!
end
function functionTwo()
functionOne()
end
function functionThree()
functionTwo()
end
functionThree()
If we run this code, we get the error message below. First, we get the addition error, but below it,
we get the stack traceback. The stack traceback goes in reverse chronological order and shows the
most recent call down to the earliest call. The error technically happened in this metamethod 'add'
function, but you can just ignore everything that starts with [C] since it’s not really relevant to us.
Underneath that, we see the Lua function functionOne that the error occurred in as well as the actual
line within the function that errored out. What’s more is that we can see the order of the calls and
lines inside functions functionTwo and functionThree that was called to produce the error. That last
55
line even shows that we called functionThree on line 13 at the top level of the file, which is not inside
a function, so it’s referred to as the “main chunk”.
In this scenario, knowing the stack trace isn’t all that useful since we only have one possible route
for the functions to call, but this becomes very useful when we call the same functions in a bunch of
different scenarios. Take a look at this next example. We have a simple function addAndPrint that
adds two numbers and prints them. However, we have two different functions calling that addAndPrint
function. safeFunction calls addAndPrint with two valid arguments. errorFunction looks like it
does the same thing, but if you look closely, the first argument is misspelled as numberone with a
lowercase o .
function addAndPrint(a, b)
print(a + b)
end
function safeFunction()
numberOne = 10
numberTwo = 20
addAndPrint(numberOne, numberTwo) -- No error!
end
function errorFunction()
numberOne = 15
numberTwo = 25
addAndPrint(numberone, numberTwo) -- Error!
end
safeFunction()
errorFunction()
The resulting error we get is below. Notice that in the error, there’s no mention of the offending variable
numberone - that’s because the value gets copied over and redefined as the function parameter a .
This makes it tricky to track down the bug. So, we know that a is nil , but we’re calling the function
from two places. Which one could it be? Well, we can easily just look through the call stack and find
that it happened when errorFunction called addAndPrint . Imagine this happening in a larger project
- without the stack trace, it would be extremely difficult to pinpoint where the error occurred if the
function gets called in a lot of different places. Now that we know what the possible problem functions
could be, we can step through each function call to see if anything stands out to us that could be
causing the error.
56
main.lua:2: in function 'addAndPrint'
main.lua:14: in function 'errorFunction'
main.lua:18: in main chunk
[C]: in ?
Another common error is just using a function when it’s nil . This can happen from misspelling the
function, but also just using the function before it’s defined. The function code doesn’t run until it
gets called, but the definition happens in the order that it is written. So, if you try calling a function
before it’s defined, it’ll error out because the function doesn’t exist yet. This time, you’ll get an
attempt to call a nil value error, telling you that you’re trying to call a function that doesn’t exist.
function myFunction()
print("Hello!")
end
57
Chapter 9: Scope
I mentioned in the Variables chapter that we can reassign values to variables.
However, this introduces a potential problem. What if we accidentally use a variable with the same
name in another location?
We’ve accidentally overridden our player position data and can’t get it back. Of course, we can just
make sure to write distinct variable names, like playerXPos and enemyXPos , but once your game gets
larger and you’re working with a big codebase with hundreds, even thousands of variables, it gets a bit
hard to keep track of.
The previous example was a bit simplistic, so let’s cover a more realistic example. Let’s say we have
some enemyAttackPlayer function. It takes an amount of damage to deal, along with a multiplier
to multiply the damage by. In the function, we calculate the amount of damage to do and store it
into a temporary variable damage and then subtract that from some player health variable. Creating
temporary variables like this is a perfectly normal and common thing to do in order to keep our code
organized, but you’ll see that it will come back to bite us later.
Let’s see what happens when we try to use our enemyAttackPlayer function. We store our enemy’s
base damage into a variable at the top called damage . This is where we run into an issue - the damage
variable in our function that we’re using to just temporarily store our calculation ends up overwriting
the base damage variable that has the same name.
58
-- Player Health
health = 100
While we’re calling enemyAttackPlayer(damage, 2) , we expect it to double our base damage of 10 and
deal 20 damage each time. However, unbeknownst to us, there’s an invisible overwrite happening in
this function, so we end up storing the doubled result into the base damage itself. Now, the second
time we call the function, it doubles our already doubled base damage and does 40 damage, and then
80 damage on the third call. Because we weren’t careful with our variable naming, we’ve introduced a
bug into our code.
This bug might be easy to spot since the function definition is right next to where we called it, but
if this was in a real game codebase, the function definition might in some other file completely. Not
only that, but as I’ve mentioned before, you’ll eventually have hundreds, potentially even thousands of
different variables. It’s a bit much to try and remember every single variable name that we’ve ever
used, and also, it’s a bit annoying having to try and think of a unique version of generic variable names
just for each individual use case. What if we have multiple enemies and types? I don’t really want to
be forced to write giantSkeletonEnemyHealth1 and giantSkeletonEnemyHealth2 .
Hmm. . . if only we had some way to tell Lua that we’re just creating a temporary variable - something
that is only contained a specific section of our code. It would be nice to have it so that this variable,
even if there happens to be another variable with the same name, would take precedence so if we modify
it, we’re only modifying this local variable instead.
As you probably expect, there does happen to be something like that. This concept is called the local
variable. A local variable is created by simply adding a local keyword anytime you create a new
variable. It operates pretty much the same as the normal variables we’ve been working with.
print(greeting) -- Hello!
However, one key difference is that a local variable is limited to the “block” that it’s in. You can think
of a block as anything that you indent the code for - functions, conditionals, etc. Just look to see
if there’s a end , else , or elseif after, and that means it’s a block. Once a block ends, the local
variables created inside of the block get discarded. So, if you try to access that local variable outside of
the block, it won’t exist, but anything inside the block can access the variable. We call this block the
scope of the variable, and we say that the local variable is scoped to that block.
59
-- Creating a "block"
if true then
-- Creating a *local* variable "health"
local health = 20
end
-- Discarded at the end of the block
This is in contrast to the variables that we’ve been working with so far. The variables we’ve been using
so far have technically been global variables. They’re called this because they can be accessed anywhere,
or globally. By default, all variables are global.
-- Creating a "block"
if true then
-- Creating a *global* variable "health"
health = 20
end
-- *Not* discarded at the end of the block
This discarding property is nice since we won’t have data leaking out of the context that it’s being used
in, but the more useful property is that if we happen to have a global variable and a local variable with
the same name, the local variable will always take precedence over the global variable. This means that
if we create a local variable and alter it, we’re given complete assurance that the only thing we could be
possibly changing is this local variable within this scope and nowhere else. Once we’re outside of that
scope, Lua will go back to using any existing global variables instead because the local one is discarded.
-- Creating a "block"
if true then
-- Creating a *local* variable "health"
local health = 0
end
-- *local* health is discarded at the end of the block
Now, to go back to our enemyAttackPlayer example. We can just make one small change, which is to
make our damage variable inside of the function into a local instead.
60
-- Player Health
health = 100
By changing the damage declaration in the function to be local, it isolates that variable to be within
the function scope, and guarantees that it will not affect anything outside of that scope. It’s like having
guard rails for your code. Every time you use a global variable, you’re introducing an invisible line of
dependency. By changing a global variable, it invites potential unintended side effects and raises the
likelihood of accidentally creating a new bug in your code.
I’ve been using global variables in all our examples so far because I haven’t introduced this concept yet,
but in reality, we want to be using local variables as much as possible to keep potential side effects
of using globals to a minimum. Global variables are therefore an exception, just as a matter of good
practice. Going forward, we’ll be declaring most things as locals in all our code examples. This is not
to say globals don’t have their place. The ability to be accessed everywhere is quite a powerful feature,
but we must be careful with how we use them.
61
-- Global variable "myVariable"
myVariable = "Hello!"
function myFunction(myVariable)
-- Altering local parameter "myVariable"
myVariable = 12345
print(myVariable) -- 12345
end
-- Call function
myFunction(myVariable)
File Scope
Note that if you’re working with multiple files, the file itself is a block, so local variables cannot be
accessed between files. We haven’t talked about working with multiple files yet, but it’s something to
keep in mind.
GRAVITY = 9.8
PLAYER_MAX_SPEED = 25
MAX_ENEMIES = 60
FINAL_LEVEL = 10
DAY_NIGHT_CYCLE_DURATION = 300
Another exception is working between different files. Beginners typically struggle a lot with figuring
how to share data between disconnected parts of code. A common example is with enemy pathfinding.
In order for the enemy to find the player, it needs to know the player position. There are ways to
organize your code so that you can get the player data over to where the enemy code is in an organized
way (e.g. command pattern, dependency injection, singleton pattern, etc.) However, those are more
advanced game programming patterns, so it’s fine to just stick the player position into a global PlayerX
and PlayerY variable so it can be accessed anywhere. If it’s a global variable that will have its value
frequently changed, it’s common to stylize it capitalized in CamelCase .
62
Local Functions
Note that you can also make functions local as well, since they’re really just variables in disguise. I’d
recommend doing that too, but since you’re usually creating fewer functions than variables, it usually
doesn’t cause as many issues having global functions.
-- ...this one!
local attack = function(amount, multiplier)
...
end
Nested Scope
What if you have multiple local variables with the same name? Which one takes precedence? In that
case, the local variable with the narrowest/innermost scope takes precedence. You can observe how
local variables behave with this code snippet here:
Multiple Assignment
As a bit of syntactic sugar, Lua allows you to assign multiple variables at once by separating the
variables and values into comma separated lists.
a, b, c = 1, 2, 3
print(a) -- 1
print(b) -- 2
print(c) -- 3
We can do the same thing with local variables. However, you just need to write local once. Then, all
variables in that declaration will be considered local
63
if true then
-- a, b, and c are all local
local a, b, c = 1, 2, 3
print(a) -- 1
print(b) -- 2
print(c) -- 3
end
print(a) -- nil
print(b) -- nil
print(c) -- nil
Scope Errors
A tricky thing about local variables versus global variables is how the order of how they’re declared can
yield different results. Take a look at the following example. We have a global variable declaredAbove
declared above myFunction , and another global variable declaredBelow declared below the func-
tion definition. We call myFunction after all of this, and because declaredBelow is defined before
myFunction is called, we see its value “below”.
declaredAbove = "above"
function myFunction()
print(declaredAbove) -- "above"
print(declaredBelow) -- "below"
end
declaredBelow = "below"
myFunction()
However, with locals, it works a little differently. We have the code setup exactly the same except that
the variables are local. However, print(declaredBelow) gives us nil . What’s going on with that?
The explanation is a bit complicated and has to do with the fact that global variables are actually an
exception. Typically, anything that gets referenced, even in a function definition, should be declared
before it’s referenced, with the exception of globals because they are the default. The function thinks
that declaredBelow is a global because it hasn’t seen that it was defined a local yet, and because the
global declaredBelow has nothing assigned to it, it returns nil .
function myFunction()
print(declaredAbove) -- "above"
print(declaredBelow) -- "nil"
end
myFunction()
64
If we aren’t certain of the value yet, we can first define it as an empty local variable first to let the
function definition know that it’s a local, and then set the value later. Very tricky and kind of confusing,
but important to keep in mind.
function myFunction()
print(declaredAbove) -- "above"
print(declaredBelow) -- "below"
end
myFunction()
65
Exercises: Functions
Now that we’ve covered scope, we’ve covered all the basics of how functions work. Below are a few
exercises to test your understanding of functions. Try using locals instead! Feel free to use your
coding environment of choice or the OneCompiler online Lua coding environment (onecompiler.com/lua).
Possible solutions to the exercises will be presented at the end of this section.
66
(Solution) Exercise 1:
Here’s one possible solution to the exercise:
local health = 20
healPlayer(50) -- 70
healPlayer(70) -- 100
(Solution) Exercise 2:
Here’s one possible solution to the exercise. Notice that you’re free to update any function arguments
like currentHealth and use them just like a normal local variable.
local health = 20
(Solution) Exercise 3:
Here’s one possible solution to the exercise:
67
local function getWeaponStats(weaponName)
if weaponName == "Sword" then
return 10, 1, "melee"
elseif weaponName == "Bow" then
return 5, 3, "ranged"
end
end
68
Chapter 10: Tables
Imagine this scenario - you have a bunch of enemies in the game and need some way to keep track
of their health. What do you do? Well, with what we know so far, you might think to create some
variables for it.
local enemy1Health = 30
local enemy2Health = 30
local enemy3Health = 20
local enemy4Health = 20
local enemy5Health = 50
Sure - but you might have 100 enemies. Should we be expected to create 100 different variables? What
if the number of enemies change?
Let’s consider another scenario. You have an inventory and the name of the currently selected item
stored as a string. You want to get how many of that item is stored in your inventory, so how would
you do it? Well, again, with we know so far, you might think to create a bunch of “if” conditions to
return different variables based on the string.
This works for now, but what if you had 100 different items? The work required to add additional
items to the inventory system could potentially get very tedious, especially once you add more code to
do things with these items.
Introducing tables! A table is a special structure that can be used to store data in many different ways.
It contains things called key value pairs. You can think about it like a table you see in math or Excel.
The keys are on the left, and the values are on the right.
Key Value
"stone" 10
"dirt" 23
"iron" 5
You can create as many keys in a table as you want and you can set or request values corresponding to
a key. A table is a dynamic data structure, which means that it can grow or shrink as you need it to,
making it extremely adaptable. Tables are typically used in two main ways, which we’ll cover next.
69
Arrays
One way to use a table is for a list of values - something we call an array (also referred to as a list).
Imagine we’re keeping track of all the enemies currently on screen. That can be visualized as an array
like this.
Key 1 2 3 4 5
Value "zombie" "ghost" "ghost" "ghoul" "zombie"
It’s like one of those pill containers that has the days of the week on it. But, instead of indexing by
weekday, we index by number. So, the first space is index 1, the next space is index 2, and so on. We
can then put data into each one of these slots. The array is an extremely common data structure, as
organizing data into a numbered list is quite useful. In most other languages, the array is actually its
own thing, but in Lua, it’s just what we call a table whose keys are numbers starting from 1. However,
this is why you’ll typically see array-specific language used, like saying “index” instead of “key”. Note
that the key numbers must be in order with no gaps for it to be considered an array.
Here’s an example of how we would create a table in code.
local fruits = {}
I created a local variable, fruits , and assigned it to an empty table. We can create an empty table
using an open and closed curly brace {} . The line local fruits = {} can be read as “Create an
empty table. Assign it to the local variable fruits ”.
To put something into the table, we can select which key we want to set the value for by using the
square bracket syntax [] , writing the key inside of the square brackets, and then assigning it to a
value.
local fruits = {}
fruits[1] = "orange"
The line fruits[1] = "orange" can be read as “Set key 1 of table fruits to the value orange ”.
Or, since we’re using numbered keys, we can think about it like an array - “Set the index 1 of array
fruits to "Orange" ”. We can access the value that we stored by using the square bracket syntax
again and using our key.
local fruits = {}
fruits[1] = "orange"
print(fruits[1]) -- "orange"
print(fruits[2]) -- nil
The line fruits[1] can be read as “Give me the value at the key of 1 in the table fruits ”. Or, as
“Give me the value at index 1 of the array fruits ”. Notice that fruits[2] gives nil . Any key that
has not been set to any value will give nil , just like any uninitialized variable. Let’s put some more
values into this table.
70
local fruits = {}
fruits[1] = "orange"
fruits[2] = "apple"
fruits[3] = "banana"
print(fruits[2]) -- apple
print(fruits[4]) -- nil
Here, since I’m using numbered keys starting from 1, we can consider it an array. Visualized, this is
what the table looks like after being initialized:
Dictionaries
Another way to use tables is as something called a dictionary. Similar to arrays, dictionaries usually
exist as a separate data structure in other programming languages, which is why I’m talking about
it like it’s a separate thing (though in Lua, colloquially, an array is referred to as an array, while a
dictionary is just referred to as a table). Think of a website. If you know the URL (the key), you can
go directly to the website (the value). Likewise, if you know the key, you can look it up in the table to
get the value.
local inventory = {}
inventory["stone"] = 10
print(inventory["stone"]) -- 10
Here, I’m creating a table called inventory and setting a stone key to the value of 10 . By
using the stone key on the table again, I’m able to retrieve the value I stored in there. The line
inventory["stone"] = 10 can be read as “In the inventory table, set the value of key stone to 10 ”.
By adding a few more elements to inventory , we can begin to see how this table can serve as an
efficient representation of an actual inventory. Any key that doesn’t have a value and returns nil just
means that the item doesn’t exist in our inventory. If it does return a value, then the number is how
many we have in our inventory.
71
local inventory = {}
inventory["stone"] = 10
inventory["dirt"] = 23
inventory["iron"] = 5
I assigned an empty table to inventory , used strings as the keys, and then set what I wanted to be the
number of items in the inventory. However, notice that the addition of a new item, “ink sac”, doesn’t
require me to create a new set of variables. I can just use the string as a new key and easily create
another entry. The table now looks like this.
"stone" 10
"dirt" 23
"iron" 5
"ink sac" 64
Let’s say we mine some stone. How would we increase the number in the inventory? Well, we can
reassign a dictionary value to the current value plus the increased amount.
local inventory = {}
inventory["stone"] = 10
inventory["stone"] = inventory["stone"] + 1
print(inventory["stone"]) -- 11
The line inventory["stone"] = inventory["stone"] + 1 can be read as “Get the value of key stone
in table inventory . Add one to it. Store the result back into key stone in table inventory ”.
Table Initialization
Since tables are so commonly used, they include some additional miscellaneous features. The first thing
is a useful piece of syntactic sugar. We can initialize tables with data already in them by putting values
in between the curly braces {} when we initialize the table variable.
To initialize an array, we just put the elements into a comma separated list, and the keys will
automatically be set to the order that the elements appear in the list without us needing to specify it.
72
-- initializing an array
local fruits = {"orange", "banana", "apple"}
print(fruits[1]) -- orange
print(fruits[2]) -- banana
print(fruits[3]) -- apple
To initialize a dictionary, we also make a comma separated list, but this time in the form of key = value .
For the key, it will automatically assume we’re using a string, so we don’t need to put quotation marks
around it.
-- initializing a dictionary
local inventory = {stone = 10, dirt = 23, iron = 5}
print(inventory["stone"]) -- 10
It’s common to split up the dictionary initialization into separate lines, with each key value pair on its
own line.
-- initializing a dictionary
local inventory = {
stone = 10,
dirt = 23,
iron = 5
}
print(inventory["stone"]) -- 10
Note, if you want to use numbers or any string with spaces as your key in a dictionary when you do
this kind of initialization, you have to wrap them in square brackets [] or else Lua will throw an error,
thinking that they’re meant to be values.
local numberToWord = {
[10] = "ten",
[20] = "twenty",
[30] = "thirty"
}
local wordToNumber = {
["one zero"] = 10,
["two zero"] = 20,
["three zero"] = 30
}
Dot Notation
Because table access with a string as a key is so common, there is a piece of syntactic sugar that allows
you to do it a different way.
73
local inventory = {
stone = 10
}
print(inventory["stone"]) -- 10
-- Using dot notation - equivalent call
print(inventory.stone) -- 10
The syntax is simply table.key . It takes whatever is after the dot, converts it into a string, and uses
that as a key. It only works for keys that are strings and have no spaces, but it’s pretty common to
have a key in that format. Be careful though, as you can easily mix this up with using a variable as a
key. Take a look at this example.
local inventory = {
stone = 10
}
If you use a word as the key inside of the square brackets without quotation marks, it gets interpreted
as a variable, not a string. So, if you use the variable stone to check the dictionary, it will search for
whatever is stored in the variable as the key, which is nil in this case and will give an error. However,
using the dot notation syntactic sugar, “stone” is interpreted directly as a string instead, so it looks up
the key “stone”. Confusing, I know, but just watch out for that.
local grid = {
{1 , 2 , 3 , 4 , 5 },
{6 , 7 , 8 , 9 , 10},
{11, 12, 13, 14, 15}
}
Length of an Array
A common operation we want to do is to find the length of a table (i.e. how many key value pairs
it has). As the size of a table is dynamic, we need some way to get the length of the array at any
given point. Luckily, we have the length operator, denoted by the hash or pound symbol ( # ). Simply
appending it to any array will return the length - however, this only works on arrays.
74
local array = {'a', 'b', 'c'}
print(#array) -- 3
As mentioned before, it can also be used to find the length of a string as well, which might be useful in
some situations.
Another common operation is adding things to the end of an array. With the ability to find the length
of an array using # , we can use the operator to automatically calculate the next index to place our
new element in.
Table Errors
Like with everything else, we’re always one spelling mistake away from accidentally using a value that’s
nil . And again, any non-initialized key in a table just returns nil .
Another common mistake is using the length operator # in order to find the length of a table that
isn’t an array. As a reminder, an array is any table that has keys that are sequentially indexed by
integers starting from 1. To find the length of a dictionary, we can use a loop, which is covered in the
next section.
local fruitCounts = {
orange = 3,
banana = 5,
apple = 1
}
print(#fruitCounts) -- 0 -> fruitCounts is a dictionary
A lot of other programming languages start arrays with 0 instead of 1. You can choose to do that, but
Lua table functions expect arrays to start at 1, so they won’t work as expected, including # .
75
local fruits = {}
fruits[0] = "orange" -- Start at 0
fruits[1] = "banana"
fruits[2] = "apple"
print(#fruits) -- 2 -> doesn't see "orange"
In addition, arrays need to have sequential indexes with valid values. So, if there happens to be a “hole”
in your array, with one or more of the values being nil , it might not be included in the length. I say
might not be, because the exact way it finds the length varies depending on how you set up your table,
so the best way to guarantee table operations work properly is to not have any gaps in your array.
local fruits = {}
fruits[1] = "orange"
fruits[2] = "banana"
fruits[5] = "apple" -- Skip index 3 and 4, go to 5
print(#fruits) -- 2 -> Not seeing "apple" in index 5
Another common mistake is thinking that tables get copied when assigned to another variable. However,
all variables that get assigned to a specific table get assigned to the exact same table. So, updating any
of the variables will update the table for all other variables pointing to the table.
Creating a “true” copy requires the use of loops, which we’ll cover in the next section.
Lastly, something you want to be careful of is the use of special keywords as keys. As we’ve learned,
there are special words that have specific meanings in Lua, like conditional keywords ( if , else ,
elseif ), function keywords ( function , end ), etc. There are some more we haven’t learned quite yet,
but if you want to set or retrieve any keys that happen to be those keywords, you must use the bracket
syntax with quotations (i.e. table["function"] ), or else Lua will interpret it as the keyword and give
an error.
76
local myTable = {}
myTable.elseif = 5 -- Error!
myTable["elseif"] = 5 -- Ok!
77
Exercises: Tables
Below are a few exercises to test your understanding of tables. Feel free to use your coding environment
of choice or the OneCompiler online Lua coding environment (onecompiler.com/lua). Possible solutions
to the exercises will be presented at the end of this section.
Key Value
"potion" 10
"key" 2
"arrows" 23
Write a function itemCount(inventory, item) that returns the item count if the item exists in the
table, and 0 otherwise. Test the function with “potion” and “bow” and print the results.
78
(Solution) Exercise 1:
Here’s one possible solution to the exercise:
enemyTypes[#enemyTypes + 1] = "orc"
print(enemyTypes[#enemyTypes]) -- orc
print(#enemyTypes) -- 5
(Solution) Exercise 2:
Here’s one possible solution to the exercise:
local playerStats = {
name = "Hero",
health = 100,
mana = 50
}
playerStats.health = 80
print("Health: " .. playerStats.health) -- 80
(Solution) Exercise 3:
Here’s one possible solution to the exercise:
local inventory = {
potion = 10,
key = 2,
arrows = 23
}
print(itemCount(inventory, "potion")) -- 10
print(itemCount(inventory, "bow")) -- 0
79
Chapter 11: Standard Libraries
The standard libraries are a set of functions built into the Lua language that help you do a bunch of
tasks. In fact, the print function that we’ve been using so far has been part of the basic standard
library. Two other useful functions part of the basic standard library are tonumber() and tostring() ,
which convert a string to a number, and vice versa.
The reason I waited until we talked about tables to bring up the standard libraries is that most of the
functions are stored inside of tables. These libraries are really just several global variables containing
tables that are automatically populated with functions whenever you run a Lua program. For example,
let’s take a look at a couple of functions from the “String” library.
String Library
We’re using this string.reverse() function to reverse a string, and this string.lower() function to
turn all the uppercase letters in a string to lowercase. If this dot . notation looks familiar, that’s
because it’s the same notation as indexing into a table. Really, string is a global variable that holds
a table, and each function is an entry in the table that can be referenced and used. It’s functionally no
different from something like:
Math Library
There are several standard libraries that you can use that have some useful functions - a full list is here
in the Lua documentation - but, by far the most useful standard library is the “Math” library. Here’s
an example of some of the functions:
80
-- Absolute value
print(math.abs(-3)) -- 3
-- Cosine and pi
print(math.cos(math.pi)) -- -1.0
-- Sine and pi
print(math.sin(math.pi/2)) -- 1.0
-- Ceiling
print(math.ceil(0.5)) -- 1
-- Floor
print(math.floor(0.05)) -- 0
-- Random
print(math.random(1, 10)) -- Random from 1-10
In game development, we use a lot of math. Luckily, most of the time, it’s usually some pretty simple
math. But because of this, the math library becomes very heavily used.
What’s cool about the libraries is that they’re no different from a regular table. So, that means we can
add our own functions to the library if we wanted to.
However, this also means we can accidentally overwrite the library completely.
print(math.abs(-5)) -- 5
print(math.abs(-5)) -- Error!
Table Library
Oftentimes, you’ll find yourself wanting to add something to a list (frequently at the end) or remove
something from a list. You can do this manually (as we saw before by doing myTable[#myTable+1] ), but
if you’re adding to the middle of an array, unless you want to overwrite the middle element, you need
to somehow move all the existing elements out of the way. Luckily Lua gives us some helpful functions
81
to do that for us in the form of table.insert and table.remove . The function table.insert takes
in 3 parameters - the table to insert into, the position to insert into, and the value to insert. Optionally,
if you want to add something at the end of a list, you can just use 2 parameters - the table to insert
into and the value to insert.
table.remove can be used in the same way, but instead of adding an element, it removes it. It takes 2
parameters - the table to remove from and the position to remove from. Optionally, you can just pass
in the table and it will remove the last element.
print(fruits[2]) -- banana
table.remove(fruits, 2) -- Remove the second element "banana"
print(fruits[2]) -- orange --> shifted into index 2
print(fruits[3]) -- kiwi
table.remove(fruits) -- Remove the last element "kiwi"
print(fruits[3]) -- nil --> it's gone!
82
Chapter 12: Loops
A common thing we would want to do when making games is a repeated action. For example, we have
some sort of wave system and we’re trying to spawn 5 enemies. Let’s take a made-up function called
spawnEnemy that spawns in an enemy. How would we spawn 5 enemies? Well, you might think to call
the function 5 times.
-- Spawn 5 enemies
spawnEnemy()
spawnEnemy()
spawnEnemy()
spawnEnemy()
spawnEnemy()
However, copy-pasting a function is very rigid and undynamic. What if you want to spawn 500 enemies
- would you copy-paste this 500 times? What if the number of enemies you need to spawn changes, as
they would in between waves? This is where the concept of loops comes in.
While Loop
The first type of loop I want to cover is the while loop. It’s the most basic form of the loop, which
involves simply checking if any condition is true before looping or exiting. Here’s a simple example of
looping from 1 to 5.
local count = 1
while count <= 5 do
print(count) -- 1, 2, 3, 4, 5
count = count + 1
end
The loop can be read as “while count is less than or equal to 5, run the code inside the loop”. You
can see that it takes the format while followed by some boolean condition, similar to a conditional.
Everything inside of the while loop block will run, and then, it will loop back to the top and check if
the condition is still true. If so, then it will keep running. If not, then it will exit out of the loop. We
increment our count variable by one each time so eventually it reaches 5. By simply changing the
condition count <= 5 to a different number, we can easily alter the number of times we run the code.
The while loop works well, but comes with a few issues. First is that it’s easy to either mess up our
condition or forget our incrementation logic. The consequence of this is that we might loop indefinitely,
which will cause your program to freeze. We call this an infinite loop. Also, the creation of the while
loop is a bit wordy. Because of this, there is another type of loop, called the for loop, that is designed
for these sorts of numerical loops.
For Loop
Here’s an example of a for loop. This loop runs five times.
for i=1,5 do
print(i)
end
83
To understand what’s happening, let’s write the equivalent while loop.
local i = 1
while i <= 5 do
print(i)
i = i + 1
end
As you can see, there’s a few hidden things that are happening in this for loop. When we write
for i=1,5 do , what it’s first doing is creating a local variable i and setting it to the first number,
1 . Then, there’s an invisible i = i + 1 that is being done at the end of the loop block, because
incrementing by one is very common. Then, it’s doing a check to see if i <= 5 , which is the second
number in the for loop declaration.
Here’s an example to show more clearly what’s happening. i is being set to 1, then it’s being printed.
Then, we add 1 to i , and then it runs the print code again. This repeats a total of 5 times until i is
equal to 5 . Then, we exit out of the loop.
for i=1,5 do
print(i)
end
-- 1
-- 2
-- 3
-- 4
-- 5
print(i) -- nil
If we try printing i outside of the loop, we get nil , since the loop variable is local to the loop and
out of scope. Now, if we wanted to spawn 500 enemies or change the number of enemies to spawn
dynamically, we can easily do it like so.
for i=1,enemyCount do
spawnEnemy() -- Executed 500 times
end
The for loop, by default, increments the loop by 1 every iteration. However, you can use an optional
third parameter to increment by some other amount.
84
-- Increments by 2. Prints even numbers from 0 to 10
for i=0,10,2 do
print(i) -- 0, 2, 4, 6, 8, 10
end
So, with this knowledge, we can create a for loop that loops through an array.
-- Output:
-- orange
-- banana
-- apple
The loop index i goes from 1 to 3, which happens to be the keys/indexes of the array, so we can
directly index into the array using i with fruits[i] .
85
local inventory = {
stone = 10,
dirt = 23,
iron = 5
}
-- stone: 10
-- dirt: 23
-- iron: 5
This “for-each loop” can be read as “For each of the key-value pairs of inventory , put the key into
item and the value into count and run the code inside the loop. Exit when all key-value pairs have
been read.”
The for-each loop follows the following format. <key> and <value> are local variables that get defined,
with <key> being equal to the current key and <value> being equal to the value corresponding to
that key. The variables can be named anything you want - I’d recommend something descriptive as
always, though the names key and value , or k and v , are pretty common. <table> is simply the
table you wish to loop through, which must be passed into a special pairs function from the basic
standard library. It loops through the entire table, with the <key> and <value> being updated each
iteration until all keys/values are iterated through.
Earlier, I introduced the length operator # , and mentioned how it does not work for dictionaries.
However, we can use this for-each loop to find the length of any table manually by just adding up
the number of times we go through the loop. In this example, we’re not using the key and value
variables for anything, so I chose to denote them with an underscore ( _ ), which is a pretty common
convention.
local inventory = {
stone = 10,
dirt = 23,
iron = 5
}
local inventorySize = 0
for _, _ in pairs(inventory) do
inventorySize = inventorySize + 1
end
print(inventorySize) -- 3
86
The thing about the for-each loop is that it does not guarantee any order. You can try this out by
running it multiple times - it gives potentially different orders each time. So, while pairs works for
arrays as well, if you want to guarantee order, you can use the special ipairs function instead of
pairs that only works with arrays. Unfortunately, there’s no way to guarantee an order for looping
through non-array tables, so you’ll have to design around that limitation somehow.
-- 1: apple
-- 2: banana
-- 3: orange
Break
In some situations, it would be useful to exit out of a loop early. In this case, you can use the special
break keyword. Let’s take a look at an example. Imagine you’re making a survival crafting game
where you can pick up items. Since there can be multiple items on the ground at the same time, you
use a loop to iterate over all the items on the ground, pick them up, and add them to your inventory.
However, what if while we’re picking items up, the inventory gets full? We don’t want to keep picking
things up, so with every item we can check if the inventory is full, and then break out of the loop to
stop the process.
87
-- Let's imagine we have some functions inventoryIsFull and addToInventory,
-- and that itemsOnGround is a populated array
for i=1, #itemsOnGround do
if inventoryIsFull() then
break -- Anything after this statement does not run and we finish the loop.
end
Now, there’s actually not anything functionally different from checking if the inventory is not full and
wrapping everything in the conditional block. However, there are two main benefits of writing it this
way. First, in terms of code organization, it’s a little easier to understand. Similar to the early function
returns we talked about earlier, if we’re quickly trying to understand the code, there’s less indentation
so we won’t get lost in the code as easily. Also, we can mentally check off that we’ve covered that
specific case of the player inventory being full. Second, there is a bit of a performance improvement,
since we don’t have to continue to check for any items on the ground once the inventory is full, which
would be redundant since we already know that the inventory is full. That probably doesn’t matter
that much in this case, but in some contexts, like running a path-finding algorithm to find a route from
an enemy to the player, each step in the loop might take a long time, so exiting early might help your
game not stutter or lag.
-- Alternative approach
for i=1, #itemsOnGround do
if not inventoryIsFull() then
local currentItem = itemsOnGround[i]
addToInventory(currentItem)
end
end
Note that the break statement must be the last statement of a block - the same as the return
statement. As a refresher, a block is whenever you indent your code, which is any conditional, loop, or
function. Just look to see if there’s a end , else , or elseif after the break statement, and if there
is, you’re good :)
88
we can use an external variable to keep track of progress, create more complex incrementation logic,
and use non-numeric loop conditions.
Occasionally, you might want to do something repeatedly until an indeterminate condition is met,
which can’t really be achieved easily with the other loop methods. Here’s an example of using a while
loop to continually loop until we randomly generate the number 50 (which is a weird thing to do, but
just for the sake of example). Since we’re checking against a condition instead of a set number of loops,
a while loop makes more sense here than a for loop.
local randomNumber = 0
while randomNumber ~= 50 do
-- Returns a random number between 1-100
randomNumber = math.random(1,100)
print(randomNumber)
end
We can also check for non-numeric loop conditions, which a for loop cannot do.
Loop Errors
With loops, we introduce the opportunity for a lot of different errors, so I’ve split this section up into
several classes of errors. These are infinite loops, errors with numeric for loops, errors with generic for
loops, and errors with removing elements.
Infinite Loops Loops introduce an interesting problem that we haven’t encountered yet, which is
the possibility of getting stuck in an infinite loop. This is usually only a problem with while loops,
since if the condition isn’t met, the loop will never exit. If we’re stuck in an infinite loop, the program
will freeze up, so you definitely don’t want that. This is typically why I’d recommend using other kinds
of loops if you can help it.
local randomNumber = 0
-- randomNumber will never be 101, so this will run forever
while randomNumber ~= 101 do
-- Returns a random number between 1-100
randomNumber = math.random(1,100)
print(randomNumber)
end
Another spot where we might find ourselves in an infinite loop is with the generic for. Since it’s looping
over the array until it runs out of elements, if we keep adding elements into the loop while it’s going, it
will never end.
89
local list = {1, 2, 3}
for i, v in ipairs(list) do
print("Index: " .. i .. " Value: " .. v)
-- Keep inserting elements - it never ends!
table.insert(list, i)
end
However, what’s interesting is that if we do this with a numeric for loop and set the end condition
to the size of the array with # , it actually doesn’t go on forever - it just stops at the original length.
This is because, with a numeric for loop, the final value is calculated once at the beginning of the loop -
it is not recalculated on each iteration.
Numeric For However, even with other loops, there are some mistakes to look out for. Typically,
they come from mistakes with the starting and ending values for the loop. In a lot of other languages,
arrays start at 0, but if you use a starting value of 0, the first value might be nil , which your code
might not be equipped to handle. Also, if you’re manually typing an end value, make sure it matches
the actual size of the string! I usually use the length operator # to guarantee it’s the same as the
length of the array. In this example, we’re taking the array value and concatenating it with a string,
which will error out if the value is nil . However, we’re starting with index 0 and ending with index 4,
which are both out of bounds of the array, so they will both return nil .
In this next example, we forget to create an index (e.g. i=1 ), giving a <name> expected near '#'
error. The <name> is just referring to creating a loop index variable.
The same error happens if we define it, but don’t set it to a starting value.
90
-- Missing "=1, " or equivalent!
for i, #fruits do
print(fruits[i] .. "s are yummy!")
end
Another thing to note is that you can use the same name as a variable that exists, but the loop is
technically creating a new variable scoped to the loop. Because of this, the loop’s variable will take
precedence for the duration of the loop, and then go out of scope, returning back to the original value
after it ends. If you want the loop to affect the variable, then use another name and assign the value
manually in the loop, or use a while loop.
Generic For Loops With generic for loops, an easy mistake to make is forgetting to write pairs
or ipairs . We get the error attempt to call a table value (for iterator 'for iterator') in this
next example. Sounds a bit cryptic, but this is because generic for loops use a concept called iterators
to work. Not really that important for us to know, but just remember if you see an error with “iterator”,
it’s probably related to one of your generic for loops.
Removing elements A common operation we might perform on a table with a loop is removing
elements from the table. However, there are issues we can easily run into with this that might not
be immediately obvious. Let’s take a look at this example - in it, we have a list of enemies and their
health. We want to remove any elements that are less than or equal to 0, since those are dead enemies.
However, this code crashes with an error attempt to compare nil with number . Why is that?
To figure it out, let’s think about a more simplified enemyHealth list with just two values {0, 8} .
91
When we’re at index 1, we see a health value equal to 0, so we remove it from the list. However,
remember that #enemyHealth is calculated once at the beginning of the loop. The loop is going to go
to index 2, but the size of the table actually shrunk, and now it looks like {8} , with no second index.
Therefore, enemyHealth[2] is nil , and we get an error trying to do a nil <= 0 comparison.
Ok, we can fix that by just checking for nil values before we do the check, right? However, interestingly,
we still have an issue. There are still some 0’s remaining inside of the list. Aren’t we checking for those
and removing them if we see them? How’s that possible?
To understand this, let’s think about another simplified example where the enemyHealth list only
consists of {0, 0} . At index 1, we remove the 0 there. However, like the last issue, the size of the
table shrinks. Now, the second 0 slides into the first position, and we actually skip over checking it
because we’re on index 2. Therefore, we skip checking the next element every time there is a 0, which
doesn’t properly handle any 0’s that are adjacent. How do we fix this? Well, there’s a really simple
answer - we loop backwards.
The concept behind this is pretty straightforward. Every time we remove an element, it shifts over all
the elements that come after it. However, if we loop backwards, all those elements that get shifted are
behind us, and we’ve already seen them. This eliminates both the need to check for nil values and
also guarantees we see each element.
92
Exercises: Loops
Below are a few exercises to test your understanding of conditionals. Feel free to use your coding
environment of choice or the OneCompiler online Lua coding environment (onecompiler.com/lua).
Possible solutions to the exercises will be presented at the end of this section.
local scores = {
level1 = 1230,
level2 = 3270,
level3 = 2890
}
Loop through the table and print the scores in the format “[level]: [score]”.
local numbers = { 3, 7, 2, 9, 5 }
Write a for loop to find and print the largest number in the table.
Hint: Create a temporary variable to keep track of the largest number.
Write a function combineInventories(inventory1, inventory2) that merges the two arrays into one
and returns it. Print the combined inventory.
93
(Solution) Exercise 1:
Here’s one possible solution to the exercise. As we’re looping over a dictionary, we must use pairs to
find all the elements.
local scores = {
level1 = 1230,
level2 = 3270,
level3 = 2890
}
(Solution) Exercise 2:
Here’s one possible solution to the exercise:
local numbers = { 3, 7, 2, 9, 5 }
local maxNumber = -1
for i=1, #numbers do
if numbers[i] > maxNumber then
maxNumber = numbers[i]
end
end
print(maxNumber) -- 9
We can also use a for-each loop with ipairs to loop through the array instead.
local numbers = { 3, 7, 2, 9, 5 }
local maxNumber = -1
for i, number in ipairs(numbers) do
if number > maxNumber then
maxNumber = number
end
end
print(maxNumber) -- 9
(Solution) Exercise 3:
Here’s one possible solution to the exercise. There are many solutions to this exercise, but I chose to
use ipairs to iterate over the second inventory and append it to the first one.
94
local inventory1 = { "potion", "sword", "key" }
local inventory2 = { "shield", "armor" }
95
Chapter 13: Interfacing With Your Game Environment
At this point, I’ve covered programming in pure Lua, which is a lot of what you’ll be working with
when making your game. However, there’s a lot more to games than just moving data around! Have
you noticed that I haven’t mentioned anything about how to draw something on the screen, play
sounds, or taking in player input? That’s because each game environment has its own way of handling
those things. The game environment then exposes that functionality to you, the developer, through a
bunch of different functions and variables. We call the set of these APIs, or Application Programming
Interfaces.
For example, let’s say you want to take some button input and do something if the player is pressing
the “Left” button. Here’s what that looks like in a couple different environments.
-- PICO 8
if btn(←) then
print("Left")
end
-- Playdate
if playdate.buttonJustPressed(playdate.kbuttonLeft) then
print("Left")
end
-- LÖVE
function love.keypressed(key, scancode, isrepeat)
if key == "left" then
print("Left")
end
end
-- Roblox
local UserInputService = game:GetService("UserInputService")
local function onInputEnded(inputObj, gameProcessedEvent)
if inputObj.UserInputType == Enum.UserInputType.Keyboard then
if inputObj.KeyCode.Name == Enum.KeyCode.Left then
print("Left")
end
end
end
UserInputService.InputEnded:Connect(onInputEnded)
In most cases, there will be a document or wiki you can look through to find everything you need,
which is referred to as the documentation. A simple “[game environment] documentation” Google
search should suffice. For the Playdate, you can find the documentation here at Inside Playdate.
In any case, the documentation for your specific game environment is your best friend. When I’m making
games for the Playdate, I always have the documentation open on another screen and I constantly
reference it. When the Playdate SDK first came out, I sat down and read the entire documentation
from start to finish over the course of a few days, which might seem a bit extreme, but it was really
helpful as it showed me exactly what tools I had at my disposal. I’d recommend at least skimming
over it. A lot of times, the game environment will have pre-written functions available to use that
can do exactly what you need. I still discover some new things even after years of referencing the
96
documentation.
I want to quickly touch on the syntax for these APIs. Take a look at this Playdate API that takes a
path to a .png file and creates an image object.
Notice how there are dots ( . ) between playdate , graphics , image , and new . Now, where have we
seen that syntax before? As a refresher, that’s how you access table elements. So, that means that the
API functions and variables are really stored in a giant table. The new function is stored in the image
table, that’s stored in the graphics table, that’s stored in the playdate table. It’s pretty much the
same thing we’re doing with the standard libraries. You can confirm this by printing out the contents
in your Playdate development environment.
97
Chapter 14: The Update Loop
Up until this point, I’ve mentioned all the basics for how to actually write code and the syntax, but
I’ve been quite vague about how everything really fits together. I’ve mentioned taking in player input
but abstracted it away for simplification purposes. You may be confused about how these concepts can
be used to create interactive games that have different animating sprites on the screen and changing
behavior when the code examples we’ve been working with only run once and finish. The secret is
something that most games rely on - the update loop.
For traditional animation, individual images are drawn one by one and shown in quick succession to
give the illusion of movement. The same thing is done for games, where game sprites are drawn to
the screen every frame which gives the illusion of animation and movement in the game. This is done
through the update loop, which is something that gets repeatedly called. A game running at 30 FPS
(frames per second) will have its update loop called 30 times a second, or once every 1/30th of a second.
The update loop first checks to see if the player has inputted anything since the last time it was called.
Then, it calls a function, called the update function, which holds all the logic for the game which can
respond to those player inputs and update what will be drawn on the next frame. Then, based on what
happened in the update loop, the next frame will get rendered to the screen, which just means actually
talking to the screen hardware and physically updating the pixels.
Each game system you’re working with will handle the input and screen rendering parts, but the update
function will be something you implement yourself. Each development environment will look a little
different, but typically, there will be an API you can call to see what the player inputted and to draw
things, and to make an update loop, you just need to create a function that has a special name that
the system will call automatically. The update loop is like a hardware sandwich with your program
in the middle, where the system talks to the buttons for you and turns it into something accessible
in code, and then when you call the APIs to draw things in the update function, the system handles
turning that into what appears physically on the screen.
Here’s an example of a made up game development environment called “System”. “System” requires
you to create a function System.Update that it will call every update loop. I wrote a small program
98
that simply draws “A Button Pressed!” to the screen every time the A button is pressed. Inside of the
update function, I define some custom logic that checks the AButtonPressed API, which will return
true if the player has pressed the A button in the last frame (made possible by the system in the first
step of the update loop). Then, we call the DrawText API to draw text to the screen (rendered by the
last step of the update loop).
Here’s an example of how you might have a circle that moves around the screen based on the player
input using the Playdate SDK API.
function playdate.update()
if playdate.isButtonPressed(playdate.kButtonLeft) then
playerX = playerX - playerSpeed
elseif playdate.isButtonPressed(playdate.kButtonRight) then
playerX = playerX + playerSpeed
elseif playdate.isButtonPressed(playdate.kButtonUp) then
playerY = playerY - playerSpeed
elseif playdate.isButtonPressed(playdate.kButtonDown) then
playerY = playerY + playerSpeed
end
playdate.graphics.drawCircleAtPoint(playerX, playerY)
end
As you can see, I’m initializing the player coordinates, and based on the player input I adjust the
coordinates. Since I’m checking the inputs on every update, this allows me to respond to user inputs as
they come in. Then, I’m drawing a circle at the player coordinates, which will make it appear as if
the player is controlling the circle. By adding complexity using conditionals and different variables to
keep track of a game state, you can create fully featured games built off of just this simple concept.
It’s quite confusing at first, but things will start to click as you go through different examples in your
coding environment of choice.
99
Chapter 15: Object-Oriented Programming
At this point, we’ve covered pretty much all the basics for the Lua programming language. Now that
we’ve covered the basics, I have an advanced topic that I want to touch on. The reason I specifically
wanted to talk about this topic is because this is something that trips up beginners a lot, and even
some experienced programmers who are new to Lua. It is by far the most common topic that I get
asked questions about.
There’s this concept called Object-Oriented Programming, also known as OOP, which is a commonly
used programming paradigm. It’s especially common in game development. The reason it’s confusing
is that the paradigm itself is a little confusing, along with the fact that in Lua specifically, the
implementation is quite strange.
To begin, let me first explain what a programming paradigm even is. A programming paradigm is simply
a way to think about and structure code. Imagine two different pizza restaurants. One restaurant
has each employee make the entire pizza from start to finish, from tossing the dough, to putting the
toppings on, to baking it in the oven. Another restaurant has each employee focus on one single task
(e.g. someone who only does toppings), and once they’re finished, they pass it off to the next person
who does their single task. Each organizational paradigm has its own pros and cons, but the end result
is the same: a finished pizza. In the same way, different programming paradigms might have their own
pros and cons, but the resulting program should roughly operate the same.
You don’t have to use OOP, but a lot of game development environments, like the Playdate and LÖVE,
use OOP as part of their development kit and APIs, so it’s likely you’ll be forced to interact with it in
some capacity.
To understand what Object-Oriented Programming is, we’re going to be building up from the basic
programming principles we know to understand why things are the way they are in OOP land. To
begin, let’s take a look at something that isn’t OOP. Think about a situation where we want to keep
track of a bunch of different enemies who each have their own position and health. How might we go
about doing that? One way might be to create multiple arrays to hold each property and have each
enemy be referenced by an index into the arrays, like the example below.
100
-- Data-Oriented approach
local enemyName = {}
local enemyX = {}
local enemyY = {}
local enemyHealth = {}
101
-- Create zombie at (0, 0) with health 10
local zombie = {
name = "zombie",
x = 0,
y = 0,
health = 10
}
Eventually, we’ll want to perform some operations on these enemies, like lowering their health if they
get damaged. We’ll create a function to do that.
local zombie = {
name = "zombie",
x = 0,
y = 0,
health = 10
}
damageEnemy(zombie, 3)
print(zombie.health) -- 7
At this point, it’s really easy to remember to use the damageEnemy function with enemies, but in the
future, we might end up with a lot of entities that can also get damaged, like other enemy types, the
player, destructible boxes, etc. The function and the object are sort of floating around as two separate
things. Some way to tie them together would be nice. A solution to this might be to just store the
function directly into the enemy table.
102
local function damageEnemy(enemy, damage)
enemy.health = enemy.health - damage
end
local zombie = {
name = "zombie",
x = 0,
y = 0,
health = 10,
damage = damageEnemy -- store function into key "damage"
}
As it turns out, this pattern is very common. We call these functions stored into objects as “methods”.
We refer to the data stored in the object as “properties” or “attributes”. Really, the crux of OOP is
that objects are just little bundles of data + functions that act on that data.
You might notice some redundancy, however, as we end up repeating ourselves by writing zombie
twice. Oh no, the horror - am I right? Because of this, our hero, syntactic sugar, comes to the rescue,
in the form of the colon ( : ) operator. The colon is the same as the dot ( . ), except, it takes whatever
table is before it, and passes it in as the first argument of the function following it. Here’s what that
looks like:
Seems small, but since this is such a common operation, it can be easy to accidentally forget to write
the object twice, so it reduces the likelihood for mistakes. Also, with it structured like this, it can be a
bit easier to understand what it’s doing since we can sort of read it as “Take zombie and damage it
by 3”. You have to be careful to not forget the colon, however, since if you forget it, you’re basically
forgetting the important object argument that holds all the data. This makes it so all the arguments
get shifted over, and you end up passing the next argument as the enemy object instead, which will
usually result in an error. Very common mistake!
103
Right now, it’s a bit tedious to remember to add all the properties and the damage method to the
enemy object every time we create a new enemy, so let’s turn the enemy creation into its own function.
We refer to this newEnemy function as a special one called the “constructor”, because it “constructs”
new objects. We’ll probably eventually add more enemy methods (moving the enemy, making the
enemy attack, etc.), so it would be nice to have them all organized into one place. Let’s create another
table to store all these functions.
104
Enemy = {}
function Enemy.moveBy(enemy, x, y)
enemy.x = enemy.x + x
enemy.y = enemy.y + y
end
We refer to this “Enemy” table as a “class”. Basically, it’s like a factory for these enemies. It holds all
the information needed to create an enemy, including all its methods and properties, and creates a new
one on request. If you noticed, I’ve made Enemy a global variable. This is just so we can access Enemy
from anywhere and use it to create new enemies. Also, we won’t ever be changing the Enemy class
variable while the game is running, so we won’t run into issues with global variables where we aren’t
sure what the value is. Of course, it can be local as well, but you’ll see commonly see classes defined
globally.
For the methods, we actually can’t use local for the declaration because we would be trying to define
the Enemy class table keys like a variable. If you remember, the way we’re using the function keyword
is just a bit of syntactic sugar, so it looks like:
105
-- This call is syntactic sugar for...
function Enemy.damage(enemy, damage)
enemy.health = enemy.health - damage
end
-- ...this declaration
Enemy.damage = function(enemy, damage)
enemy.health = enemy.health - damage
end
-- As another example, this doesn't make sense, since the key is not a variable
local Enemy.damage = 5
The Enemy variable is already defined as a global, and damage is not a variable - it’s just a key in the
Enemy table.
Hope you’re still following! It’s not the most intuitive - I know. Anyways, while we’re here, let’s look
at a few more examples to cement these concepts because unfortunately, there’s some more confusing
stuff after this point.
In this next example, we’re creating two different enemy objects from the Enemy . We call each of these
objects “instances” of class Enemy . I initialized them with the same values, but they are two separate
tables that each hold their own data. So, when I call damage on zombie1 , it does not affect zombie2
at all. It only touches zombie1 ’s health value. In the same way, when I call damage on zombie2 , it
only affects zombie2 ’s health value.
106
-- Initialize zombie1 as a zombie with 10 health
local zombie1 = Enemy.new("zombie", 0, 0, 10)
-- Initialize zombie2 also as a zombie with 10 health
local zombie2 = Enemy.new("zombie", 0, 0, 10)
-- ...this code
zombie1.health = zombie1.health - 2
zombie2.health = zombie2.health - 7
The reason it works that way is because the enemy object is passing itself into the damage method as
the first argument through the use of the colon operator. Each enemy object instance is just a table,
but crucially, it holds all of its data as well. Once an object is created, it is completely independent
from every other object created through the class constructor. The damage method is just a function
that has no information or data about any enemies on its own - it gets all data about the enemy
only from the first enemy argument. Therefore, it only operates on one enemy at a time. For the
zombie1:damage(2) call, that is zombie1 . For zombie2:damage(7) , that is zombie2 .
This independence is exactly what we want, because we want each object to have its own lifecycle,
much like objects in the real world. If a factory makes two cars and you damage one, the other one
wouldn’t suddenly get damaged as well, right?
Another thing to watch out for is declaring a variable outside of the class and using it inside of a
method. If we update the variable, since there’s only one copy, it will be shared across all instances of
the class. Let’s take a look at this simple example where we want to add some armor to the enemies.
We create a setArmor and getArmor method, but instead of modifying an existing property on the
enemy object, we just create a local variable at the top and use that instead. What’s important to
understand is that when we call the enemy constructor using Enemy.new , we are creating a new table
each time, which creates a whole copy of an enemy with all of its own properties. In the context of
local variables, that would be like if we made variables armor1 , armor2 , etc. Instead, what we’re
doing here is only creating one variable, so if it gets updated by one instance of the class, it will be the
same variable accessed by all other instances of the class.
107
local armor = 0 -- only one variable instance
Enemy = {}
function Enemy.new()
return {
setArmor = Enemy.setArmor,
getArmor = Enemy.getArmor
}
end
function Enemy.getArmor(enemy)
return armor
end
Instead, if there is any data that you want to be independent of other instances of the class, it must
be stored in the class instance itself. However, if it is something that should be shared across every
instance of the class, like a MAX_ARMOR value that is the same for every enemy, that’s fine to just have
it as one, top-level variable.
108
local MAX_ARMOR = 10
function Enemy.new()
return {
setArmor = Enemy.setArmor,
getArmor = Enemy.getArmor
}
end
function Enemy.getArmor(enemy)
return enemy.armor
end
enemy1:setArmor(5)
print(enemy1:getArmor()) -- 5, independent from other enemies
print(enemy2:getArmor()) -- nil, since we didn't initialize it yet
In this example, we’re not initializing armor in the constructor. That’s fine - we can actually create
new properties just by defining them on the object at any point since it’s really just a table which we’re
adding a new key to. Beware that since we didn’t initialize it though, if you try to access it, it will just
be nil , which might be unexpected if you don’t account for that. So, generally, it’s good to initialize
any properties you’ll use on an object to a reasonable default in the constructor.
Awesome - seems good so far, but can we make this any better? If you look closely, we still have a bit of
redundancy. Namely, in the constructor Enemy.new , we are manually setting all the functions/methods
on the created enemy object. What if we create a new method? We’ll have to remember to add it in
the constructor.
109
Also, take a look at the method definitions. The all have enemy as the first parameter since we always
need a reference to the specific class instance that called the method in order to get the relevant data
from it.
I wonder if there’s any way to make these things easier? Well, turns out there is, but it gets crazy.
Basically, a lot of other programming languages have Object-Oriented Programming principles built
directly into the language, but in Lua, there is no such luck. However, what it does is provide us tools
to basically hijack tables and give them custom behavior to essentially hack together our own version
of OOP. This is part of why we have the colon syntax - most other languages do not have a colon
operator, but it was added to Lua specifically to aid in making this frankenstein OOP implementation
work better.
Luckily, you won’t really need to understand how it all works, since in some cases, the game development
environment you’re working in will have an implementation created for you, and in other cases, you
can just copy someone else’s implementation off the internet. Or just don’t use OOP. But, it basically
comes down to this tool that allows us to alter the behavior of tables, which is the setmetatable
function.
Don’t worry if a lot of the metatable stuff goes over your head since it’s an advanced concept that you
will realistically never need to use, but just for your reference, I’ll go ahead and explain it anyways so
you can get an idea of how it works under the hood.
Metatables
Normally, if you try doing certain operations on tables, like adding them together, it gives you an error,
because it doesn’t really make sense to add two tables.
However, we can actually override this behavior by creating a “metatable”. This metatable is just a
table that can hold functions that you want to call instead when a certain operation is executed on
a table. To specify which operation, there are special key names you can use in the metatable. For
example, __add for the addition operation, __mult for the multiplication operation, __equal for the
== operation, etc. You can find a full list here. Then, to make a table use any of those “metamethods”,
you just call setmetatable() , with the first argument being the table, and the second argument being
the metatable. For example, let’s say we wanted the + operator to add the first elements of the tables.
We can do that by doing something like:
110
-- Create our metatable
local metatable = {}
-- Define our custom add metamethod
metatable.__add = function(a, b)
return a[1] + b[1]
end
Anyways, most of these metamethods aren’t that relevant for us in OOP land. However, there is one
that is exactly what we need, which is the __index operation. This controls what happens when we try
to access a key of a table using brackets [] or the dot operator . . Now, the __index metamethod
has a special behavior, where we can create a sort of proxy table. If we create another table as a proxy
and set it to __index in the metatable, whenever we try to access a key that does not exist in a table
with that metatable, instead of returning nil , it will first search the proxy table to see if the key exists
in that table and return that value.
local proxyTable = {
defaultName = "Squid"
}
local metatable = {}
metatable.__index = proxyTable
local emptyTable = {}
-- Empty table has no "defaultName" key!
print(emptyTable.defaultName) -- nil
setmetatable(emptyTable, metatable)
-- Grabs "defaultName" from the proxy table instead!
print(emptyTable.defaultName) -- "Squid"
The proxy table doesn’t have to be a separate table - we can make this even more compact by making
the metatable itself be the proxy table.
111
local metatable = {
defaultName = "Squid"
}
metatable.__index = metatable
local emptyTable = {}
print(emptyTable.defaultName) -- nil
setmetatable(emptyTable, metatable)
print(emptyTable.defaultName) -- "Squid"
Now, remember how we wished that we didn’t have to manually set the methods on enemy objects in
the constructor? Since we’re defining the methods in the class, we can just set the class itself as the
metatable for any object instances we create and set the __index property as the class as well, so that
any time we look for those methods on created objects it will look to our class instead.
Enemy = {}
-- Make Enemy instances look to Enemy table for missing keys
Enemy.__index = Enemy
function Enemy.moveBy(enemy, x, y)
enemy.x = enemy.x + x
enemy.y = enemy.y + y
end
112
That addresses our first concern. The next thing we noted was that we were repeatedly defining enemy
as the first argument for all the methods. For this, we can actually use the colon ( : ) operator as well.
We learned that the colon operator takes whatever table is before it and passes it as the first argument.
However, for functions, it has a slightly different behavior. It creates an invisible parameter called
self and sets it as the first argument. Here’s what that looks like:
local someTable = {}
As you can see, there isn’t anything particular about the self parameter created when using the colon
operator on a function. It operates like any other parameter. However, it’s really meant to be used in
conjunction with calling the function with the colon operator.
113
Enemy = {}
Enemy.__index = Enemy
As to why it’s called self , that’s because, in other programming languages, it is just a common way to
refer to instances of an object in a method. For example, when we call zombie:damage(5) , the zombie
is calling damage on itself.
Notice that the constructor is not using the colon operator, since we’re calling it directly. The constructor
is the one that is creating the new instance table so it can’t receive it as an argument. Now, if we
really wanted to, we could make the constructor operate more like a method by somehow automatically
creating an instance table and somehow passing it in as an argument. How do we do this? You guessed
it - by using another metatable for the class instead. I want to reiterate that all this metatable stuff is
not very important for you to understand (in fact, I never use metatables in my games), but having a
cursory understanding of what’s going on under the hood might help clear up some misconceptions
about using OOP in Lua.
The special metamethod we want to take advantage of is the __call operation, which allows you to
call a table like a function (e.g. myTable() ). Normally, you wouldn’t be able to “call” a table with
114
parentheses since it’s not a function, but we can hack that behavior in using a metatable and tell it
what function to run instead.
local metatable = {
-- Set "__call" metamethod
__call = function(self, ...)
-- Print all arguments
-- ("..." is a special variable argument syntax)
print(...)
end
}
local myTable = {}
-- Error: attempt to call a table value
myTable("Arg1", "Arg2", "Arg3")
-- But, if we set a metatable with "__call" defined, it will call that function
setmetatable(myTable, metatable)
myTable("Arg1", "Arg2", "Arg3") -- Arg1 Arg2 Arg3
Now, if we create another metatable with the __call field set to a function that does all our initialization
for us (creating the instance table, setting the instance metatable to the class, calling the new class
function with our new instance as an argument, and returning the instance), we can rewrite Enemy.new
to Enemy:new , and then use self inside the constructor, which will refer to our new instance. And
now, instead of calling Enemy.new , we can just call the Enemy class directly with our initialization
arguments.
115
local objectMetatable = {
-- Here, "self" refers to the "Enemy" table
__call = function(self, ...)
-- Create our new instance table
local instance = {}
-- Set instance metatable to "Enemy"
setmetatable(instance, self)
-- Call constructor, with "instance" being passed as "self"
self.new(instance, ...)
-- Return our new instance
return instance
end
}
Enemy = {}
Enemy.__index = Enemy
setmetatable(Enemy, objectMetatable)
While we’re here, why not just wrap up all the class initialization into its own function, so we can easily
create new classes anywhere?
116
function Class()
local newClass = {}
local objectMetatable = {
__call = function(self, ...)
local instance = {}
setmetatable(instance, self)
self.new(instance, ...)
return instance
end
}
newClass.__index = newClass
setmetatable(newClass, objectMetatable)
return newClass
end
In some Lua implementations, the class creation function takes a string as a class name and creates the
class variable for us. Normally, you can’t make variables from strings, but there is actually a table that
holds all our global variables called the “Global Table” ( _G ), so you can create a global variable from
a string. I wouldn’t really recommend doing this, but we’re hacking so much stuff anyway so we might
as well. Also, there are some OOP implementations that use init (as in initialization) or something
else instead of new for the constructor function name. This somewhat makes sense, since it’s the
metamethod that’s creating the new class instance, and the constructor is just doing the initialization
of the data values. We can change that here too.
117
-- Take in class name as a string
function Class(className)
local newClass = {}
local objectMetatable = {
__call = function(self, ...)
local instance = {}
setmetatable(instance, self)
-- Rename to "init"
self.init(instance, ...)
return instance
end
}
newClass.__index = newClass
setmetatable(newClass, objectMetatable)
-- Create a global variable from a string
_G[className] = newClass
end
Now, at this point, we have a pretty general OOP implementation. This looks very similar to what
the Playdate SDK uses for its OOP implementation. However, I am missing one big concept of
Object-Oriented Programming, called “Inheritance”. Inheritance is the concept of creating a class that
is a child of another class, where it copies all the properties and methods of the parent class and allows
you to extend its behavior without modifying the parent. It’s a pretty big topic, but I won’t cover it
here, since this section is already way longer than I intended on it being, but if you’re working with
OOP a lot it would probably be good to look into.
Common mistakes
As you’ve seen from the previous section, the implementation of OOP in Lua is quite complicated and
there are a lot of implementation details being hidden. Because of this, there are a lot of ways to mess
up when writing OOP code. Here are some of the common pitfalls that I see people falling into.
The first mistake is modifying the class instance of the class instance. Let’s take a look at this next
example. We’re creating a zombie enemy instance and we want to call the damage method on it.
However, frequently, people call the method on the class itself. This is incorrect - remember, the class
is like a factory and produces the enemies, which we call “instances” of the class. The instances are the
ones with the actual data, and we can have as many instances as we want. There’s just a singular class,
which we do not modify.
118
local zombieEnemy = Enemy("zombie", 200, 120, 10)
Another mistake is forgetting the class for the method declarations. Without the class, it’s just a
normal global function. Also, it’s missing the self argument, so self would just be nil . The
method function should be stored in the class table so that it can be accessed by the class instances via
the metamethod.
A similar mistake is just using a . instead of a : . What happens is you’re missing the self parameter.
Remember, there is nothing special about self - it’s just a normal parameter. The difference is that
an invisible one is created with the colon operator on a function. Without it, self is not defined, so it
does not exist.
119
-- Missing self parameter!
function Enemy.damage(amount)
-- "self" doesn't exist, so it's nil
self.health = self.health - amount
end
I’ve mentioned this one before, but I’ll repeat it again since it’s extremely common. It’s very easy to
forget the colon : when calling a class method. When you do this, you’re forgetting to pass in the
class instance in as an argument to the method. The method needs to get the data it uses from the
self argument since it has no other way to access the data. The class methods are all shared between
all instances of the class - they all point to the one you define in the class (through metatable magic).
So, the method function is dumb - it has no idea who is calling it and needs you to pass it in as an
argument.
Another set of mistakes I see revolves around the incorrect use of self . I think this stems from a lack
of understanding of what self is since it’s hidden from the parameters in the methods using the colon
operator. As I mentioned before, self is no different from any other parameter - it basically operates
the same as a local variable. The first one of these mistakes is adding another self to the argument
list when it’s already there.
120
-- We're duplicating the self!
function Enemy:damage(self, amount)
self.health = self.health - amount
end
If you use the colon operator, you should never need to write self into the parameter list for the
method, since it’s already there. If you write it again, what happens is that the first self value gets
overwritten by your second one. So, in the above example, the number you want to be in amount gets
put into self instead.
Another thing I see sometimes is people using self outside of a method. The variable self is just a
function property and the value of self is coming from somewhere - specifically, it’s just the class
instance. Since it’s just an argument, it gets scoped to the method it’s in, like a local variable. That
means it doesn’t exist outside of any of the class methods. When you want to access it outside of a
method, what you really want to do is to use the class instance itself.
The last thing I see in regards to self is actually the overuse of it. A lot of people end up creating new
properties when it could have just been a variable. This isn’t strictly a bad thing, but it’s generally not
good practice. Let’s take a look at this next example to see what I mean. Here, we have this damage
method that takes an amount and multiplier , and damages the enemy by the amount multiplied
by the multiplier. Just to organize our code, we first calculate the resulting multiplied value and
store it into self.calculatedValue . However, what this does is actually create a new property on the
zombieEnemy object.
I think people mistakenly believe that everything has to use self in class methods. However, it’s just
like any ordinary function. If you’re just making a temporary variable you’ll only use locally, you should
just make a local variable, or else the data will leak outside of the method. Also, performance-wise it’s
faster to use a local variable versus a property.
121
function Enemy:damage(amount, multiplier)
-- Just use a local variable like normal
local calculatedDamage = amount * multiplier
self.health = self.health - calculatedDamage
end
Conclusion
I put this book together to help complete beginners with no knowledge of programming understand the
Lua programming language. Regardless of the level you were at in the beginning, I hope I was able
to help you get a deeper understanding of Lua and programming in general. I understand that the
content of the book is a lot to take in, but I don’t expect everything to click at once. I’ve layered in
multiple levels of programming concepts to make this a resource that you can continuously revisit, so
as you learn more, you can come back to this book and get more insights.
Will you be able to make a game immediately with this knowledge? Most likely not, but my hope is
that you can now start to look at game development resources and tutorials and not be completely
lost. Game development can seem so unapproachable because many tutorials, whether consciously or
unconsciously, already expect that you have some knowledge of programming. I would be very happy if
I was able to close that gap in some way.
Please let me know if you feel like some parts are confusing and need more explanation, or if you feel
like there are additional things that should be in the book! Feel free to leave a review or ask questions
on the Itch community page: squidgod.itch.io/lua-for-game-development/community
If you’d like to submit feedback anonymously, you can use this feedback form instead:
https://forms.gle/g3ogBBbV2XWbQcdf6
You can also follow me on my socials for updates. Follow my YouTube where I make content for the
Playdate (programming in Lua!), or my Discord, where you can join my community and discuss game
development or ask questions! Also, if you follow me on Itch IO, you’ll get notified when I update the
book. Don’t hesitate to reach out to me directly as well - the best way is probably by email or through
Discord.
• Youtube: https://www.youtube.com/@SquidGodDev
• Discord: https://discord.gg/kDM8RU4aFt
• Itch IO: https://squidgod.itch.io
• Twitter: https://twitter.com/SquidGodDev
• Email: squidgoddev@gmail.com
Thank you so much for reading! Best of luck on your game dev journey!
122
Optional Exercises
Below are a set of optional exercises that will further test your knowledge of the topics covered. These
exercises are more challenging and a little different from the exercises that come after each chapter.
Each exercise will begin with a description of some imaginary game that you will be working on. There
will be a chunk of code already written, and your job is to fill in certain sections in the code to complete
a development task. Included as part of the code in the exercise is additional code, called a test suite,
that automatically tests the correctness of your code when you run it. You do not need to understand
or touch that part of the code. At the end of each exercise will be a possible solution to the exercise,
but if your code passes the test suite, then you can consider your code to be correct.
You can choose to copy and paste the code from the exercises, but, for your convenience, I have
shortlinks to OneCompiler that will have the code pre-filled out for you to use instead.
Operators Exercise
In this exercise, we have an imaginary puzzle game that has a score, some flags to pick up, a timer,
and a bomb. Based on the values of those variables, we want to check if the player has won the game.
The player has won the game if they have a high enough score OR they have collected all the flags,
unless they are out of time OR if they have picked up the bomb. To reiterate, here are the conditions.
The variable, playerWin , should be set to false by default, signifying that the player has not yet won.
However, it should be set to true if score is greater than or equal to REQUIRED_SCORE OR if flags
is greater than or equal to REQUIRED_FLAGS , signifying the player has won. However, if remainingTime
is less than or equal to 0 OR pickedUpBomb is true , then playerWin should be false , signifying
the player has lost.
Your job is to write some code to check against those conditions using operators and store if the player
has won ( true ) or not won ( false ) into the playerWin variable.
I’d recommend putting parenthesis around your operations (e.g. (score >= REQUIRED_SCORE) ) just to
make sure that the order of operations is clear and what you expect.
You only need to write within the == YOUR CODE HERE!!! == block using some combination of what we
learned to achieve the desired behavior. Running the code will put your code through a test suite that
checks different game conditions. If your code is correct, it should print RESULT: 6/6 tests passed at
the bottom. All the code for the test suite written below the == TEST == comment can be ignored.
Here’s a link to this exercise: onecompiler.com/lua/432a2uc3b
123
REQUIRED_SCORE = 1000
score = 1000
REQUIRED_FLAGS = 3
flags = 3
remainingTime = 1000
pickedUpBomb = false
function didPlayerWin()
-- == YOUR CODE HERE!!! ==
playerWin = false -- Store your result into *playerWin*
-- =======================
end
-- == TEST ==
local testCount, passedTestCount = 0, 0
function passedTest(expected)
didPlayerWin()
print("Expected result: " .. tostring(expected))
print("Actual result: " .. tostring(playerWin))
testCount = testCount + 1
if expected == playerWin then
passedTestCount = passedTestCount + 1
print("TEST PASSED")
else
print("TEST FAILED")
end
print("===================================")
end
print("Scenario 1: All conditions met")
passedTest(true)
print("Scenario 2: Score not high enough, but flags collected")
score = 300
passedTest(true)
print("Scenario 3: No flags collected, but score is high enough")
flags = 0
score = 1500
passedTest(true)
print("Scenario 4: Score not high enough, and not enough flags")
score = 200
flags = 2
passedTest(false)
print("Scenario 5: Conditions met, but bomb collected")
score = 1000
flags = 3
pickedUpBomb = true
passedTest(false)
print("Scenario 6: Conditions met, but out of time")
remainingTime = 0
pickedUpBomb = false
passedTest(false)
print("RESULT: " .. passedTestCount .. "/" .. testCount .. " tests passed")
124
(Solution) Operators Exercise
There are many ways to solve this exercise. Here are two examples of how you might go about doing it.
The first example puts everything on one line (which I’ve split into two since it was a little long), and
if you notice, we’re checking if we have enough time and if we don’t have the bomb. Another thing
to note is that we have parenthesis around the win condition checks - this is important since if those
parentheses weren’t there, it would return the wrong result. This is because of the order in which Lua
evaluates logical operators, so that’s why I mentioned that it’s generally good to use parenthesis if
you’re not sure about the order of operations. Not just for clarity, but to get the correct behavior.
The second example uses some variables instead, storing the win condition and lose conditions separately.
This time, for the loss conditions, we’re checking if we’re out of time, and if we do have the bomb.
Then, in the final check, we can check if we’re not hitting any of the loss conditions. I like this approach
a bit more since it’s easier to follow the logic.
function didPlayerWin()
-- == YOUR CODE HERE!!! ==
playerWin = ((score >= REQUIRED_SCORE) or (flags >= REQUIRED_FLAGS))
and (remainingTime > 0) and (not pickedUpBomb)
-- =======================
end
function didPlayerWin()
-- == YOUR CODE HERE!!! ==
winCondition = (score >= REQUIRED_SCORE) or (flags >= REQUIRED_FLAGS)
loseCondition = (remainingTime <= 0) or pickedUpBomb
playerWin = winCondition and not loseCondition
-- =======================
end
Something to pay attention to as well is the use of greater/less than equal to ( >= / <= ) versus greater/less
than ( > / < ) versus equal to ( == ). In game development, there are a lot of situations where the use
of these operators might have subtle differences. For example, checking if score > REQUIRED_SCORE
versus score >= REQUIRED_SCORE . In most cases, the score will probably be greater than the minimum
required amount, so there would be no difference, but what about the rare case where the score is
exactly the required amount? If you use score > REQUIRED_SCORE , it would return false, and the player
might be frustrated, thinking that they deserve to pass the level since they hit the required amount.
So, the difference between the two checks can have game design implications.
Another difference is how these checks can have an impact on protecting your code from bugs. Let’s
say there are only 3 flags in the level. In most cases, there would be no difference between checking if
flags == 3 or flags >= 3 . However, let’s say you have a rare bug that makes it so that the player
can accidentally pick up two flags from a single flag, so you sometimes end up with 4 or more flags.
If you move onto the next level only when flags == 3 , you might accidentally soft-lock the game,
preventing the player from progressing, but writing flags >= 3 is like insurance to cover those cases.
125
Conditionals Exercise
Our next exercise! In this coding exercise, I’ll be giving you a simple situation where we’re trying to
move a player around based on user input. However, to do that, we have to have a way to take user
input. Since we’re just using an online coding environment for simplicity, we can’t take in button input
or anything, so we’re simply putting the direction as a string into a direction variable.
In this exercise, we have a player position in the form of an (X, Y) coordinate. You can imagine the
player is on some sort of grid, like a board game. Those coordinates are stored in the playerX and
playerY variables. We want to take user input in the form of the strings left , right , up , and down
and update the player’s position accordingly. Typically for games, the coordinate system increases
down and to the right, unlike the math coordinates that you might be used to that increase up and to
the right. So, for the vertical axis, up should make playerY go down by one, and down should make
playerY go up by one. The horizontal axis is the same as those math coordinates, where left should
make playerX go down by one, and right should make the playerX go up by one. We read user
input to a direction variable, so you can check against that to see what direction the user typed in. In
this case, the user would just be you typing the directions into the terminal. The last condition is that
if the player types anything other than left , right , up , and down , we should print Invalid input .
To recap:
1. If direction is the string left , then update playerX to decrease by one
2. If direction is the string right , then update playerX to increase by one
3. If direction is the string up , then update playerY to decrease by one
4. If direction is the string down , then update playerY to increase by one
5. If direction is none of those, then print the string Invalid input
You only need to write within the == YOUR CODE HERE!!! == block using some combination of what we
learned to achieve the desired behavior. Running the code will put your code through a test suite that
checks different game conditions. If your code is correct, it should print RESULT: 7/7 tests passed at
the bottom. All the code for the test suite written below the == TEST == comment can be ignored.
A possible solution is listed in the next section, so try to figure it out before taking a look!
Here’s a link to this exercise: onecompiler.com/lua/432a59559
126
-- Player Coordinates
playerX = 0
playerY = 0
direction = "left"
-- =======================
end
-- == TEST ==
local passCount, totalCount = 0, 0
local function passedTest(expectedX, expectedY)
print("Checking direction: " .. direction)
print(string.format("Expected Position: (%d,%d)", expectedX, expectedY))
print(string.format("Actual Position: (%d,%d)", playerX, playerY))
if playerX == expectedX and playerY == expectedY then
passCount = passCount + 1
print("TEST PASSED")
else
print("TEST FAILED")
end
totalCount = totalCount + 1
print("=============================")
end
direction = "left"
movePlayer()
passedTest(-1, 0)
movePlayer()
passedTest(-2, 0)
direction = "down"
movePlayer()
passedTest(-2, 1)
direction = "down"
movePlayer()
passedTest(-2, 2)
direction = "invalid"
movePlayer()
passedTest(-2, 2)
direction = "up"
movePlayer()
passedTest(-2, 1)
direction = "right"
movePlayer()
passedTest(-1, 1)
print("RESULT: " .. passCount .. "/" .. totalCount .. " tests passed")
127
(Solution) Conditionals Exercise
Here’s a possible solution to the exercise. I want to reiterate that there is no singular solution to any of
these exercises. In fact, there is no singular solution to anything in programming. There are different
ways to write things that all have their pros and cons.
In this solution, I used if and elseif statements to capture the possible direction inputs, so that I
could use an else statement to capture everything that isn’t one of our 4 possible direction inputs.
128
Functions Exercise
This time, for the functions exercise, we’ll be implementing the logic for something similar to the simple
card game War, where you and an opponent both draw a card, and the higher card wins.
In this example, the player will be versing the spire. I have written some code that will draw a
random card (a number between 1 and 13) for both the player and the spire, prints what was drawn,
and stores them into the local variables playerCard and spireCard . The code is repeated three times
for a total of three rounds.
Your job is to create a single function that does the following:
1. Prints out who won (has the higher card), or if there’s a tie
2. Adds a point to playerPoints or spirePoints based on who wins, or no points added if there is
a tie
3. Prints out the player and spire points
The function should be defined in the YOUR FUNCTION HERE section, and you should call the function in
the YOUR FUNCTION CALL HERE sections.
A possible solution is listed in the next section, so try to figure it out before taking a look! This exercise
does not come with a test suite, so please check your own work :)
Here’s a link to this exercise: onecompiler.com/lua/432a3c5hm
129
local playerPoints = 0
local spirePoints = 0
-- ===========================
-- Round 1
playerCard, spireCard = drawCards()
-- == YOUR FUNCTION CALL HERE!!! ==
-- ===============================
-- Round 2
playerCard, spireCard = drawCards()
-- == YOUR FUNCTION CALL HERE!!! ==
-- ===============================
-- Round 3
playerCard, spireCard = drawCards()
-- == YOUR FUNCTION CALL HERE!!! ==
-- ===============================
130
local playerPoints = 0
local spirePoints = 0
-- Round 1
playerCard, spireCard = drawCards()
-- == YOUR FUNCTION CALL HERE!!! ==
calculateScore(playerCard, spireCard)
-- ===============================
-- Round 2
playerCard, spireCard = drawCards()
-- == YOUR FUNCTION CALL HERE!!! ==
calculateScore(playerCard, spireCard)
-- ===============================
-- Round 3
playerCard, spireCard = drawCards()
-- == YOUR FUNCTION CALL HERE!!! ==
calculateScore(playerCard, spireCard)
-- ===============================
131
Tables Exercise
In this exercise, we will be creating a simple inventory system. Your job is to fill out the two functions
addItem and useItem . Both take 3 arguments:
addItem should check to see if the item exists in inventory . If so, it should add quantity to the
existing quantity of the item in the inventory. If the item does not yet exist in the inventory, it should
add the item as a key to the table and set the value to quantity .
removeItem should reduce the quantity of the specified item in the inventory. If the quantity becomes
less than or equal to 0, remove the item by setting the item entry to nil in the inventory.
Running the code runs your code through a test suite, and you should see RESULT: 6/6 tests passed if
your code was written properly. All the code for the test suite written below the == TEST == comment
can be ignored.
Here’s a link to this exercise: onecompiler.com/lua/432a3qmzr
132
local inventory = {}
-- =======================
end
-- =======================
end
-- == TEST ==
local testCount = 0
local passCount = 0
local function checkInventoryTest(inventory, item, expected)
testCount = testCount + 1
print("Scenario " .. testCount .. ": " .. item)
local actual = inventory[item]
print("Expected: " .. tostring(expected) .. " | Actual: " .. tostring(actual))
if actual == expected then
passCount = passCount + 1
print("TEST PASSED")
else
print("TEST FAILED")
end
print("=============================")
end
addItem(inventory, "Arrow", 5)
checkInventoryTest(inventory, "Arrow", 5)
addItem(inventory, "Potion", 2)
checkInventoryTest(inventory, "Potion", 2)
addItem(inventory, "Potion", 3)
checkInventoryTest(inventory, "Potion", 5)
useItem(inventory, "Arrow", 4)
checkInventoryTest(inventory, "Arrow", 1)
useItem(inventory, "Arrow", 1)
checkInventoryTest(inventory, "Arrow", nil)
useItem(inventory, "Potion", 5)
checkInventoryTest(inventory, "Potion", nil)
print("RESULT: " .. passCount .. "/" .. testCount .. " tests passed")
133
nil is considered to be false for logical operations. However, if curCount is equal to any value, then
not curCount returns false. This is because values are considered to be true for logical operations.
Just a shorthand that you’ll commonly see.
Another thing to note is the early return I used in useItem . I didn’t actually test against this in the
test suite, but technically, if you somehow use useItem on an item that doesn’t exist, it will give an
error if it’s not handled in some way. I chose to just exit out of the function and do nothing.
134
Loops Exercise
In this exercise, your job is to spawn a bunch of enemies. You are given a function spawnEnemies
with the parameters enemies , which is an empty table, and count , which is the number of enemies
to spawn. The enemies table should be populated in order as an array, with index 1 being the first
enemy, and index count being the last enemy. Each individual enemy should be a table, with name ,
x , and y properties. name should be in the format “Enemy[index]” (e.g Enemy1, Enemy2, etc.). x
should be a random number between MIN_X and MAX_X , and y should be a random number between
MIN_Y and MAX_Y . Here’s an example of what the enemy table structure should look like:
key value
name "zombie1"
x 200
y 120
Executing the code runs your function through a test suite. If everything is written correctly, it
should print out RESULT: TEST PASS at the bottom. All the code for the test suite written below the
== TEST == comment can be ignored.
135
local MIN_X, MAX_X = 0, 400
local MIN_Y, MAX_Y = 0, 240
-- =======================
end
-- == TEST ==
local count = math.random(5, 10)
local enemies = {}
spawnEnemies(enemies, count)
local allPass, passCount = true, 0
if enemies[0] then
print("FAIL: enemy created at index 0 or less")
allPass = false
end
for i=1, #enemies do
local enemy = enemies[i]
if not enemy then
print("FAIL: enemy" .. i .. " not initialized")
allPass = false
break
end
local x, y, name = enemy.x, enemy.y, enemy.name
print(string.format("%s: (%d, %d)", name, x, y))
if x < MIN_X or x > MAX_X then
print("FAIL: X out of bounds")
allPass = false
elseif y < MIN_Y or y > MAX_Y then
print("FAIL: Y out of bounds")
allPass = false
elseif name ~= "Enemy" .. i then
print("FAIL: Name does not match: " .. "Enemy" .. i)
allPass = false
elseif i > count then
print("FAIL: Too many enemies")
allPass = false
break
else
print("PASS: All match")
end
end
if #enemies ~= count then
allPass = false
print("FAIL: Actual count: " .. #enemies .. " | Expected count: " .. count)
else
print("PASS: Actual count: " .. #enemies .. " | Expected count: " .. count)
end
print("============================================")
print("RESULT: TEST " .. (allPass and "PASS" or "FAIL"))
136
(Solution) Loops Exercise
For loops, it’s pretty common to have to use the index in some way. For this exercise, I made it so
you have to use it for the enemy name, which honestly doesn’t make much sense, but you can sort
of imagine if you have like a level select or something, and you can automatically populate the level
number with the loop index.
There are many ways to create tables - we could create an empty table and fill out the properties one
by one, but I chose to just do it all at once.
For inserting into the array, there are a few ways to go about it. table.insert(enemies, enemy) and
enemies[#enemies + 1] = enemy are functionally equivalent. Both approaches find the length of the
array, and then place the new element at the end of the array. However, enemies[i] = enemy simply
directly places the element in the i position. However, since we’re going in sequential order from i
to count because of the for loop, it ends up being the same as the other approaches in this situation.
137