27. Simple OOP with metatables
Chapter 26 put a few methods directly on a table. That works for one
object, but wastes effort for many objects of the same
shape. Imagine 100 dogs — copying bark onto each one is
silly. This chapter introduces the metatable, then uses
it to write one method table that every instance shares.
This is also where Roblox-style code starts to look familiar. A
Roblox Part, Player, and Tool all
follow this pattern.
A class with .new and
:method
Here is the standard Lua "class" pattern. Read it twice, then check the explanation:
local Point = {}
Point.__index = Point
function Point.new(x, y)
local self = setmetatable({}, Point)
self.x = x
self.y = y
return self
end
function Point:distance(other)
local dx = self.x - other.x
local dy = self.y - other.y
return math.sqrt(dx * dx + dy * dy)
end
local a = Point.new(0, 0)
local b = Point.new(3, 4)
print(a:distance(b)) -- 5.0What is going on:
Point = {}creates the class table — every method lives here, defined once.Point.__index = Pointis the magic line. It tells Lua: if a key lookup on an instance fails, also check here. The metatable isPointitself, its__indexpointing back to itself.Point.new(x, y)is a regular function (note the dot, not the colon). It creates an instance — an empty table — sets its metatable toPoint, fills the fields, and returns it.setmetatable({}, Point)attaches the metatable, so the empty table now knows: for missing keys, look insidePoint.function Point:distance(other)defines the method with the colon, makingselfthe first parameter — same as chapter 26.a:distance(b)is the call site.ais an instance, so Lua checksafordistance. Not found, so__indexredirects the lookup toPoint, which has it. The method runs withself = aandother = b.
The result: one method table, many small instances, no copying.
Open exercises/27/01-point.lua. Add a
Point:move(dx, dy) method that adds dx to
self.x and dy to self.y. Create a
point, move it twice, then print its position.
Why the metatable indirection?
A class needs a metatable, not a plain table, because assignment to an instance must not affect the class.
Skip the metatable and write local self = Point, and
self.x = 0 writes x = 0 onto the
Point class itself. Every "instance" would share the same
x — not what "instance" means.
setmetatable({}, Point) gives a fresh empty table per
instance. Writes go into the instance; reads fall through to the class
only when the instance lacks the key.
A second class: Character
A small character class that can take damage:
local Character = {}
Character.__index = Character
function Character.new(name, hp)
local self = setmetatable({}, Character)
self.name = name
self.hp = hp
self.max_hp = hp
return self
end
function Character:takeDamage(amount)
self.hp = self.hp - amount
if self.hp < 0 then
self.hp = 0
end
end
function Character:heal(amount)
self.hp = self.hp + amount
if self.hp > self.max_hp then
self.hp = self.max_hp
end
end
function Character:isAlive()
return self.hp > 0
end
local c = Character.new("Keiko", 100)
c:takeDamage(30)
print(c.hp) -- 70
c:heal(50)
print(c.hp) -- 100 (capped at max_hp)
print(c:isAlive()) -- trueEach Character.new(...) produces a fresh instance with
its own name, hp, and max_hp. The
four methods live on Character; every instance reaches them
through __index.
Inheritance with two
__index hops
A child class inherits from a parent by giving the child's metatable
its own __index chain. The two short tables:
local Animal = {}
Animal.__index = Animal
function Animal.new(name)
local self = setmetatable({}, Animal)
self.name = name
return self
end
function Animal:describe()
print("I am " .. self.name .. ".")
end
-- Dog inherits from Animal
local Dog = setmetatable({}, { __index = Animal })
Dog.__index = Dog
function Dog.new(name)
local self = Animal.new(name) -- build with Animal's fields
return setmetatable(self, Dog) -- but with Dog as metatable
end
function Dog:bark()
print(self.name .. ": Woof!")
end
local rex = Dog.new("Rex")
rex:describe() -- I am Rex. (inherited from Animal)
rex:bark() -- Rex: Woof! (defined on Dog)The key line is
local Dog = setmetatable({}, { __index = Animal }). It
says: when a lookup falls through Dog, fall through
again into Animal. So rex:describe()
checks rex, misses; checks Dog, misses; checks
Animal, finds it.
That is enough inheritance for most purposes. Metatables can do more (operator overloading, custom comparison, callable tables), but these patterns cover everything Part 6's mini-project and the Roblox bridge need.
Homework
Problem 1 — Point class with move
Open exercises/27/homework/01-point.lua. Build a
Point class as above, plus a
Point:move(dx, dy) method that adds the deltas to the
position. Test by creating two points, moving one, then printing the
distance between them.
Problem 2 — Character class
Open exercises/27/homework/02-character.lua. Build the
Character class with .new(name, hp),
:takeDamage(amount), :heal(amount), and
:isAlive(). Add a :report() method that prints
the name and current HP. Walk through a small fight: damage twice, heal
once, and print the report after each action.
Problem 3 — Rectangle class
Open exercises/27/homework/03-rectangle.lua. Build a
Rectangle class with .new(width, height),
:area(), and :perimeter(). Test it with two
rectangles of different sizes.
Challenge — Animal and Dog
Open exercises/27/homework/04-inheritance.lua. Build
Animal and Dog exactly as in the chapter's
inheritance example. Then add a second child class
Cat that also inherits from Animal and has its
own :meow() method. Create one dog and one cat, call
:describe() on both, then :bark() on the dog
and :meow() on the cat.
Stuck or finished? Open the homework solutions page.