Back

Bonus: Inheritance

Optional — builds on Activity 1.11. Useful in Unit 2 the moment you want a second kind of enemy.

divider

The Idea

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.

The two lines that do it
local Fast = setmetatable({}, { __index = Enemy })
Fast.__index = Fast

Read them separately, because they are doing different jobs and beginners routinely write only one:

  • Line 1 gives the Fast table itself a metatable, so that Fast falls back to Enemy. This is the inheritance.
  • Line 2 sets Fast up as a fallback for its own instances, exactly as in Activity 1.11. This is the class part.

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.

Overriding, and why it works

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.

Calling the parent's version

There is no super. You name the parent directly and pass self yourself.

main.lua
local Enemy = {}
Enemy.__index = Enemy
function Enemy.new(hp) return setmetatable({ hp = hp }, Enemy) end
function Enemy:describe() return "hp=" .. self.hp end
local Boss = setmetatable({}, { __index = Enemy })
Boss.__index = Boss
function 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())
Output
hp=10
BOSS! hp=500

Note 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.

Before you build a hierarchy

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.

divider

Build

Start a fresh main.lua. You are building the enemy types for a game that does not exist yet.


Task 1: The base type

  • Make an Enemy class with hp and speed, speed defaulting to 1.
  • Give it a kind() returning its name, and a describe() that uses self:kind().
  • The call to self:kind() is the whole point of the exercise — do not inline the string.
main.lua
-- 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())
Output
Enemy hp=10 speed=1

Task 2: A child that overrides one method

  • Make Fast inherit from Enemy with the two lines from the concept section.
  • Give it its own constructor that calls Enemy.new and then re-parents the result to Fast.
  • Override only kind(). Do not write a describe method.
  • The inherited describe should nevertheless say "Fast". If it says "Enemy", you called Enemy.kind() somewhere instead of self:kind().
main.lua
-- 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())
Output
Enemy hp=10 speed=1
Fast hp=3 speed=5

Task 3: A child that extends the parent's method

  • Make a Boss that also inherits from Enemy.
  • Override describe() to wrap the parent's version in markers, rather than rewriting it.
  • Use Enemy.describe(self) with a dot. Try the colon once, deliberately, and read what you get — it is instructive.
main.lua
-- 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())

Challenge (Optional)

  • Put all three in a list and ipairs over it calling describe() on each. One loop, three behaviors, no if — that is what the hierarchy bought you.
  • Add Enemy:isA(class) that walks the metatable chain with getmetatable and reports whether the given class appears in it.
  • Rewrite Fast using composition instead: one Enemy class with a behavior table. Then decide which version you would rather add a fourth enemy to.
divider

Check Your Work

Output
Enemy hp=10 speed=1
Fast 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.