0% found this document useful (0 votes)
2 views

luaForGameDevelopment

This document outlines a book on Lua programming, detailing its structure and content, including chapters on programming concepts, coding environment setup, variables, data types, operators, conditionals, and functions. Each chapter includes exercises for practical application of the concepts discussed. The book aims to provide a comprehensive guide for learning Lua programming.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
2 views

luaForGameDevelopment

This document outlines a book on Lua programming, detailing its structure and content, including chapters on programming concepts, coding environment setup, variables, data types, operators, conditionals, and functions. Each chapter includes exercises for practical application of the concepts discussed. The book aims to provide a comprehensive guide for learning Lua programming.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 137

Contents

What This Book Is and Why I Wrote It 6

Chapter 1: What is Lua? 7


What is a Computer? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
What is a Modern Computer? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
What is a Programming Language? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
What is Lua? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13

Chapter 2: Setting Up Your Coding Environment 15


Following Along with Code Examples and Exercises . . . . . . . . . . . . . . . . . . . . . . . 15
Official Documentation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16

Chapter 3: Writing Code vs Natural Language 17

Chapter 4: Variables 19
Additional Details (Variables) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
Case Sensitivity . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
Error Messages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
Naming Conventions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21

Chapter 5: Data Types 22


Numbers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
Strings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
Boolean . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
Nil . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
Additional Details (Data Types) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
Escape Characters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
String Errors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24

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

Chapter 10: Tables 69


Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
Dictionaries . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
Additional Details (Tables) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
Table Initialization . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
Dot Notation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
Tables Within Tables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74

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

Chapter 11: Standard Libraries 80


String Library . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
Math Library . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
Table Library . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81

Chapter 12: Loops 83


While Loop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
For Loop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
Looping Through an Array . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
Looping Through Dictionaries . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
Additional Details (Loops) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
Why “i” as a Loop Variable? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
Break . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
While Loop Use Cases . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
Loop Errors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89

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

Chapter 13: Interfacing With Your Game Environment 96

Chapter 14: The Update Loop 98

Chapter 15: Object-Oriented Programming 100


Metatables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110
Common mistakes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118

Conclusion 122

Optional Exercises 123


Operators Exercise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123
(Solution) Operators Exercise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125
Conditionals Exercise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126
(Solution) Conditionals Exercise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128
Functions Exercise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129

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.

Figure 1: A physical representation of a Turing Machine (Wikipedia)

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.

What is a Modern Computer?


For the Turing Machine, we had some theoretical idea of a machine, but what about in real life? The
modern day version of the computer is kind of like an advanced Turing Machine. Instead of a paper
tape of cells, we have cells in the form of electronic memory (e.g. the RAM on your computer). For the
actual operation of reading and processing the instructions, we have a CPU (central processing unit).

Figure 2: Diagram of a simple CPU (Wikipedia)

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.

Figure 4: Diagram of an AND Logic Gate (Wikipedia)

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.

Figure 6: A photo of some silicon wafers (Wikipedia)

Today, the transistors are so small they can be measured in lengths of literal atoms.

What is a Programming Language?


So, when the computer is running these instructions, what language is it running? If you’re using Lua,
does it run Lua? You may have heard of other programming languages like C, C++, Java, or Python.

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:

00101 100 00000010

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.

SUB AX, 0x2

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.

int square(int number) {


return number * 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];

// Ask the user to input some text


printf("Enter your first name: \n");

// Get and save the text


scanf("%s", name);

// Output the text


printf("Hello %s!", name);

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.

Figure 7: Language levels

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

Following Along with Code Examples and Exercises


There will be a lot of code examples throughout this book, and most of the examples you can copy
and paste directly into the online code editor and run it. I’d encourage doing so and playing around
with the code to get a better understanding of the concepts covered. One thing I want to note is
that if you’re reading the PDF version of this book, PDF readers sometimes don’t preserve special
characters or spacing properly, so copy and pasting can potentially result in some errors in the code.
If this happens, you can use the HTML version of the book (which can be viewed by dragging and
dropping the .html file onto your browser of choice), as copying from there works fine. You can also try
looking at the code examples and typing them yourself if you want to get some muscle memory in how
to type code, which is quite different from writing normally, as we’ll touch on in the next section.
Throughout the book there will be exercises to test your understanding of the concepts of the previous
chapter. These can be completed using an online Lua coding environment like OneCompiler and I have

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 health > 0 then


print("You're alive")
else
print("You're dead")
end

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:

local health=25 if health>0 then print("You're alive")else print("You're dead")end

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.

Additional Details (Variables)


In each chapter, I will have a section covering additional details about what we’ve covered. Usually,
these “Additional Details” sections will touch on common pitfalls, so if you’re ever running into any
issues, it might be good to come back to these sections to see if you haven’t missed anything. For

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.

introDialog = "You wake up in a dark forest..."


print(introDialog) -- You wake up in a dark forest...

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"

print(undefinedVariable) -- 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.

Additional Details (Data Types)


In this section, we’ll be covering how to use certain special characters in strings and common errors
that occur when using strings.

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.

print("\"") -- Prints "


