Lua Advanced: Metatables, Coroutines, and Powerful Patterns

A clean, practical deep dive into Lua’s advanced features: metatables, metamethods, coroutines, modules, and common design patterns.

Lua is simple on the surface, but extremely powerful underneath.
This article explores metatables, metamethods, coroutines, advanced module design, and a set of practical patterns used in games, tools, and embedded systems.

All examples are fully runnable with standard Lua 5.3/5.4.

1. Metatables: Lua’s Custom Behavior Engine

Metatables let you customize how tables behave—similar to operator overloading, custom indexing, inheritance, and more.

1.1 Basic Example: __index Fallback

local defaults = {hp = 100, mp = 50}

local player = {}
setmetatable(player, {
  __index = defaults
})

print(player.hp)  -- 100
print(player.mp)  -- 50

Explanation:

  • player.hp is not found.
  • Lua checks metatable → __index
  • It returns defaults.hp

2. Metamethods: Overriding Operators

2.1 Custom Addition (__add)

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(1, 2)
local v2 = Vec.new(3, 4)
local v3 = v1 + v2

print(v3.x, v3.y)   -- 4 6

2.2 Custom tostring (__tostring)

function Vec.__tostring(v)
  return string.format("(%d, %d)", v.x, v.y)
end

print(v3)   -- (4, 6)

2.3 Custom indexing logic

local t = {x = 10}
local mt = {
  __index = function(_, key)
    return "missing:" .. tostring(key)
  end
}
setmetatable(t, mt)

print(t.x)     -- 10
print(t.y)     -- missing:y

3. Inheritance via Metatables

Lua has no classes, but inheritance is straightforward.

local Animal = {}
Animal.__index = Animal

function Animal:new(name)
  return setmetatable({name=name}, self)
end

function Animal:speak()
  print(self.name .. " makes a sound")
end

local Dog = setmetatable({}, Animal)   -- inheritance
Dog.__index = Dog

function Dog:speak()
  print(self.name .. " barks")
end

local d = Dog:new("Buddy")
d:speak()  -- Buddy barks

4. Coroutines: Lua’s Lightweight Threads

Coroutines allow cooperative multitasking.
They are used for scripting, game AI, animations, and non-blocking flows.

4.1 Basic Coroutine

local co = coroutine.create(function()
  print("step 1")
  coroutine.yield()
  print("step 2")
end)

print(coroutine.resume(co))  -- step 1
print(coroutine.resume(co))  -- step 2

4.2 Producer–Consumer Pattern

local function producer()
  return coroutine.create(function()
    for i = 1, 3 do
      coroutine.yield(i * 10)
    end
  end)
end

local p = producer()

while true do
  local ok, val = coroutine.resume(p)
  if not ok or val == nil then break end
  print("received:", val)
end

Output:

received: 10
received: 20
received: 30

4.3 Coroutine Pipeline (Game Loop Style)

local function wait(n)
  local start = os.time()
  while os.time() - start < n do
    coroutine.yield()
  end
end

local co = coroutine.create(function()
  print("Start")
  wait(1)
  print("1 second passed")
end)

while coroutine.status(co) ~= "dead" do
  coroutine.resume(co)
end

5. Advanced Module Patterns

Lua modules can be designed in multiple styles.

math_ex.lua:

local M = {}

function M.add(a, b) return a + b end
function M.mul(a, b) return a * b end

return M

Use:

local m = require("math_ex")
print(m.mul(3, 4))

5.2 Constructor-Based Module

Ideal for game objects.

enemy.lua:

local Enemy = {}
Enemy.__index = Enemy

function Enemy.new(hp)
  return setmetatable({hp = hp}, Enemy)
end

function Enemy:hit()
  self.hp = self.hp - 10
end

return Enemy

6. Useful Patterns in Lua Development

Lua’s flexibility encourages several idioms widely used in production.

6.1 Prototype-Based Objects

local prototype = {hp = 100}

function prototype:new(o)
  o = o or {}
  return setmetatable(o, {__index = self})
end

local p1 = prototype:new({hp = 150})
local p2 = prototype:new()

print(p1.hp, p2.hp)  -- 150 100

6.2 Fluent API Pattern

local builder = {}
builder.__index = builder

function builder.new()
  return setmetatable({data={}}, builder)
end

function builder:setName(n)
  self.data.name = n
  return self
end

function builder:setAge(a)
  self.data.age = a
  return self
end

print(builder.new():setName("Alice"):setAge(20).data.name)

6.3 Functional Utilities (map/filter/reduce)

local function map(t, fn)
  local r = {}
  for i, v in ipairs(t) do r[i] = fn(v) end
  return r
end

local arr = {1, 2, 3}
local doubled = map(arr, function(x) return x * 2 end)

for _, v in ipairs(doubled) do
  print(v)
end

6.4 Event System (Mini Emitter)

local Emitter = {}
Emitter.__index = Emitter

function Emitter.new()
  return setmetatable({listeners = {}}, Emitter)
end

function Emitter:on(event, handler)
  self.listeners[event] = self.listeners[event] or {}
  table.insert(self.listeners[event], handler)
end

function Emitter:emit(event, ...)
  local list = self.listeners[event]
  if not list then return end
  for _, fn in ipairs(list) do fn(...) end
end

-- Demo
local e = Emitter.new()
e:on("hit", function(dmg) print("got hit:", dmg) end)
e:emit("hit", 15)

7. Metatable Tricks for Clean Code

7.1 Auto-Default Values

local defaults = {hp = 100, mp = 50}

local mt = {
  __index = function(_, key)
    return defaults[key]
  end
}

local p = setmetatable({}, mt)
print(p.hp, p.mp)  -- 100 50

7.2 Read-Only Tables (immutable)

local function readOnly(t)
  return setmetatable({}, {
    __index = t,
    __newindex = function()
      error("attempt to modify read-only table")
    end
  })
end

local config = readOnly({version="1.0"})
print(config.version)
config.version = "2.0"  -- error

8. Practical Example: Coroutine-Based AI Script

local function npcAI()
  print("NPC idle")
  coroutine.yield()

  print("NPC patrols")
  coroutine.yield()

  print("NPC attacks")
end

local co = coroutine.create(npcAI)

for i = 1, 3 do
  coroutine.resume(co)
end

This structure is used in real games (e.g., Defold, Roblox behaviour trees, OpenResty workflows).

9. Summary

In this advanced article, you learned:

  • Metatables and metamethods
  • Operator overloading
  • Inheritance via metatables
  • Coroutines and real-world coroutine patterns
  • Module design styles
  • Common Lua patterns (OOP, fluent API, events, functional utilities)
  • Practical tricks for clean APIs

Lua’s simplicity hides a huge amount of expressive power.
With these advanced tools, you can design flexible game scripts, DSLs, simulation logic, or embedded automation systems.

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