Lua has no classes. No class keyword, no new keyword, nothing to import. What it has instead is one small rule that you can build objects out of, and this page is that rule.
Start from something you already know. A table can hold a function (Activity 1.7), and a table can hold data (Activity 1.10). Put both in the same table and you have most of an object.
local ship = { name = "Skynest", charge = 50 }
function ship.report(self) return self.name .. " at " .. self.charge .. "%"end
print(ship.report(ship))print(ship:report())Skynest at 50%Skynest at 50%Both lines printed the same thing, because ship:report() is exactly ship.report(ship). That is the whole of the colon. It passes the thing on the left as the first argument.
The same shorthand works when you define the function, and it is where self comes from — function Ship:report() is shorthand for function Ship.report(self). self is not a keyword. It is an ordinary parameter that the colon declared for you.
So mixing the two is the classic beginner error. Define with a dot and call with a colon and you get an extra argument nobody expected; define with a colon and call with a dot and self is nil. Both fail confusingly. Use the colon for both, always, and the problem disappears.
The table above works, but every ship would need its own copy of every function. What you want is for many tables to share one set of functions. That needs the rule.
__index means: if a key is not in this table, go look in that one.
local Base = { greet = function() return "hi" end }local obj = {}
print(type(obj.greet))
setmetatable(obj, { __index = Base })
print(type(obj.greet))print(obj.greet())nilfunctionhiBefore the setmetatable call, obj.greet was nil. Afterwards it finds the function — without anything being copied into obj. A metatable is just a table of rules attached to another table, and __index is the rule for failed lookups.
Note this is a fallback, not a merge. A key that exists on the object itself always wins, and writing to the object never touches the table it falls back to.
Every Lua "class" you will ever read is these four lines, so it is worth being able to write them from memory.
local Ship = {}Ship.__index = Ship
function Ship.new(name, charge) return setmetatable({ name = name, charge = charge }, Ship)end
function Ship:report() return self.name .. " at " .. self.charge .. "%"end
local a = Ship.new("Skynest", 50)local b = Ship.new("Vagrant", 92)
print(a:report())print(b:report())Skynest at 50%Vagrant at 92%Reading it one line at a time:
local Ship = {} — the "class" is an ordinary empty table. Capitalized only by convention.Ship.__index = Ship — the line that does the work. It says: when a lookup misses, come back here. Ship is both the class and its own fallback table, which looks circular and is simply thrifty.Ship.new — a plain function that makes a fresh table of data and attaches Ship as its metatable. Nothing about the name new is special. Note the dot, not a colon: you are not calling it on an instance, because there is not one yet.function Ship:report() — a method, stored on the class, found by every instance through the fallback.The data is per-instance and the methods are shared. Each ship's table holds only its own name and charge. One copy of report exists no matter how many ships you make — which is exactly what you want when Unit 2 has two hundred bullets on screen.
Activity 1.8 built something with methods and private state using nothing but closures. Both approaches are real Lua, and they trade off against each other:
ship.charge = 9999 is nobody's business but works fine.For games, metatables win, because you make a lot of instances and privacy is not the problem you have. That is what Unit 2 uses.
Start a fresh main.lua. You are turning the ship record from Activity 1.10 into something you can make many of.
report function that takes self explicitly.-- main.lua - Skynest ship objects
local ship = { name = "Skynest", charge = 50 }
function ship.report(self) return self.name .. " at " .. self.charge .. "%"end
print(ship.report(ship))print(ship:report())Skynest at 50%Skynest at 50%Ship.__index = Ship and a Ship.new constructor.report with a colon this time, and drop the explicit self parameter.Ship.__index = Ship line and running it. The error you get names the exact thing that line was doing.-- main.lua - Skynest ship objects
local Ship = {}Ship.__index = Ship
function Ship.new(name, charge) return setmetatable({ name = name, charge = charge }, Ship)end
function Ship:report() return self.name .. " at " .. self.charge .. "%"end
local skynest = Ship.new("Skynest", 50)local vagrant = Ship.new("Vagrant", 92)
print(skynest:report())print(vagrant:report())Skynest at 50%Vagrant at 92%recharge method that adds to the charge.math.min from Activity 1.4.-- main.lua - Skynest ship objects
local Ship = {}Ship.__index = Ship
function Ship.new(name, charge) return setmetatable({ name = name, charge = charge }, Ship)end
function Ship:report() return self.name .. " at " .. self.charge .. "%"end
local skynest = Ship.new("Skynest", 50)local vagrant = Ship.new("Vagrant", 92)
print(skynest:report())print(vagrant:report())
function Ship:recharge(amount) self.charge = math.min(100, self.charge + amount)end
skynest:recharge(25)vagrant:recharge(25)
print(skynest:report())print(vagrant:report())Ship:isCharged() returning true at 80 or above, and use it in an if.Ship.new a default charge of 100 when none is passed, using the or idiom from Activity 1.7.table.insert and report them all in one ipairs loop. This is the shape of every game object in Unit 2 — a list of instances, walked once per frame.Your finished file should produce this.
Skynest at 50%Vagrant at 92%Skynest at 75%Vagrant at 100%Vagrant stopped at 100, not 117. The clamp worked, both ships kept their own charge, and one shared method served both. That is object-oriented programming in Lua, and you did it with a table and one metatable field.