How do I do conditional / multiple inheritance in Lua with OOP?

Let’s say I have a beat-'m-up game with many enemies. I have a metatable metaEnemy containing methods for enemy behavior. I also have different enemy types with their respective metatables that define their behavior, such as metaOrc for orc enemies and metaBat for bat enemies. Both metatables have their metatables set to metaEnemy to inherit generic enemy methods. So in code I have the following so far:

local metaEnemy = {}
metaEnemy.__index = metaEnemy
function metaEnemy:update(...) ... end -- etc.

local metaOrc = setmetatable({}, metaEnemy) -- inherit methods from metaEnemy
metaOrc.__index = metaOrc
function metaOrc:doExclusiveOrcThing(...) ... end -- and more methods etc.

local metaBat = setmetatable({}, metaEnemy)
metaBat.__index = metaBat
function metaBat:doExclusiveBatThing(...) ... end -- and more methods etc.

So if I were to create an orc, I would do:

local orc = setmetatable({orc_properties}, metaOrc)

I can then call methods on the orc variable which will either reside in the orc table, the metaOrc table or the metaEnemy table.

Now I want to add a super-duper-cool feature to my game called ‘boss enemies’, which are regular enemies, but bigger and they have some exclusive ‘boss methods’.

If I were to create, for example, a boss orc enemy, I would want to construct an orc enemy that has its metatable set to metaOrc to inherit the orc’s behavior. However, I would also want to inherit the generic boss methods which I may have defined in a bossMeta metatable. I could make it so the bossMeta metatable has its metatable set to metaOrc, but then I wouldn’t be able to reuse it for bat boss enemies, or other types of boss enemies.

Alternatively I could create a seperate boss metatable for each enemy type, but then I would be repeating the same code over and over again for the different enemies.

Preferably I would like to have only one metatable for boss behavior, and depending on which enemy type it’s for, it would have its metatable set to the corresponding enemy’s metatable (so metaOrc or metaBat etc.). Is there a way to construct this ‘conditional inheritance’ in a clean manner?

2 Likes

If a boss is always an enemy it can just inherit from enemy. With that, there are two types of orcs:

  1. orc → boss → enemy
  2. orc → enemy

One solution could be to make two separate orc classes and copy the code. This is not ideal.

Here is my alternate proposed solution:

local function copy(tbl)
	local newTbl = {}

	for key, value in pairs(tbl) do
		newTbl[key] = value
	end

	return newTbl
end
-- Enemy:

local Enemy = {}
Enemy.__index = Enemy

function Enemy.new()
	return setmetatable({}, Enemy)
end

-- Boss:

local Boss = setmetatable({}, Enemy)
Boss.__index = Boss

function Boss.new()
	return setmetatable({}, Boss)
end

-- Orc:

local Orc = {}

function Orc:method()

end

-- Orc methods and properties must be defined before you copy them!

local OrcBoss = setmetatable(copy(Orc), Boss)
OrcBoss.__index = OrcBoss

local OrcEnemy = setmetatable(copy(Orc), Enemy)
OrcEnemy.__index = OrcEnemy

-- You can construct the different orc types like this:

function Orc.enemy()
	return setmetatable({}, OrcEnemy)
end

function Orc.boss()
	return setmetatable({}, OrcBoss)
end

Orc.enemy()
Orc.boss()

-- or like this:

function OrcEnemy.new()
	return setmetatable({}, OrcEnemy)
end

function OrcBoss.new()
	return setmetatable({}, OrcBoss)
end

OrcEnemy.new()
OrcBoss.new()

This method does have two different orc classes, but you do not have to copy code. You can also abstract those two classes away with the first constructors I showed.

The down side of this method is that you have to copy the orc class twice. The functions are just references in the copies so it isn’t a huge deal.

1 Like