print(""") -- Error!

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 .

print(true and true) -- true


print(true and false) -- false
print(false and true) -- false
print(false and false) -- false

Let’s think about our level pass requirement example again.

-- Check if we defeated enough enemies and store the result


enemiesDefeated = 10
enemyRequirement = 8
enoughEnemiesDefeated = enemiesDefeated >= enemyRequirement -- true

-- Check if we have enough keys and store the result


keys = 5
keysRequired = 3
enoughKeys = keys >= keysRequired -- true

-- Check if we defeated enough enemies AND we have enough keys


print(enoughEnemiesDefeated and enoughKeys) -- true

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 .

print(true or true) -- true


print(true or false) -- true
print(false or true) -- true
print(false or false) -- 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.

greeting = "Hello "


playerName = "SquidGod"
output = greeting .. playerName
print(output) -- Hello SquidGod

text = "Hello World"


print(#text) -- 11

You can also group operations and affect the order of operations using parentheses.

isAlive = true
hasEnoughKeys = false
killedEnoughEnemies = true

doorUnlocked = isAlive and (hasEnoughKeys or killedEnoughEnemies)

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

Additional Details (Operators)


In this section, we will be discussing the purpose of spaces around operators, why some boolean checks
are redundant, a surprising property of logical operators, common errors from using operators, and
what assignment operators are.

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.

a = 1+2 -- This is pretty common too


a=1+2 -- This, not so much (too cramped)
a = 1 + 2 -- ??????

Redundant Boolean Checks


Let’s consider the equality check below. In it, we’re checking if playerIsJumping is true.

playerIsJumping = true

-- Is the player jumping?


print(playerIsJumping == true) -- 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

-- Is the player jumping?


print(playerIsJumping) -- true

-- Is the player *not* jumping?


print(not playerIsJumping) -- false

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.

Valid Logical Operator Arguments


Take a look at the example below. What do you think the not operator will return? Try it yourself.

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.

doorUnlocked = isAlive and (hasEnoughKeys or killedEnoughEnemies or bossDefeated))


print(doorUnlocked)

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.

doorUnlocked = isAlive and (hasEnoughKeys or (killedEnoughEnemies or bossDefeated)


print(doorUnlocked)

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:

# Python code alert! Looks very similar to Lua, no?


a = 3
b = 5

# 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!!!!

For the Playdate, here’s a list of the available assignment operators.

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.

Exercise 1: Basic Arithmetic Operations


Calculate and print the following:
1. The sum of 15 and 10
2. The difference between 20 and 7
3. The product of 3 and 12
4. The result of dividing 50 by 5
5. The remainder when 17 is divided by 3

Exercise 2: Comparing Numbers


Define two numbers, a and b , and print whether:
1. a is greater than b
2. a is less than or equal to b
3. a is equal to b

Exercise 3: Logical Operators


Define the following variables:

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:

isRaining hasUmbrella Output


true true true
true false false
false true true
false false true

Optional Operators Exercise


An additional, more challenging exercise can be found at the end of the book under Optional Exercises:
Operators Exercise.

34
(Solution) Exercise 1:

-- The sum of 15 and 10


print(15 + 10)
-- The difference between 20 and 7
print(20 - 7)
-- The product of 3 and 12
print(3 * 12)
-- The result of dividing 50 by 5
print(50 / 5)
-- The remainder when 17 is divided by 3
print(17 % 3)

(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

if level <= 5 then


print("Reward: Wooden Sword")
end

if level > 5 and level <= 10 then


print("Reward: Iron Sword")
end

if level > 10 and level <= 15 then


print("Reward: Steel Sword")
end

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

if level <= 5 then


print("Reward: Wooden Sword")
elseif level <= 10 then
print("Reward: Iron Sword")
elseif level <= 15 then
print("Reward: Steel Sword")
end

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

if level <= 5 then


print("Reward: Wooden Sword")
elseif level <= 10 then
print("Reward: Iron Sword")
elseif level <= 15 then
print("Reward: Steel Sword")
else
print("Invalid level")
end

Additional Details (Conditionals)


In this section, we’ll be talking about the age-old question of tabs vs spaces, along with common errors
that occur when using conditionals.

Indent Size (Tabs vs Spaces)


In these examples, I’ve been using 4 spaces. However, this is not a hard and fast rule. If you’re not
aware, “tabs” vs “spaces” is actually quite a point of contention within the developer community, along
with the size of the indent (usually somewhere between 2 to 8 spaces). It seems silly, and it kind of is,
but it also kind of isn’t (see Indentation Style). The most important thing is to stay consistent within
your project.

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

-- Compiler sees the above code as this


if true then
print("Hello")
else
-- And thinks we're missing an "end" here
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.

-- main.lua:3: unexpected symbol near 'then'


if true then
print("Hello")
else then
print("Bye")
end

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.

Exercise 1: Door Unlock Condition


Define a boolean variable hasKey . If the player has a key, print “Door unlocked!”. Otherwise, print
“Key required!”.

Exercise 2: Game Character Health Check


Define a variable health for a game character. Write a script that prints:
• “Critical health!” if health is less than 20
• “Healthy!” if health is greater than or equal to 80
• “Moderate health” for any other value

Exercise 3: Finding the Largest Number


Define three number variables, x , y , and z , and print the value of the largest number. Use relational
operators ( > , < ) to compare the numbers. Be sure to try changing the numbers to test the accuracy
of your code.

Optional Conditionals Exercise


An additional, more challenging exercise can be found at the end of the book under Optional Exercises:
Conditionals Exercise.

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

if y > result then


result = y
end

if z > result then


result = z
end

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

sayHello() -- Hello friend!


sayHello() -- Hello friend!
sayHello() -- Hello friend!

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

sayHello("Bob") -- Hello Bob!


sayHello("Alice") -- Hello Alice!
sayHello("reader") -- Hello reader!

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

-- Create "damagePlayer" function


damagePlayer = function(amount)
damage = amount - armor
if damage < 0 then
damage = 0
end
health = health - damage
end

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).

Function Syntactic Sugar


I’ve introduced functions with the syntax of creating a variable and setting the function to that variable
because that’s what it is really doing, but in most cases, we use a bit of syntactic sugar to define a
function. Here’s what that looks like:

47
-- Syntactic sugar: saved you from typing a single "=" - wow!!!
function myFunction()
print("Hello!")
end

-- What is really happening


myFunction = function()
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

-- Print out returned value


print(getTotalGemCount()) -- 14

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 Select Screen


movesRequired = 30
if movesRequired <= 20 then
print("Selected Difficulty: Easy")
elseif movesRequired <= 50 then
print("Selected Difficulty: Medium")
else
print("Selected Difficulty: Hard")
end

-- 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

-- Puzzle Results Screen


movesRequired = 30
if movesRequired <= 20 then
print("Completed Difficulty: Easy")
elseif movesRequired <= 50 then
print("Completed Difficulty: Medium")
else
print("Completed 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 Select Screen


movesRequired = 30
print("Selected Difficulty: " .. getPuzzleDifficulty(movesRequired))

-- Puzzle Scene
movesRequired = 30
print("Current Difficulty: " .. getPuzzleDifficulty(movesRequired))

-- Puzzle Results Screen


movesRequired = 30
print("Completed 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:

print("Selected Difficulty: " .. getPuzzleDifficulty(30))

When the game is running, getPuzzleDifficulty runs and returns the string Medium , and it gets
converted to:

print("Selected Difficulty: " .. "Medium")

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.

Additional Details (Functions)


In this section, we will be covering why functions are formatted the way they are, how code execution
really works with functions, empty returns in functions, some niche function properties, and common
errors you’ll face when working with functions.

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

Code Flow with Functions


Previously, I said that code runs sequentially, line by line, but functions put a bit of a qualification on
that. You can imagine that when we call a function, we’re stepping into the function and jumping to
the start of the function definition to continue our code execution. Then, once we hit the end of a
function, then we step out of the function and jump back to the line where the function was called.

myFunctionA = function() -- Line 1


print("Function A 1") -- Line 2
print("Function A 2") -- Line 3
end -- Line 4

myFunctionB = function() -- Line 5


print("Function B 1") -- Line 6
print("Function B 2") -- Line 7
end -- Line 8

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 .

Empty Function Return


Remember how I just said that at the end of a function, we step out of it and jump back to the line
where the function was called? Technically, it’s actually an implicit return statement that does that.
It’s implicit, so you don’t need to write it, but it’s there. This return has no value associated with it,
so you can consider it a sort of empty return. It simply returns nil .

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

if remainingJumps <= 0 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

Extra Function Properties


I’ve covered the most important aspects of functions, but there are some other interesting properties
that I think are worth mentioning, as you will probably come across them. The first is that functions
can return multiple values using a comma-separated list. To capture them in variables, you use a
comma-separated list of variables.

-- Converts from cm to m
function getDimensionsInMeters(width, height)
return width/100, height/100
end

widthInMeters, heightInMeters = getDimensionsInMeters(250, 120)


print(widthInMeters) -- 2.5
print(heightInMeters) -- 1.2

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

function getSum(a, b, unitConverter)


return unitConverter(a) + unitConverter(b)
end

sumInMeters = getSum(10, 10, cmToM) -- 0.2


sumInMillimeters = getSum(10, 10, cmToMM) -- 200

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”.

lua: main.lua:2: attempt to add a 'string' with a 'number'


stack traceback:
[C]: in metamethod 'add'
main.lua:2: in function 'functionOne'
main.lua:6: in function 'functionTwo'
main.lua:10: in function 'functionThree'
main.lua:13: in main chunk
[C]: in ?

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.

lua: main.lua:2: attempt to perform arithmetic on a nil value (local 'a')


stack traceback:

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.

-- main.lua:1: attempt to call a nil value (global 'myFunction')


myFunction()

function myFunction()
print("Hello!")
end

57
Chapter 9: Scope
I mentioned in the Variables chapter that we can reassign values to variables.

-- Player is at (7, 15)


xPosition = 7
yPosition = 15

-- Move player to (8, 15)


xPosition = 8 -- Reassign xPosition

However, this introduces a potential problem. What if we accidentally use a variable with the same
name in another location?

-- Player is at (7, 15)


xPosition = 7
yPosition = 15

-- Move player to (8, 15)


xPosition = 8

-- ...far down in our file

-- Intending to set *Enemy* position


xPosition = 0 -- Overrode player x: 8
yPosition = 0 -- Overrode player y: 15

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.

function enemyAttackPlayer(amount, multiplier)


-- Using "damage" as a temporary variable
damage = amount * multiplier
health = health - damage
print("Damage: " .. damage)
end

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

-- Enemy Base Damage


damage = 10

function enemyAttackPlayer(amount, multiplier)


damage = amount * multiplier -- Using "damage" as a temporary variable
health = health - damage
print("Damage: " .. damage)
end

enemyAttackPlayer(damage, 2) -- Expected: 20 | Reality: 20


enemyAttackPlayer(damage, 2) -- Expected: 20 | Reality: 40
enemyAttackPlayer(damage, 2) -- Expected: 20 | Reality: 80

print("Health: " .. health) -- Expected: 40 | Reality: -40

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.

local greeting = "Hello!"

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

print(health) -- "nil" -> It doesn't exist!

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

print(health) -- 20 --> It's still here!

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 *global* variable "health"


health = 100

-- 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

print(health) -- 100 --> Global variable untouched!

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

-- Enemy Base Damage


damage = 10

function enemyAttackPlayer(amount, multiplier)


local damage = amount * multiplier -- Changing this to local
health = health - damage -- Using "local" damage here
print("Damage: " .. damage) -- Using "local" damage here
end

-- Using "global" damage here


enemyAttackPlayer(damage, 2) -- Expected: 20 | Reality: 20
enemyAttackPlayer(damage, 2) -- Expected: 20 | Reality: 20
enemyAttackPlayer(damage, 2) -- Expected: 20 | Reality: 20

print("Health: " .. health) -- Expected: 40 | Reality: 40

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.

Additional Details (Scope)


In this section, we will be covering how function parameters are actually local, how locals are faster
than globals, how local variables work between files, when we can use globals, local functions, how
nested scopes work, multiple assignment, and common scope errors.

Local Function Parameters


There has been one other place where we’ve declared a variable, and that’s with function definitions.
When we create function parameters, we’re basically creating some new variables. How do we make
these parameters local? Well, actually, they’re already local! Function parameters are by default local.
We can check this property with a small example.

61
-- Global variable "myVariable"
myVariable = "Hello!"

function myFunction(myVariable)
-- Altering local parameter "myVariable"
myVariable = 12345
print(myVariable) -- 12345
end

-- Call function
myFunction(myVariable)

print(myVariable) -- Hello! --> Untouched

Local vs Global Performance


Game development differs from other types of programming because of how important performance
is. If your webpage takes an extra second to load, it’s a minor inconvenience. If your game stutters
for one second and the player dies because of it, they’ll pick up their pitchfork. Because of this, any
sort of marginal performance gain can be quite important. Another benefit of locals is that it is faster
to access than a global variable. We’re talking fractions of a second here, but added up across your
codebase, it can be quite impactful.

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.

Global Variable Exceptions


The exceptions for globals typically follow a rule, which are things that you never change. This
minimizes the drawback of the ambiguity of what value a global variable is, while maintaining the
benefit of being accessible everywhere. We call this a constant. Typically, you stylize a constant global
variable differently to make it very clear what it is and how you need to be careful with it. Usually
you’ll see all uppercase using snake_case. Here are some examples:

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 declaration is the same as...


local function attack(amount, multiplier)
...
end

-- ...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:

if true then -- Block 1


local a = 100
print(a) -- 100
if true then -- Block 2
print(a) -- 100, as set by the outer scope
local a = 200
print(a) -- 200, as it is now referring to the new "a"
end -- Block 2 ends
print(a) -- 100, as the Block 2 local variable gets discarded
end -- Block 1 ends
print(a) -- nil, as the Block 1 local variable gets discarded

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 .

local declaredAbove = "above"

function myFunction()
print(declaredAbove) -- "above"
print(declaredBelow) -- "nil"
end

local declaredBelow = "below"

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.

local declaredAbove = "above"


-- defer initialization (nil by default)
local declaredBelow

function myFunction()
print(declaredAbove) -- "above"
print(declaredBelow) -- "below"
end

declaredBelow = "below" -- set value of local here

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.

Exercise 1: Heal Player


Define a variable health and write a function healPlayer(healAmount) that:

• Adds healAmount to health


• If the result exceeds 100 (the maximum health), set health to 100
• Print the resulting health value
Call the function and test it with different health and heal amount values.

Exercise 2: Heal Player (function return)


Write a function healPlayer(currentHealth, healAmount) that:

• Adds healAmount to currentHealth


• If the result exceeds 100 (the maximum health), return 100
• Otherwise, return the new health value
• Print the resulting health value
Create a variable health and call healPlayer by passing in health and use the return value to
update health .
Test the function with different values for the health and heal amount.

Exercise 3: Weapon Stats (multiple return)


Write a function getWeaponStats(weaponName) that:
• Takes a string weaponName (e.g., “Sword”, “Bow”)
• Returns three values:
– damage: the base damage of the weapon
– range: the range of the weapon
– type: the type of weapon (e.g., “melee” or “ranged”)
You can come up with your own values for the weapons and associated stats. Test the function by
calling it with different weapon names and printing the results.
Hint: You can return multiple values from a function like: return value1, value2, value3

Optional Functions Exercise


An additional, more challenging exercise can be found at the end of the book under Optional Exercises:
Functions Exercise.

66
(Solution) Exercise 1:
Here’s one possible solution to the exercise:

local health = 20

local function healPlayer(healAmount)


health = health + healAmount
if health > 100 then
health = 100
end
print(health)
end

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

local function healPlayer(currentHealth, healAmount)


currentHealth = currentHealth + healAmount
if currentHealth > 100 then
currentHealth = 100
end
print(currentHealth)
return currentHealth
end

health = healPlayer(health, 50) -- 70


health = healPlayer(health, 70) -- 100

(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

local swordDmg, swordRange, swordType = getWeaponStats("Sword")


print(swordDmg, swordRange, swordType) -- 10 1 melee

local bowDmg, bowRange, bowType = getWeaponStats("Bow")


print(bowDmg, bowRange, bowType) -- 5 3 ranged

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.

local selectedItem = "stone"


local stoneCount, dirtCount, ironCount = 10, 23, 5

if selectedItem == "stone" then


print(stoneCount) -- 10
elseif selectedItem == "dirt" then
print(dirtCount) -- 23
elseif selectedItem == "iron" then
print(ironCount) -- 5
end

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:

"orange" "apple" "banana"


1 2 3
You can think about each key as basically its own variable, but a variable you can access by a number
or string instead.

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

print(inventory["dirt"]) -- 23: Player has 23 dirt blocks

print(inventory["ink sac"]) -- nil: Player has no ink sacs!


inventory["ink sac"] = 64 -- Add 64 ink sacs to the player's inventory
print(inventory["ink sac"]) -- 64: Player now has 64 ink sacs!

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 ”.

Additional Details (Tables)


Since tables are so commonly used in Lua, there are a lot of helpful features that come with it, along
with a few gotchas. In this section, we will be covering a bit of syntactic sugar with table initialization
and the dot notation, tables within tables, finding the length of a table, and common table errors.

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
}

print(inventory[stone]) -- error! stone *variable* is used, which is nil

print(inventory.stone) -- key "stone" is used


print(inventory["stone"]) -- equivalent statement

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.

Tables Within Tables


Another thing is that for table values, you can put pretty much anything in them, even functions and
other tables. For example, if you want to represent a list of lists, like a 2D-grid or a matrix for math, a
common method would be to use tables within tables (see matrices and multi-dimensional arrays for
more).

local grid = {
{1 , 2 , 3 , 4 , 5 },
{6 , 7 , 8 , 9 , 10},
{11, 12, 13, 14, 15}
}

local row, column = 2, 3


print(grid[row][column]) -- 8

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.

local greeting = "Hello World"


print(#greeting) -- 11

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.

local array = {'a', 'b', 'c'}


array[#array + 1] = 'd' -- #array + 1 == 4
print(array[4]) -- 'd'

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 .

local fruits = {"orange", "banana", "apple"}


print(fruits["Apple"]) -- nil, capitalized "A"
print(fruits.pear) -- nil, doesn't exist

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 fruits = {"orange", "banana", "apple"}


print(#fruits) -- 3 -> fruits is an array

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.

local fruitsOriginal = {"orange", "banana", "apple"}


-- Create "copy" by pointing to original
local fruitsCopy = fruitsOriginal
-- Change index 1 on "copy"
fruitsCopy[1] = "pear"
print(fruitsOriginal[1]) -- "pear" -> original gets updated!

Creating a “true” copy requires the use of loops, which we’ll cover in the next section.

local fruitsOriginal = {"orange", "banana", "apple"}


-- Create entirely separate table
local fruitsCopy = {}
-- Create true copy
for i=1, #fruitsOriginal do
fruitsCopy[i] = fruitsOriginal[i]
end
-- Change index 1 on true copy
fruitsCopy[1] = "pear"
print(fruitsOriginal[1]) -- "orange" -> original unchanged!

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!

Here’s a list of those keywords:

and break do else elseif end false


for function if in local nil not
or repeat return then true until while

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.

Exercise 1: Create and Access a Table (Array)


Create a table named enemyTypes with the values: “zombie”, “skeleton”, “slime”, “bat”. Print the 3rd
element.
Add a new “orc” element to the end of array using the # operator to calculate the next array index.
Print the new element and new array length.

Exercise 2: Create and Access a Table (Dictionary)


Create a table named playerStats with the following fields:
• name set to “Hero”
• health set to 100
• mana set to 50
Print the player’s name, health, and mana. Update the health to be 80, and print the updated health.

Exercise 3: Check If a Value Exists


Create a table named inventory with the following keys and values:

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.

Optional Tables Exercise


An additional, more challenging exercise can be found at the end of the book under Optional Exercises:
Tables Exercise.

78
(Solution) Exercise 1:
Here’s one possible solution to the exercise:

local enemyTypes = {"zombie", "skeleton", "slime", "bat"}


print(enemyTypes[3]) -- slime

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
}

print("Name: " .. playerStats.name) -- "Hero"


print("Health: " .. playerStats.health) -- 100
print("Mana: " .. playerStats.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
}

local function itemCount(inventory, item)


local count = inventory[item]
if not count then
count = 0
end
return count
end

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

local greeting = "Hello World!"


local reversedGreeting = string.reverse(greeting)
print(reversedGreeting) -- !dlroW olleH

local uppercase = "HELLO WORLD!"


local lowercase = string.lower(uppercase)
print(lowercase) -- hello world!

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:

string = {} -- Global table "string"


-- Some function that reverses a string
local function reverseStringFunction(inputString)
-- Some code to reverse a string
end
-- Store the function into the table
string.reverse = reverseStringFunction

local someString = "Some string"


local reversedString = string.reverse(someString)
-- Equivalent to...
local reversedString = reverseStringFunction(someString)

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.

-- Create custom math library function


-- Equivalent to math.clamp = function(value, min, max)...
function math.clamp(value, min, max)
if min > max then
min, max = max, min
end
return math.max(min, math.min(max, value))
end

local randomNumber = math.random(0, 20)


-- Clamp value to be between 0 and 10
print(math.clamp(randomNumber, 0, 10))

However, this also means we can accidentally overwrite the library completely.

print(math.abs(-5)) -- 5

-- Replace with empty table


math = {}

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.

local fruits = {'apple', 'banana', 'orange'}


table.insert(fruits, 2, 'kiwi') -- Add "kiwi" into index 2
print(fruits[2]) -- kiwi

table.insert(fruits, 'melon') -- Add "melon" to the end


print(fruits[5]) -- melon

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.

local fruits = {'apple', 'banana', 'orange', 'kiwi'}

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.

local enemyCount = 500

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

-- Increments by -1. Prints 10 to 1 backwards


for i=10,1,-1 do
print(i) -- 10, 9, 8, 7, 6, 5, 4, 3, 2, 1
end

Looping Through an Array


A common thing you would find yourself doing is going through an array and doing something with each
of the elements. For example, looping through a list of enemies to damage them all in an AoE attack.
To do that, we can create a loop that goes from an index of 1 to the length of the array. Remember, we
can find the length of an array using the length operator, denoted by the hash or pound symbol ( # ).

array = {"a", "b", "c"}


print(#array) -- 3

So, with this knowledge, we can create a for loop that loops through an array.

local fruits = {"orange", "banana", "apple"}


for i=1,#fruits do
print(fruits[i])
end

-- 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] .

Looping Through Dictionaries


We’ve covered looping through an array, but you will likely find yourself wanting to loop through a
dictionary. For example, looping through your inventory to display all the items inside of it in a UI.
We can’t use the same approach as an array because a dictionary isn’t indexed by sequential numbers
- it’s indexed by key in the form of a string. Luckily, we have something called the generic for loop,
also known commonly as the for-each loop. The other type of loop we learned is technically called the
numeric for loop, but people normally just call that a “for loop”. This loop finds the keys for you and
goes through them one by one. Here’s an example.

85
local inventory = {
stone = 10,
dirt = 23,
iron = 5
}

for item, count in pairs(inventory) do


print(item .. ": " .. count)
end

-- 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.”

-- <table> is equal to the table you want to loop through


for <key>, <value> in pairs(<table>) do
-- <key> is equal to the current key
-- <value> is equal to the associated value
end

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.

local fruits = {'apple', 'banana', 'orange'}

for i, fruit in ipairs(fruits) do


print(i .. ": " .. fruit)
end

-- 1: apple
-- 2: banana
-- 3: orange

Additional Details (Loops)


In this section, we’ll be covering why we use i as a loop variable, how to exit out of a loop prematurely,
when to use while loops, and common loop errors.

Why “i” as a Loop Variable?


The use of i as a variable name might seem contradictory to what I previously mentioned about
descriptive variable names. Loop variables are an exception - the scope for loop counters is typically
very small, in the context of a for loop the variable is pretty self-explanatory, and using i is extremely
ubiquitous. Common other names are x and y or row and col if working in a grid context, along
with j and k if using loops within loops (nested loops) since they come after i alphabetically.

-- Nested for loops


for i=1, 10 do
for j=1, 10 do
for k=1, 10 do
print(i * j * k)
end
end
end

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

-- If our inventory is not full, we can proceed to this line


local currentItem = itemsOnGround[i]
addToInventory(currentItem)
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 :)

for i=1, #collidedObjects do


if checkCollision(player, object) then
handleCollision(player, object)
break -- Exit loop after first collision

print("Collided!") -- ERROR! Line will never run, so Lua will complain


end
end

While Loop Use Cases


Right now, while loops seem like a less elegant for loop. However, the structure of the while loop lends
itself to some things that cannot be easily replicated in another loop format. Some examples are that

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.

local stoplight = "green"

while stoplight == "green" do


driveCar()
if pedestrianDetected() do
stoplight = "yellow"
end
end

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.

local list = {1, 2, 3}

-- #list == 3 -> calculated at the start!


for i=1, #list do
-- Only runs 3 times!
table.insert(list, i)
print(list[i])
end

print(#list) -- Now equal to 6!!

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 .

local fruits = {"apple", "banana", "orange"}


-- Starting with 0 and ending on 4 - should be 1 to 3!!
for i=0, 4 do
print(fruits[i] .. "s are yummy!") -- Error!
end

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.

-- Missing "i=1, " or equivalent!


for #fruits do
print(fruits[i] .. "s are yummy!")
end

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.

local fruits = {"apple", "banana", "orange"}


local index = 10
for index=1, #fruits do
-- Uses the loop's "index"
print(index) -- 1, 2, 3
end
-- Return to the unchanged outer "index" value
print(index) -- 10

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.

local fruits = {"apple", "banana", "orange"}


-- Missing ipairs()!
for i, fruit in fruits do
print(i .. ": " .. fruit)
end

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?

local enemyHealth = {10, 5, 7, 0, 0, 0, 0, 4, 8}

for i=1, #enemyHealth do


-- Error at this check!
if enemyHealth[i] <= 0 then
table.remove(enemyHealth, i)
end
end

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?

local enemyHealth = {10, 5, 7, 0, 0, 0, 0, 4, 8}

for i=1, #enemyHealth do


-- Check if enemyHealth[i] is not nil first
if enemyHealth[i] and enemyHealth[i] <= 0 then
table.remove(enemyHealth, i)
end
end

-- Still have 0's in the list - what's going on?


for i=1, #enemyHealth do
print(enemyHealth[i]) -- {10, 5, 7, 0, 0, 4, 8}
end

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.

local enemyHealth = {10, 5, 7, 0, 0, 0, 0, 4, 8}

-- Start at the end of the array, end at 1, and increment by -1


for i=#enemyHealth, 1, -1 do
if enemyHealth[i] <= 0 then
table.remove(enemyHealth, i)
end
end

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.

Exercise 1: Traverse Dictionary


Given a table of scores:

local scores = {
level1 = 1230,
level2 = 3270,
level3 = 2890
}

Loop through the table and print the scores in the format “[level]: [score]”.

Exercise 2: Find the Largest Number


Given an array of positive integers:

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.

Exercise 3: Combine Tables


Create two arrays:

local inventory1 = { "potion", "sword", "key" }


local inventory2 = { "shield", "armor" }

Write a function combineInventories(inventory1, inventory2) that merges the two arrays into one
and returns it. Print the combined inventory.

Optional Loops Exercise


An additional, more challenging exercise can be found at the end of the book under Optional Exercises:
Loops Exercise.

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
}

for level, score in pairs(scores) do


print(level .. ": " .. score)
end

(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" }

local function combineInventories(inventory1, inventory2)


for _, item in ipairs(inventory2) do
table.insert(inventory1, item)
end
return inventory1
end

local combinedInventory = combineInventories(inventory1, inventory2)


for i, item in ipairs(combinedInventory) do
print(i, item)
end

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.

local playerImage = playdate.graphics.image.new("images/player")

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.

printTable(playdate) -- Prints out all the different Playdate APIs


printTable(playdate.graphics) -- Prints out all the graphics APIs

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.

Figure 9: An Update Loop Running at 30 FPS

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).

-- Create special update function to be called by update loop


function System.Update()
-- Check if the A button was just pressed
if System.AButtonPressed() then
-- Draw text at the coordinates (200, 120)
System.DrawText("A Button Pressed!", 200, 120)
end
end

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.

local playerX, playerY = 200, 120 -- Center of the screen


local playerSpeed = 3

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 = {}

-- Create zombie at (0, 0) with health 10


enemyName[1] = "zombie"
enemyX[1] = 0
enemyY[1] = 0
enemyHealth[1] = 10

-- Create skeleton at (200, 120) with health 5


enemyName[2] = "skeleton"
enemyX[2] = 200
enemyY[2] = 120
enemyHealth[2] = 5

-- Create function to damage enemy associated with "index"


local function damageEnemy(index, amount)
enemyHealth[index] = enemyHealth[index] - amount
end

-- Get our skeleton enemy's health using its index (2)


print(enemyHealth[2]) -- 5
-- Damage our skeleton by 3
damageEnemy(2, 3)
print(enemyHealth[2]) -- 2

This would be something more akin to Data-Oriented Programming as opposed to Object-Oriented


Programming. This approach has its own benefits, namely that it is more efficient, so it would be good
in a context where you have a lot of enemies at once. However, there is nothing tying the different
arrays to each other. It’s up to the programmer to make sure that index 2 in enemyHealth is always
associated with index 2 in enemyName and so on.
In addition, as people who live in a physical world, we typically don’t think of things as indexes into
lists of data. Rather, the real world is made up of various objects that are independent from one another
and have their own properties. When you take a cup from your cupboard, you typically wouldn’t say
“Cup number 3 is red”, but rather “This cup is red”.
If we can somehow separate each of these enemies into their own separate objects, we can get information
about the enemy directly from the enemy object itself rather than having to keep track of an index.
This way, it might be easier to conceptualize and work with the code. In this way, OOP was born as a
way to try and make code more human-friendly.
Let’s think back to our loops exercise. In it, we created a bunch of different tables to represent different
enemies. This way, we can think about each table as a separate enemy object and they can all have
distinct properties.

101
-- Create zombie at (0, 0) with health 10
local zombie = {
name = "zombie",
x = 0,
y = 0,
health = 10
}

-- Create skeleton at (200, 120) with health 5


local skeleton = {
name = "skeleton"
x = 200,
y = 120,
health = 5
}

-- Get enemy 2's health - no index required


print(skeleton.health) -- 5

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 function damageEnemy(enemy, damage)


enemy.health = enemy.health - damage
end

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"
}

-- This is equivalent to...


zombie.damage(zombie, 3)
-- ...this call
damageEnemy(zombie, 3)

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:

-- This is equivalent to...


zombie:damage(3)
-- ...this call
zombie.damage(zombie, 3)

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!

-- Forgot the colon! This code is equivalent to...


zombie.damage(3)
-- ...this call, which calls...
zombie.damage(3, nil)
-- ...this function, which translates the above into...
local function damageEnemy(enemy, damage)
enemy.health = enemy.health - damage
end
--- ...this line, which results in an error. A number isn't a table!
3.health = 3.health - nil -- Error: malformed number near '3.h'

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.

local function damageEnemy(enemy, damage)


enemy.health = enemy.health - damage
end

local function newEnemy(name, x, y, health)


local enemy = {
name = name,
x = x,
y = y,
health = health,
damage = damageEnemy
}
return enemy
end

local zombie = newEnemy("zombie", 0, 0, 10)


zombie:damage(6)
print(zombie.health) -- 4

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 = {}

-- This is the same as Enemy.new = function(name, x, y, health)


function Enemy.new(name, x, y, health)
local enemy = {
name = name,
x = x,
y = y,
health = health,
damage = Enemy.damage,
moveBy = Enemy.moveBy
}
return enemy
end

function Enemy.damage(enemy, damage)


enemy.health = enemy.health - damage
end

function Enemy.moveBy(enemy, x, y)
enemy.x = enemy.x + x
enemy.y = enemy.y + y
end

local zombie = Enemy.new("zombie", 0, 0, 10)


zombie:damage(6)
print(zombie.health) -- 4
zombie:moveBy(30, 30)
print(zombie.x) -- 30

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

-- When we do this, it looks like...


local function Enemy.damage(enemy, damage)
enemy.health = enemy.health - damage
end

-- ...this, which doesn't make sense since "Enemy" is already defined


local 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 call would look just like this


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)

-- Damage both zombies


zombie1:damage(2) -- Zombie 1 health is now equal to 8. Zombie 2 untouched.
zombie2:damage(7) -- Zombie 2 health is now equal to 3. Zombie 1 untouched.
-- The above code is equivalent to...

-- ...this code, which calls...


zombie1.damage(zombie1, 2)
zombie2.damage(zombie2, 7)

-- ...this function, which translates the above call to...


function Enemy.damage(enemy, damage)
enemy.health = enemy.health - damage
end

-- ...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.setArmor(enemy, amount)


armor = amount
end

function Enemy.getArmor(enemy)
return armor
end

local enemy1 = Enemy.new()


local enemy2 = Enemy.new()

-- Set armor value *local* variable, not an object property


enemy1:setArmor(5)
-- Enemy 1 sees that armor is set to 5, but...
print(enemy1:getArmor()) -- 5
-- ...so does Enemy 2
print(enemy2:getArmor()) -- 5

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.setArmor(enemy, amount)


if amount > MAX_ARMOR then
amount = MAX_ARMOR
end
enemy.armor = amount
end

function Enemy.getArmor(enemy)
return enemy.armor
end

local enemy1 = Enemy.new()


local enemy2 = Enemy.new()

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.

function Enemy.new(name, x, y, health)


local newEnemy = {
name = name,
x = x,
y = y,
health = health,
-- Manually setting damage method
damage = Enemy.damage,
-- Manually setting moveBy method
moveBy = Enemy.moveBy
}
return newEnemy
end

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.

-- First argument is "enemy"


function Enemy.damage(enemy, damage)
enemy.health = enemy.health - damage
end

-- First argument is also "enemy"


function Enemy.moveBy(enemy, x, y)
enemy.x = enemy.x + x
enemy.y = enemy.y + y
end

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.

local table1 = {3}


local table2 = {7}
print(table1 + table2) -- Error: attempt to perform arithmetic on a table value

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

local table1 = {3}


local table2 = {7}
-- Set our custom metatable on our table
setmetatable(table1, metatable)
-- Don't need to do it on table2, since Lua uses the first metamethod it sees
print(table1 + table2) -- 10 -> No more error!

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.new(name, x, y, health)


local enemy = {
name = name,
x = x,
y = y,
health = health
-- No need to define methods here, because of metatable
}
-- Set metatable of our new enemy to our Enemy class
setmetatable(enemy, Enemy)
return enemy
end

function Enemy.damage(enemy, damage)


enemy.health = enemy.health - damage
end

function Enemy.moveBy(enemy, x, y)
enemy.x = enemy.x + x
enemy.y = enemy.y + y
end

local zombie = Enemy.new("zombie", 0, 0, 10)


-- damage is nil in zombie - looks in proxy table instead
zombie:damage(6)
print(zombie.health) -- 4
-- moveBy is nil in zombie - looks in proxy table instead
zombie:moveBy(30, 30)
print(zombie.x) -- 30

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 = {}

-- This declaration is equivalent to...


function someTable:someFunction()
-- Using the invisible "self" argument
print(self)
end

-- ...this one, which looks like...


function someTable.someFunction(self)
print(self)
end

--- ... this declaration


someTable.someFunction = function(self)
print(self)
end

-- "Hello" gets passed in as the self argument, like normal


someTable.someFunction("Hello") -- "Hello"

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

function Enemy.new(name, x, y, health)


local enemy = {
name = name,
x = x,
y = y,
health = health
}
setmetatable(enemy, Enemy)
return enemy
end

-- Switch to using colon operator


function Enemy:damage(damage)
-- Change "enemy" references to "self"
-- "self" refers to "zombie" Enemy instance
self.health = self.health - damage
end

-- Switch to using colon operator


function Enemy:moveBy(x, y)
-- Change "enemy" reference to "self"
-- "self" refers to "zombie" Enemy instance
self.x = self.x + x
self.y = self.y + y
end

local zombie = Enemy.new("zombie", 0, 0, 10)


-- Pass "zombie" Enemy instance in as "self"
zombie:damage(6)
print(zombie.health) -- 4
-- Pass "zombie" Enemy instance in as "self"
zombie:moveBy(30, 30)
print(zombie.x) -- 30

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)

-- Switch to "Enemy:new" instead of "Enemy.new"


function Enemy:new(name, x, y, health)
-- "self" is "instance" from our __call metamethod
self.name = name
self.x = x
self.y = y
self.health = health
end

-- Call Enemy() instead of Enemy.new()


local zombie = Enemy("zombie", 0, 0, 10)
print(zombie.health) -- 10

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

-- Create new classes easily


Enemy = Class()

function Enemy:new(name, x, y, health)


self.name = name
self.x = x
self.y = y
self.health = health
end

local zombie = Enemy("zombie", 0, 0, 10)


print(zombie.health) -- 10

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

-- Creates new class "Enemy"


Class("Enemy")
-- Rename to "init"
function Enemy:init(name, x, y, health)
self.name = name
self.x = x
self.y = y
self.health = health
end

local zombie = Enemy("zombie", 0, 0, 10)


print(zombie.health) -- 10

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)

-- Incorrect! Enemy is the class and has no data


Enemy:damage(5)
-- Translates to this call
Enemy.damage(Enemy, 5)

-- Correct! You want to modify class instances instead


zombieEnemy:damage(5)
-- Translates to this call
zombieEnemy.damage(zombieEnemy, 5)
-- Which, because of metatables, is actually technically this
Enemy.damage(zombieEnemy, 5)

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.

-- This is just a normal global function


function damage(amount)
-- Also, "self" doesn't exist, so it's nil
self.health = self.health - amount
end

-- Correct way to write a method...


function Enemy:damage(amount)
self.health = self.health - amount
end

-- ...which is equivalent to this...


Enemy.damage = function(self, amount)
self.health = self.health - amount
end

-- ...which is equivalent to this


local enemyDamageFunction = function(self, amount)
self.health = self.health - amount
end
Enemy["damage"] = enemyDamageFunction

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

-- Correct way to write a method...


function Enemy:damage(amount)
self.health = self.health - amount
end

-- ...which is equivalent to this


function Enemy.damage(self, amount)
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.

local zombieEnemy = Enemy("zombie", 0, 0, 10)


-- Used dot (.) instead of colon (:)!!!
zombieEnemy.damage(5)
-- Equivalent to this call, since missing arguments are nil
zombieEnemy.damage(5, nil)
-- What you want instead is this
zombieEnemy.damage(zombieEnemy, 5)

-- Here, "self" is equal to "5" and "amount" is nil


function Enemy:damage(amount)
-- Error!
self.health = self.health - amount
end

-- Remember, the above is equivalent to this


function Enemy.damage(self, amount)
self.health = self.health - amount
end

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

-- Ends up looking like this


function Enemy.damage(self, 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.

local zombieEnemy = Enemy("zombie", 0, 0, 10)


-- "self" is nil! You can't use it outside of a method
print(self.health)
-- What you're looking to do is this:
print(zombieEnemy.health)

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.

function Enemy:damage(amount, multiplier)


-- Create a new property just to store a temporary value
self.calculatedDamage = amount * multiplier
self.health = self.health - self.calculatedDamage
end

local zombieEnemy = Enemy("zombie", 0, 0, 100)


print(zombieEnemy.calculatedDamage) -- nil -> doesn't exist
zombieEnemy:damage(5, 3) -- Damage by 5 * 3 = 15
print(zombieEnemy.calculatedDamage) -- 15 -> a new property created!

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"

local function movePlayer()


-- == YOUR CODE HERE!!! ==

-- =======================
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.

local function movePlayer(direction)


-- == YOUR CODE HERE!!! ==
if direction == "left" then
playerX = playerX - 1
elseif direction == "right" then
playerX = playerX + 1
elseif direction == "up" then
playerY = playerY - 1
elseif direction == "down" then
playerY = playerY + 1
else
print("Invalid input")
end
-- =======================
end

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

local function drawCards()


local minCard, maxCard = 1, 13
local playerCard = math.random(minCard, maxCard)
local spireCard = math.random(minCard, maxCard)
print("Player drew: " .. playerCard .. " | " .. "Spire drew: " .. spireCard)
return playerCard, spireCard
end

-- == YOUR FUNCTION HERE!!! ==

-- ===========================

local playerCard, spireCard

-- 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!!! ==

-- ===============================

(Solution) Functions Exercise


Here’s a possible solution to the exercise. Notice that I’m taking in the cards as arguments, and that we
can name the parameters of the function the same as the local variables playerCard and spireCard ,
because the scope of the function creates two new local variables.
We can’t access playerCard and spireCard without taking them in as arguments, because they are
defined after the function definition, but we can adjust playerPoints and spirePoints , because they
are defined before the function definition.

130
local playerPoints = 0
local spirePoints = 0

local function drawCards()


local minCard, maxCard = 1, 13
local playerCard = math.random(minCard, maxCard)
local spireCard = math.random(minCard, maxCard)
print("Player drew: " .. playerCard .. " | " .. "Spire drew: " .. spireCard)
return playerCard, spireCard
end

-- == YOUR FUNCTION HERE!!! ==


local function calculateScore(playerCard, spireCard)
if playerCard > spireCard then
playerPoints = playerPoints + 1
print("Player win!")
elseif spireCard > playerCard then
spirePoints = spirePoints + 1
print("Spire win!")
else
print("It's a tie!")
end
print("Player: " .. playerPoints .. " | " .. "Spire: " .. spirePoints)
print("--------------------------------")
end
-- ===========================

local playerCard, spireCard

-- 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:

1. inventory : a table representing the inventory


2. item : a string representing the item to be added or used
3. quantity : an integer number that represents the quantity of the item to be added or used

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 = {}

local function addItem(inventory, item, quantity)


-- == YOUR CODE HERE!!! ==

-- =======================
end

local function useItem(inventory, item, quantity)


-- == YOUR CODE HERE!!! ==

-- =======================
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")

(Solution) Tables Exercise


As always, there are many ways to go about solving this exercise. The code is relatively straightforward,
but one thing I want to point out is one of the shorthands that I’ve used. Instead of writing
if curCount == nil then , I simply wrote if not curCount then . These end up being equivalent
statements, because if curCount is nil , then doing not curCount returns true . This is because

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.

local function addItem(inventory, item, quantity)


-- Get the value of item in inventory
local curCount = inventory[item]

if not curCount then


-- If value is nil then set quantity directly
inventory[item] = quantity
else
-- Otherwise, add to existing quantity
inventory[item] = curCount + quantity
end
end

local function useItem(inventory, item, quantity)


-- Get the value of the item in inventory
local curCount = inventory[item]
-- Just in case the value is nil, return early so we don't get an error
if not curCount then return end
-- Subtract quantity
curCount = curCount - quantity

if curCount <= 0 then


-- If quantity depleted, remove entry
inventory[item] = nil
else
-- Otherwise, update quantity in inventory
inventory[item] = curCount
end
end

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

As a refresher, math.random(a, b) returns some number between a and b , inclusively.


Also, you can add elements to a list using table.insert or setting the index directly using
someTable[index] = someValue .

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.

Here’s a link to this exercise: onecompiler.com/lua/432a3vf27

135
local MIN_X, MAX_X = 0, 400
local MIN_Y, MAX_Y = 0, 240

local function spawnEnemies(enemies, count)


-- == YOUR CODE HERE!!! ==

-- =======================
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.

local function spawnEnemies(enemies, count)


for i=1, count do
local enemy = {
name = "Enemy" .. i,
x = math.random(MIN_X, MAX_X),
y = math.random(MIN_Y, MAX_Y)
}
-- We can either use table.insert...
table.insert(enemies, enemy)
-- ... or alternatively, we can do...
enemies[#enemies + 1] = enemy
-- ... or, something like this
enemies[i] = enemy
end
end

137

You might also like