Lua Advanced: Metatables, Coroutines, and Powerful Patterns
Leeting Yan
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.hpis 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.
5.1 Classic Return Table (recommended)
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.