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