Lua for Game Development — Chapter 3: Practical Patterns for Real Games

A complete guide to Lua architecture patterns used in modern games: ECS-friendly entities, event buses, timers, schedulers, state machines, behavior trees, tweens, and modular script design.

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:

  1. Entity & component architecture (ECS-friendly)
  2. Messaging & event buses
  3. Timer systems & schedulers
  4. Finite state machines (FSM)
  5. Behavior trees (BT) in pure Lua
  6. Tweening & animation patterns
  7. 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.

Join newsletter Browse articles