Lua for Game Development — Chapter 2: Language Essentials
Leeting Yan
Lua’s syntax is small, elegant, and optimized for scripting game logic.
This chapter teaches Lua through a game development lens, focusing on features you will actually use when scripting AI, animations, UI, events, gameplay systems, or tools.
This is not a language textbook.
This is Lua for building real games.
1. The Building Blocks of Lua
Lua’s core types:
nilbooleannumberstringtable← the most important typefunction← first-class citizen
There are no classes, arrays, or objects—tables represent all of these.
2. Variables and Scope
Lua variables default to global, unless declared with local.
x = 10 -- global
local y = 20 -- local
Always use local in game code:
- Faster (local lookups are optimized)
- Safer (prevents global pollution)
- Avoids subtle bugs
Best practice for game scripts:
local enemy_speed = 120
local max_hp = 80
Globals are only for shared constants:
GAME_VERSION = "1.0"
3. Tables: The Heart of Lua Game Programming
Lua tables are:
- Dictionaries
- Arrays
- Objects
- Components
- Configuration structures
- JSON-like data
- ECS storage
Everything starts from tables.
3.1 Tables as Arrays
local enemies = { "goblin", "slime", "orc" }
print(enemies[1]) -- goblin
Remember: Lua arrays start at index 1, not 0.
3.2 Tables as Dictionaries
local stats = {
hp = 100,
attack = 12,
speed = 4.5,
}
print(stats.hp)
3.3 Tables as Game Objects
Lua doesn’t have classes, but you can simulate objects easily.
local player = {
x = 0,
y = 0,
hp = 100
}
function player:move(dx, dy)
self.x = self.x + dx
self.y = self.y + dy
end
4. Functions: The Workhorse of Game Logic
Functions are first-class, meaning they are:
- Values
- Passed as arguments
- Returned from functions
- Stored inside tables
This is why Lua excels at scripting.
4.1 Basic function
local function attack(target)
print("Attacking", target)
end
Equivalent:
local attack = function(target)
print("Attacking", target)
end
4.2 Anonymous functions (useful in events & UI)
button:onClick(function()
print("clicked!")
end)
4.3 Multiple return values
Perfect for game logic:
local function damage(hp, amount)
local new_hp = hp - amount
local dead = new_hp <= 0
return new_hp, dead
end
local hp, dead = damage(30, 50)
4.4 Closures (critical for timers, AI, state)
Closures “capture” variables:
local function create_counter()
local n = 0
return function()
n = n + 1
return n
end
end
local counter = create_counter()
print(counter()) -- 1
print(counter()) -- 2
Many game features rely on closures:
- AI memory
- UI state
- Animation systems
- Cooldown timers
- Internal state machines
5. Metatables: Custom Behavior for Game Objects
Metatables allow:
- Operator overloading
- Prototype inheritance
- Default values
- State machines
- Custom lookup logic
5.1 Vector Example (common in games)
local Vec = {}
Vec.__index = Vec
function Vec.new(x, y)
return setmetatable({x=x, y=y}, Vec)
end
function Vec.__add(a, b)
return Vec.new(a.x + b.x, a.y + b.y)
end
local v1 = Vec.new(3, 4)
local v2 = Vec.new(1, 2)
local v3 = v1 + v2 -- operator overloaded
print(v3.x, v3.y)
You can extend this system to physics, movement, transforms, or camera logic.
5.2 Default Values via Metatable
Useful for entity configs:
local defaults = {hp=100, speed=3}
local mt = { __index = defaults }
local goblin = setmetatable({}, mt)
print(goblin.hp) -- 100
6. Coroutines: The Secret to Game-Friendly Scripting
Lua coroutines are:
- Cheaper than OS threads
- More flexible than async/await
- Ideal for AI, cutscenes, timelines, and animation logic
6.1 Basic coroutine
local co = coroutine.create(function()
print("Step 1")
coroutine.yield()
print("Step 2")
end)
coroutine.resume(co)
coroutine.resume(co)
6.2 Coroutines as Timelines (SUPER important)
local function timeline()
play("idle")
wait(1)
play("attack")
wait(0.2)
play("recover")
end
Game designers LOVE this style of code, which is why countless studios adopt Lua.
7. Error Handling and Debugging
Game scripting must fail gracefully.
7.1 pcall: protected call
local ok, err = pcall(function()
error("boom")
end)
print(ok, err)
Used in:
- Script hot reload
- Sandbox/modding
- Safe mission scripting
7.2 xpcall with traceback
local ok, err = xpcall(function()
error("bad!")
end, debug.traceback)
print(err)
8. Logging & Debug Utilities
During development:
local function log(msg)
print("[LOG]", msg)
end
log("Player spawned")
Production engines replace this with:
- On-screen debug console
- File logging
- Remote log sinks
9. Putting It All Together: A Mini Scripted Enemy
This combines:
- tables
- functions
- closures
- timers via coroutines
- simple AI state
local enemy = {
x = 0,
hp = 60,
}
function enemy:attack()
print("enemy attacks!")
end
function enemy:run_ai()
print("idle")
wait(0.5)
print("walk")
self.x = self.x + 1
wait(1)
print("attack")
self:attack()
wait(0.2)
print("done")
end
local co = coroutine.create(function() enemy:run_ai() end)
while coroutine.status(co) ~= "dead" do
coroutine.resume(co)
end
This is the foundation of real AI scripts used in:
- Defold
- Roblox
- Cocos
- LÖVE
- Custom native engines
10. Summary of Chapter 2
You learned:
- Lua core syntax from a game development viewpoint
- Tables as objects, arrays, configs, and components
- Functions + closures powering game logic
- Metatables for custom behavior
- Coroutines for timelines and AI
- Basic debugging and error-handling patterns
- A complete mini enemy script
These skills form the backbone of all real Lua-based games.