Back

Bonus: Operators on Your Own Types

Optional — builds on Activity 1.11. Nothing in Unit 2 requires it, but Unit 2 gets nicer if you have it.

divider

The Idea

Activity 1.11 used one metatable field, __index, to make method lookup fall back to a class table. There are about twenty more fields, and each one lets you say what a built-in operator should do when it meets your type.

Out of the box, it does not know.

main.lua
local a = { x = 1, y = 2 }
local b = { x = 10, y = 20 }
local ok = pcall(function() return a + b end)
print("did it work?", ok)
Output
did it work? false

Adding two tables is an error, and reasonably so — Lua has no idea what you meant. Define __add and you have told it.

main.lua
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
function Vec.__tostring(v)
return "(" .. v.x .. ", " .. v.y .. ")"
end
local a = Vec.new(1, 2)
local b = Vec.new(10, 20)
print(a + b)
Output
(11, 22)

Two things happened there. a + b called your function, and print showed a readable vector instead of table: 0x14b2f30 — because print asks __tostring first.

__tostring is the one to add to everything. Debugging a game full of tables that all print as hex addresses is genuinely miserable, and this is a four-line fix per type.

The ones worth knowing

FieldTriggered by
__adda + b
__suba - b
__mula * b
__diva / b
__unm-a
__eqa == b
__lta < b
__concata .. b
__tostringprint(a), tostring(a)
__calla()
__indexa key that is not there

Three rules that will save you an afternoon.

  • __eq is only consulted when both sides are tables. Comparing your vector to a number is simply false, and your function never runs.
  • There is no __ne or __gt. Lua builds ~= from __eq and > by swapping the operands into __lt.
  • Skip __len. Plain Lua honors it for tables and LÖVE does not — the same code gives a different answer in Unit 2. Write a :length() method instead.

One caution on where this belongs. Operators are for types where the arithmetic is genuinely obvious — vectors, colors, money, times. Giving Enemy an __add because you could is how code becomes unreadable. If a reader has to look up what + means here, use a named method.

divider

Build

Start a fresh main.lua. You are building a 2D vector — the single most useful small type in game programming, and the reason this page exists.


Task 1: Add and print

  • Make a Vec class exactly as in Activity 1.11, with an x and a y.
  • Give it __add and __tostring.
  • Print the sum of two vectors directly — no tostring call needed.
main.lua
-- main.lua - vectors with operators
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
function Vec.__tostring(v)
return "(" .. v.x .. ", " .. v.y .. ")"
end
local a = Vec.new(1, 2)
local b = Vec.new(10, 20)
print(a + b)
Output
(11, 22)

Task 2: Subtract, scale, negate

  • Add __sub, __mul, and __unm.
  • Note that __mul takes a vector and a number, not two vectors. Multiplying two vectors has several possible meanings, so leave it out rather than pick one.
  • Print all four results.
main.lua
-- main.lua - vectors with operators
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
function Vec.__sub(a, b)
return Vec.new(a.x - b.x, a.y - b.y)
end
function Vec.__mul(v, s)
return Vec.new(v.x * s, v.y * s)
end
function Vec.__unm(v)
return Vec.new(-v.x, -v.y)
end
function Vec.__tostring(v)
return "(" .. v.x .. ", " .. v.y .. ")"
end
local a = Vec.new(1, 2)
local b = Vec.new(10, 20)
print(a + b)
print(b - a)
print(a * 3)
print(-a)
Output
(11, 22)
(9, 18)
(3, 6)
(-1, -2)

Task 3: Equality, and the line this was all for

  • Add __eq and confirm that two separately-made vectors with the same numbers compare equal. Without it they never would, because two different tables are two different tables.
  • Then write the payoff line: pos = pos + vel * dt.
  • That is the entire movement step of a game, on one line, reading exactly like the physics it came from. Compare it to writing pos.x = pos.x + vel.x * dt and the same again for y.
main.lua
-- main.lua - vectors with operators
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
function Vec.__sub(a, b)
return Vec.new(a.x - b.x, a.y - b.y)
end
function Vec.__mul(v, s)
return Vec.new(v.x * s, v.y * s)
end
function Vec.__unm(v)
return Vec.new(-v.x, -v.y)
end
function Vec.__eq(a, b)
return a.x == b.x and a.y == b.y
end
function Vec.__tostring(v)
return "(" .. v.x .. ", " .. v.y .. ")"
end
local a = Vec.new(1, 2)
local b = Vec.new(10, 20)
print(a + b)
print(b - a)
print(a * 3)
print(-a)
print(a == Vec.new(1, 2))
local pos = Vec.new(100, 200)
local vel = Vec.new(5, -3)
local dt = 2
pos = pos + vel * dt
print(pos)

Challenge (Optional)

  • Add a Vec:length() method using math.sqrt. A method, not __len — you were warned why.
  • Add __lt comparing lengths, then table.sort a list of vectors with no comparison function at all.
  • Give a Color type the same treatment, and check that __mul by a number does something sensible at the edges. Clamping belongs in the metamethod.
divider

Check Your Work

Output
(11, 22)
(9, 18)
(3, 6)
(-1, -2)
true
(110, 194)

(110, 194) — two steps of velocity applied to a position, written the way you would write it on paper.