Optional — builds on Activity 1.11. Useful in Unit 2 the moment you want a second kind of enemy.
Activity 1.11 gave you the rule: __index means "if the key is not here, look there." Inheritance is that rule applied a second time.
An instance falls back to its class. Make the class fall back to another class, and a lookup that misses both ends up at the parent. There is no new mechanism — it is one extra link in the same chain.
local Fast = setmetatable({}, { __index = Enemy })Fast.__index = FastRead them separately, because they are doing different jobs and beginners routinely write only one:
So a lookup on a Fast instance takes up to three hops: the instance, then Fast, then Enemy. Miss all three and you get nil, as always.
Because lookup stops at the first table that has the key, defining a method on the child simply wins. Nothing needs to be marked virtual or overridable — that concept does not exist here.
The interesting consequence: an inherited method that calls self:kind() gets the child's version, because the lookup starts from the actual instance every time. That is polymorphism, and it falls out of the fallback rule rather than being a feature.
There is no super. You name the parent directly and pass self yourself.
local Enemy = {}Enemy.__index = Enemy
function Enemy.new(hp) return setmetatable({ hp = hp }, Enemy) endfunction Enemy:describe() return "hp=" .. self.hp end
local Boss = setmetatable({}, { __index = Enemy })Boss.__index = Bossfunction Boss.new(hp) return setmetatable(Enemy.new(hp), Boss) end
function Boss:describe() return "BOSS! " .. Enemy.describe(self)end
print(Enemy.new(10):describe())print(Boss.new(500):describe())hp=10BOSS! hp=500Note the dot: Enemy.describe(self), not Enemy:describe(). The colon would pass Enemy as self, and you would be describing the class rather than the boss. This is the single most common bug on this page.
Writing self:describe() inside Boss:describe is worse — it finds Boss's version and calls itself forever.
Games usually want composition more than inheritance. Three levels deep, and working out where a method actually lives becomes a chore. The common alternative is to give one Enemy type a table of behavior — a behavior field holding functions, swapped per enemy.
One level, as here, is genuinely useful and easy to read. Reach for a second level only when you can name what it buys you.
Start a fresh main.lua. You are building the enemy types for a game that does not exist yet.
kind() returning its name, and a describe() that uses self:kind().self:kind() is the whole point of the exercise — do not inline the string.-- main.lua - enemy hierarchy
local Enemy = {}Enemy.__index = Enemy
function Enemy.new(hp, speed) return setmetatable({ hp = hp, speed = speed or 1 }, Enemy)end
function Enemy:kind() return "Enemy"end
function Enemy:describe() return string.format("%s hp=%d speed=%d", self:kind(), self.hp, self.speed)end
print(Enemy.new(10):describe())Enemy hp=10 speed=1Enemy.new and then re-parents the result to Fast.kind(). Do not write a describe method.describe should nevertheless say "Fast". If it says "Enemy", you called Enemy.kind() somewhere instead of self:kind().-- main.lua - enemy hierarchy
local Enemy = {}Enemy.__index = Enemy
function Enemy.new(hp, speed) return setmetatable({ hp = hp, speed = speed or 1 }, Enemy)end
function Enemy:kind() return "Enemy"end
function Enemy:describe() return string.format("%s hp=%d speed=%d", self:kind(), self.hp, self.speed)end
local Fast = setmetatable({}, { __index = Enemy })Fast.__index = Fast
function Fast.new(hp) return setmetatable(Enemy.new(hp, 5), Fast)end
function Fast:kind() return "Fast"end
print(Enemy.new(10):describe())print(Fast.new(3):describe())Enemy hp=10 speed=1Fast hp=3 speed=5describe() to wrap the parent's version in markers, rather than rewriting it.Enemy.describe(self) with a dot. Try the colon once, deliberately, and read what you get — it is instructive.-- main.lua - enemy hierarchy
local Enemy = {}Enemy.__index = Enemy
function Enemy.new(hp, speed) return setmetatable({ hp = hp, speed = speed or 1 }, Enemy)end
function Enemy:kind() return "Enemy"end
function Enemy:describe() return string.format("%s hp=%d speed=%d", self:kind(), self.hp, self.speed)end
local Fast = setmetatable({}, { __index = Enemy })Fast.__index = Fast
function Fast.new(hp) return setmetatable(Enemy.new(hp, 5), Fast)end
function Fast:kind() return "Fast"end
local Boss = setmetatable({}, { __index = Enemy })Boss.__index = Boss
function Boss.new(hp) return setmetatable(Enemy.new(hp, 1), Boss)end
function Boss:kind() return "Boss"end
function Boss:describe() return "!! " .. Enemy.describe(self) .. " !!"end
print(Enemy.new(10):describe())print(Fast.new(3):describe())print(Boss.new(500):describe())ipairs over it calling describe() on each. One loop, three behaviors, no if — that is what the hierarchy bought you.Enemy:isA(class) that walks the metatable chain with getmetatable and reports whether the given class appears in it.behavior table. Then decide which version you would rather add a fourth enemy to.Enemy hp=10 speed=1Fast hp=3 speed=5!! Boss hp=500 speed=1 !!Fast says "Fast" from a method it never defined, and Boss reuses the parent's text without copying it. Both come from the same fallback rule you learned for one class.