Lua for Game Development — Chapter 3: Practical Patterns for Real Games
Leeting Yan
Modern game development relies heavily on Lua’s flexibility.
Studios consistently use a set of Lua architectural patterns to build clean, scalable, and maintainable game logic.
This chapter presents the patterns used in:
- 2D/3D indie engines
- AAA game scripting systems
- LiveOps / network games
- Modular UI systems
- Defold / Love2D / Roblox (Luau) / Cocos2d-x Lua
- Custom C++ engines with embedded Lua
We will cover:
- Entity & component architecture (ECS-friendly)
- Messaging & event buses
- Timer systems & schedulers
- Finite state machines (FSM)
- Behavior trees (BT) in pure Lua
- Tweening & animation patterns
- Modular game script structure
This is the first “serious engineering” chapter in the book.
1. Entity & Component Architecture (ECS-Friendly)
Lua is excellent for lightweight ECS-style entities.
Below is a “minimal but production-ready” Entity system.
1.1 Entity Object
local Entity = {}
Entity.__index = Entity
function Entity.new(id)
return setmetatable({
id = id,
components = {}
}, Entity)
end
function Entity:add(name, comp)
self.components[name] = comp
end
function Entity:get(name)
return self.components[name]
end
function Entity:update(dt)
for _, comp in pairs(self.components) do
if comp.update then comp:update(dt) end
end
end
return Entity
1.2 Component Example
local Move = {}
Move.__index = Move
function Move.new(speed)
return setmetatable({x=0, y=0, speed=speed}, Move)
end
function Move:update(dt)
self.x = self.x + self.speed * dt
end
return Move
1.3 Use it together
local Entity = require "Entity"
local Move = require "Move"
local e = Entity.new(1)
e:add("move", Move.new(20))
e:update(1.0)
print(e:get("move").x) -- 20
This simple pattern supports:
- Physics
- Rendering
- Animation
- Input
- AI
- Network sync
- Status effects
- Inventory
Because every behavior is just another component table.
2. Event Bus / Messaging System
Games are event-heavy:
- player hit
- item collected
- AI detected player
- scene loaded
- animation finished
Lua makes event buses trivial.
2.1 Minimal Event Bus
local Bus = {}
Bus.__index = Bus
function Bus.new()
return setmetatable({listeners = {}}, Bus)
end
function Bus:on(event, fn)
self.listeners[event] = self.listeners[event] or {}
table.insert(self.listeners[event], fn)
end
function Bus:emit(event, ...)
for _, fn in ipairs(self.listeners[event] or {}) do
fn(...)
end
end
return Bus
2.2 Usage
local bus = Bus.new()
bus:on("hit", function(dmg)
print("Player took damage:", dmg)
end)
bus:emit("hit", 15)
This scales well even in large projects.
3. Timer Systems & Schedulers
Lua coroutines enable game-friendly timing.
We will build a real scheduler (similar to Roblox’s heartbeat scripts or Defold’s coroutine system).
3.1 Wait function (non-blocking)
local function wait(sec)
local start = os.clock()
while os.clock() - start < sec do
coroutine.yield()
end
end
3.2 Full Scheduler
local Scheduler = {}
Scheduler.__index = Scheduler
function Scheduler.new()
return setmetatable({tasks={}}, Scheduler)
end
function Scheduler:add(fn)
table.insert(self.tasks, coroutine.create(fn))
end
function Scheduler:update()
local alive = {}
for _, co in ipairs(self.tasks) do
local ok = coroutine.resume(co)
if ok and coroutine.status(co) ~= "dead" then
table.insert(alive, co)
end
end
self.tasks = alive
end
return Scheduler
3.3 Demo: Multiple tasks running “in parallel”
local scheduler = Scheduler.new()
scheduler:add(function()
wait(1)
print("A finished")
end)
scheduler:add(function()
for i = 1, 3 do
print("B tick:", i)
wait(0.3)
end
end)
while #scheduler.tasks > 0 do
scheduler:update()
end
Output:
B tick: 1
B tick: 2
B tick: 3
A finished
This is how game engines implement:
- Cutscenes
- AI timelines
- Scripted events
- Tween sequences
4. Finite State Machines (FSM)
Game logic is state-driven.
4.1 Simple FSM
local FSM = {}
FSM.__index = FSM
function FSM.new(initial)
return setmetatable({state = initial, states = {}}, FSM)
end
function FSM:add(name, fn)
self.states[name] = fn
end
function FSM:update()
local fn = self.states[self.state]
if fn then fn(self) end
end
return FSM
4.2 Example: Enemy states
local fsm = FSM.new("idle")
fsm:add("idle", function(self)
print("Idle...")
self.state = "walk"
end)
fsm:add("walk", function(self)
print("Walking...")
self.state = "attack"
end)
fsm:add("attack", function(self)
print("Attacking!")
self.state = "dead"
end)
while fsm.state ~= "dead" do
fsm:update()
end
5. Behavior Trees (BT) in Pure Lua
Behavior Trees power modern AI systems in:
- RPGs
- Strategy games
- MOBAs
- Action games
- Simulation games
Below is a minimal real behavior tree implementation.
5.1 Node Types
local BT = {}
function BT.action(fn)
return {type="action", run=fn}
end
function BT.sequence(nodes)
return {type="sequence", nodes=nodes}
end
function BT.selector(nodes)
return {type="selector", nodes=nodes}
end
return BT
5.2 BT Runner
function run(node)
if node.type == "action" then
return node.run()
end
if node.type == "sequence" then
for _, n in ipairs(node.nodes) do
if run(n) ~= "success" then
return "fail"
end
end
return "success"
end
if node.type == "selector" then
for _, n in ipairs(node.nodes) do
if run(n) == "success" then
return "success"
end
end
return "fail"
end
end
5.3 Example: Enemy AI BT
local BT = require "BT"
local tree = BT.selector({
BT.action(function()
if player_in_range() then
enemy_attack()
return "success"
end
return "fail"
end),
BT.action(function()
enemy_patrol()
return "success"
end)
})
run(tree)
This scales into a full AI system.
6. Tweening & Animation Patterns
Tween systems power:
- UI animations
- Camera movement
- Entity interpolation
- FX
- Cutscene motions
6.1 Linear Tween Example
local Tween = {}
Tween.__index = Tween
function Tween.new(obj, prop, target, duration)
return setmetatable({
obj = obj,
prop = prop,
start = obj[prop],
target = target,
t = 0,
duration = duration
}, Tween)
end
function Tween:update(dt)
self.t = self.t + dt
local p = math.min(self.t / self.duration, 1)
self.obj[self.prop] = self.start + (self.target - self.start) * p
end
return Tween
6.2 Usage
local box = {x = 0}
local tween = Tween.new(box, "x", 100, 1.0)
for i = 1, 60 do -- simulate 60 fps
tween:update(1/60)
end
print(box.x) -- ~100
You can expand this to:
- easing functions
- sequences
- parallel tweens
- tween manager
7. Modular Game Script Structure
Large games use module-based architecture.
Recommend structure:
game/
core/
entity.lua
scheduler.lua
eventbus.lua
components/
move.lua
health.lua
attack.lua
ai/
fsm.lua
bt.lua
behaviors/
enemy_basic.lua
boss_phase1.lua
systems/
physics.lua
combat.lua
tween.lua
scenes/
world.lua
battle.lua
main.lua
Lua plays extremely well with such modular designs.
8. Putting It All Together: Scripted Enemy with Components + FSM + Scheduler
This combines everything in this chapter.
local scheduler = Scheduler.new()
local enemy = Entity.new(1)
enemy:add("move", Move.new(2))
local fsm = FSM.new("idle")
fsm:add("idle", function(self)
print("idle")
scheduler:add(function()
wait(1)
self.state = "walk"
end)
end)
fsm:add("walk", function(self)
print("walking")
enemy:get("move").speed = 10
scheduler:add(function()
wait(0.5)
self.state = "attack"
end)
end)
fsm:add("attack", function()
print("attack")
end)
while true do
scheduler:update()
fsm:update()
enemy:update(0.016)
if fsm.state == "attack" then break end
end
This is the foundation of real production gameplay scripting.
9. Summary of Chapter 3
You now understand:
- ECS-like entity architecture
- Event-driven messaging
- Coroutine-based schedulers and timers
- FSM architecture for AI
- Behavior Trees in pure Lua
- Tween systems
- Modular game project structure
- How to integrate all patterns together
This is the Lua that powers real games—not toy examples.