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.
Keep Reading
Follow the engineering thread
Get the next practical Birdor note, or browse the archive for related systems, tooling, and architecture work.