Why Games Use Lua
See why a game hosts Lua, run a small observer script, and learn the values and host boundary that later lessons build on.
Cheatsheet for Lesson 11.2
A one-page summary of “Why Games Use Lua” you can keep beside your notes. Plain Markdown.
Lesson 11.2 — Why Games Use Lua: cheatsheet
- Remember: A script can act only through the host functions it receives; copied observations do not grant arbitrary memory access.
- Also useful: Embedding Lua is a conversation: the host asks the interpreter to run a script, and a registered callback lets the script ask the host for one service.
Key terms
game: Heregameis a table of functions supplied by our lab host,logis one of those functions, and the quoted text is its argument.log: Heregameis a table of functions supplied by our lab host,logis one of those functions, and the quoted text is its argument.second: There is only onehealthfield, so the write throughsecondis the value read throughfirst.- aliasing: This shared reference is called aliasing.
- capability design: This is capability design: code can do only what the values and functions in its environment allow.
entity: In theforline below,_discards the numeric position andentitynames the current entry.
Review
- Q: In an embedded Lua design, who decides which game operations a script can request? A: The compiled host that builds the script API
- Q: What is the main practical benefit of embedding Lua in a compiled game engine? A: Designers can change a quest or rule without rebuilding the engine
Chapter 10 followed game files and mods through their formats, integrity checks, and loading behavior. Some games also offer a supported way to run small scripts inside the game. That changes the first question: instead of asking where a value lives in memory, ask which values and actions the game deliberately gives to a script. Lua is one common language for that boundary.
We will first write small Lua scripts and define the host API that contains them. Once those pieces work, we will open the interpreter itself: source text becomes bytecode, and bytecode runs on a virtual machine with its own values, stack, and object lifetimes.
Compiled engine, flexible script
Section titled “Compiled engine, flexible script”The native host is normally compiled ahead of time into machine code. Changing its source means building a new executable or library.
Lua is a small programming language commonly embedded inside a larger program. The game owns a Lua interpreter and asks it to run text scripts. A designer can change a quest, unit behavior, or user-interface behavior without rebuilding the whole engine.
Use the host-boundary model:
A host table exposes selected operations. This step-by-step trace needs JavaScript. Here is the code and where the run ends.
read the selected inputs
evaluate each stated condition
combine the required conditions
perform the effect only when accepted
otherwise retain the old state
report the result and its limitsResult: the exported operation runs within its documented host boundary.
Final values: operation = snapshot; exported = true; accepted = true; returned = copied fields.
Read every step of the lesson’s example
Step 1, line 1. Start with the selected inputs and follow how the next operation changes the state.
operation = snapshot.
Step 2, line 2. This host deliberately installs snapshot and log; it does not export a raw-memory function.
operation = snapshot; exported = true.
Step 3, line 3. Accept only when the required conditions all hold.
operation = snapshot; exported = true; accepted = true.
Step 4, line 4. The model performs the accepted effect.
operation = snapshot; exported = true; accepted = true; returned = copied fields.
Step 5, line 6. Result: the exported operation runs within its documented host boundary.
operation = snapshot; exported = true; accepted = true; returned = copied fields.
The host installs log, snapshot and request functions in a Lua game table. A log argument reaches the host console, named entity fields are copied into Lua, and a selection request returns for host validation. Process handles and raw memory remain behind the host boundary.
Read the steps
- The compiled host owns the live entities, process handles, and Lua runtime. An empty game table gives the script no route to those resources.
- The host installs three function capabilities in game: log, snapshot, and request. These callable values grant specific operations, not unrestricted engine access.
- The script calls game.log with observer started. That argument travels to the host-supplied log function, which decides where the message appears.
- game.snapshot asks the host for an observation. The host copies named fields into Lua-owned tables. Those values are separate from live entity storage.
- A select_entity request travels in the other direction. The host checks the requested ID against the current world before allowing a bounded action.
- No raw memory function was supplied. Looking for memory.write does not create a capability or open another route through the boundary.
An API is a set of deliberately supplied routes. Copying observations and checking requests keeps ownership with the host.
The engine owns the runtime and decides which values and functions cross the API boundary. A script cannot automatically do everything the engine can do; it sees only the capabilities that the host deliberately exposes.
From a script file to one visible result
Section titled “From a script file to one visible result”The host loads Lua source, checks that it is valid Lua, and asks the Lua runtime
to execute it. When the script calls game.log, execution crosses back to a
function the host supplied.
The interpreter inside that runtime is also called a Lua virtual machine, or VM: it follows internal instructions made from the script. That is enough to understand why a host can later count script steps. Lesson 11.9 opens the VM and shows the instructions themselves.
We can observe the host crossing without changing any game state:
game.log("observer started")Here game is a table of functions supplied by our lab host, log is one of
those functions, and the quoted text is its argument. The host decides where
the message appears. For this first script, we only need to know who runs it
and who supplies game.log.
Control Run the script
Step 1: Native host to Lua script
The host asks its Lua runtime to execute the script.
Request game.log("observer started")
Step 2: Lua script to Native host
game.log was registered by the host, so this call crosses back into native code with the message.
Reply Callback logs it, then returns
Step 3: Native host to Lua script
The host decides what logging means (a file, a console or a buffer); then the script continues after the call.
The script’s game.log("observer started") call sends its argument back through the callback the host supplied.
Lua is dynamically typed
Section titled “Lua is dynamically typed”The host language knows a variable’s type at compile time:
let health: u32 = 100;In Lua, a variable can refer to values of different types at different times:
local health = 100health = "unknown"That flexibility is convenient for configuration and gameplay behavior. It also moves many mistakes from compile time to runtime. Clear names, validation, tests, and small APIs matter even more.
For these lessons we pin the lab to Lua 5.4 through mlua 0.12. Other games may embed Lua 5.1, LuaJIT, Lua 5.5, or a customized dialect. Check the target’s documentation or version string before assuming features exist.
The values you meet first
Section titled “The values you meet first”These declarations put seven different kinds of value behind local names.
Nothing runs merely because the function value is created; decide can be
called later, while the other values can be read or changed by later code.
local name = "bot_alpha" -- stringlocal health = 100 -- integer numberlocal distance = 12.5 -- floating-point numberlocal alive = true -- booleanlocal target = nil -- no valuelocal position = { x = 4, y = 9, z = 2 } -- tablelocal decide = function() end -- functionLua also has threads/coroutines and userdata supplied by a host. A table is the main container: it can behave like a list, dictionary, record, namespace, or object.
A table variable refers to one table object. Assigning it to another name copies that reference, so two names can reach the same table:
local first = { health = 100 }local second = firstsecond.health = 75assert(first.health == 75) -- both names reach the same tablesecond = first copied the reference, the arrow, not the table it points to.
There is only one health field, so the write through second is the value
read through first.
This shared reference is called aliasing. Lesson 11.3 develops tables and how iteration selects keys; Lesson 11.10 explains how the runtime represents values and eventually reclaims unreachable tables. When a table crosses into a host function, the host must still validate the fields it receives.
Local versus global names
Section titled “Local versus global names”The two assignments below look similar, but only the first declares a name private to its scope. The second writes into Lua’s shared global environment.
local selected = nil -- visible only in this scope ✅state = "running" -- creates or replaces a global name ⚠️Prefer local unless you intentionally publish a value. Accidental globals let separate scripts overwrite each other’s state and make spelling mistakes difficult to find.
The host API is the security boundary
Section titled “The host API is the security boundary”A poor host might expose an all-powerful function:
-- ❌ A script can ask for any address, size, and write.memory.write(address, bytes)The book’s lab exposes a narrow interface instead:
local entities = game.snapshot()if entities[2] then game.request({ kind = "select_entity", id = entities[2].id })endgame.snapshot() returns copied sample data. If a second entity exists, the
script asks to select its ID; the host can still reject the request if that ID
is no longer valid. game.request accepts only a small list of validated
actions. The script never receives a process handle, arbitrary pointer,
command shell, or raw Windows API.
This is capability design: code can do only what the values and functions in its environment allow.
Your first observer
Section titled “Your first observer”The file lua-labs/scripts/observer.lua contains:
First, game.snapshot() supplies a list of copied entity tables. Lua’s
ipairs(entities) then visits list entries in order. In the for line below,
_ discards the numeric position and entity names the current entry.
The alive and "alive" or "dead" expression chooses one of two words, and
string.format fills the %s text slots and %.1f one-decimal number slots.
Those pieces turn one copied list into readable output:
local entities = game.snapshot()
for _, entity in ipairs(entities) do local state = entity.alive and "alive" or "dead" game.log(string.format( "%s: %s at (%.1f, %.1f, %.1f)", entity.name, state, entity.position.x, entity.position.y, entity.position.z ))endRead it as a sequence:
- ask for one snapshot;
- visit each entity in its list order;
- choose a readable state word;
- format one message;
- send that text to the host logger.
Nothing here edits a game. It proves the language, table shape, and host boundary before later automation.
Run the lab
Section titled “Run the lab”From the repository root:
cargo run --manifest-path lua-labs/Cargo.toml -- scripts/observer.luaThe host works on Windows, macOS, and Linux because it uses a simulated snapshot. Later you can connect the same API shape to one of the book’s read-only Windows observers.
Versions and trust differ between games
Section titled “Versions and trust differ between games”Games that load Lua mods differ in each of these, and each one changes what a script can do or how its failures show up:
- Lua version or dialect;
- search paths and file names;
- which standard libraries are available;
- host-provided functions and their required argument types and ranges;
- whether scripts are trusted, signed, or user-authored;
- CPU, memory, and action limits;
- how a script error is reported and disabled.
Lua makes behavior easier to change. A well-designed host also makes the boundaries easier to explain.
Find the authority
In an embedded Lua design, who decides which game operations a script can request?
The host owns the interpreter and chooses which functions and values enter the environment, making that API the capability boundary. The interpreter only runs what it is given.
Finished Lesson 11.2?
Mark it done and the lesson list shows a beside it. A chapter whose lessons are all done is marked done too. Progress is saved in this browser only.
Plain Markdown, saved in this browser.
This book grew out of the original Game Hacking Academy by attilathedud and has been rewritten as a beginner-first course. Like the original repository, its source is published under the Apache License 2.0.