Attempt on ULTRA Light Multi-Inheritance OOP (example build)

I am attempting to make ULTRA light multi layer OOP. I wonder if I missed anything in my current, please reply!

the main goal is lower memory usage as much as possible without killing readability, make it close to 0 performance / iteration cost without sacrificing redundancy

What I accounted for:

  • Prevented recreating IndexSearch function
  • Tables start empty by design, and only cache if modified
  • Looks up from the listed recipe order, by Ipairs, (ideally core → unique components)

Cons:

  • because there aren’t template data it will pull from, I’d have to use functions like getHealth,
  • if modified data is equalivent to starter data, it shouldn’t exist, so need to add cleanup methods without having any constant check costs
local HealthComponent = {
	maxHealth = 100,
	getHealth = function(self) 
		return self.health or self.maxHealth 
	end,
	takeDamage = function(self, amount) 
		self.health = math.max(0, (self:getHealth()) - amount)
	end
}
local AttackComponent = {
	damage = 10,
	attack = function(self, target) 
		target:takeDamage(self.damage)
	end
}
local Recipes = {
	Soldier = {HealthComponent, AttackComponent},
	Building = {HealthComponent}
}
local function indexFunction(self, key)
	local value = rawget(self, key)
	if value then return value end
	for _, component in ipairs(getmetatable(self)[1]) do
		value = rawget(component, key)
		if not value then continue end
		return value
	end
end
for className, recipe in Recipes do
	Recipes[className] = {__index = indexFunction,[1]=recipe}
end
local function createObject(recipeName)
	return setmetatable({}, Recipes[recipeName])
end

local soldier = createObject("Soldier")
local building = createObject("Building")

print(soldier:getHealth())  -- 100
soldier:takeDamage(30)
print(soldier:getHealth())  -- 70
soldier:attack(building)
print(building:getHealth()) -- 70
print(building) -- {health=90}
print(soldier) -- {health=70}
print(createObject("Building")) -- {}

Screenshot 2025-03-10 at 9.12.14 PM

I made it lighter by doing one layer index if doesn’t exist check from component itself, therefore multilayer indexing costs isn’t costing simple gets when you know the explict locations already:

local HealthComponent 
HealthComponent = {
	maxHealth = 100,
	getHealth = function(self) 
		return rawget(self,"health") or HealthComponent.maxHealth 
	end,
	takeDamage = function(self, amount) 
		self.health = math.max(0, (self:getHealth()) - amount)
	end
}
local AttackComponent
AttackComponent = {
	damage = 10,
	attack = function(self, target) 
		target:takeDamage(AttackComponent.damage)
	end
}

See, memory usage isn’t that important, for me you over-complicate stuff instead of making it actually better

OOP is ment to be more memory-intense, but in return you gain a lot more scalability and readability than in procedural or functional programming

If you’re really concerned about few bytes metatable takes, you can try using Composition

Instead of storing methoods under classes, you remove self and use object & editor rather than objects

Example code:

function Car:drive()
    self.Distance += 1
end
function CarEditor.DriveCar(Car: Car)
    Car.Distance += 1
end

Note: Performance gain is minimal, and you should aim for redability over code-optimizations, best place to optimize your game is at the structure level, it might require more work, but it also provides you a lot better results

Edit: grammar

1 Like

Please use the conventional luau OOP.
This will become impossible to maintain, you went for readability, but this is unreadable.

You should make classes, in ModuleScripts, and require them when you need to, here’s your code converted to conventional OOP:

--Base class, since all of your entities share health values.
local Entity = {}
Entity.__index = Entity

export type Entity = typeof(setmetatable({} :: {
	Health: number,
	MaxHealth: number,
	Damage: number,
}, Entity))

function Entity:TakeDamage(damage: number): ()
	self.Health = math.max(0, self.Health - damage)
end

return table.freeze(Entity)
--Soldier class, which adds a Damage member, and extends Entity with :Attack
local Entity = require "./Entity"

local Soldier = setmetatable({}, Entity)
Soldier.__index = Soldier

export type Soldier = typeof(setmetatable({} :: {
	Health: number,
	MaxHealth: number,
	
	Damage: number
}, Soldier))

function Soldier.new(): Soldier
	return setmetatable({
		Health = 100, 
		MaxHealth = 100, 
		Damage = 30
	}, Soldier)
end

function Soldier:Attack(Target: Entity.Entity): ()
	Target:TakeDamage(self.Damage)
end

return table.freeze(Soldier)
--Building class, which adds nothing, but it has a constructor...
local Entity = require "./Entity"

local Building = setmetatable({}, Entity)
Building.__index = Building

export type Building = typeof(setmetatable({} :: {
	Health: number,
	MaxHealth: number
}, Building))

function Building.new(): Building
	return setmetatable({
		Health = 100,
		MaxHealth = 100
	}, Building)
end

return table.freeze(Building)

And finally, your script that displays health values:

local Soldier = require "./Soldier"
local Building = require "./Building"

local soldier: Soldier.Soldier = Soldier.new()
local building: Building.Building = Building.new()

print(soldier.Health)--100

soldier:TakeDamage(30)
soldier:Attack(building)

print(soldier.Health)--70
print(building.Health)--70

This is much more readable, and it has separation of concerns, not everything is put into the same script. Finally, it is also modular, you can require these classes wherever you need them.

OOP was never designed with memory efficiency in mind, or at least not in lua, where oop is not natively supported. OOP trades memory for maintainability, and readability. It makes things easier, if your goal was raw performance, you would’ve never even chosen the OOP paradigm to begin with.

Hope this helps.

I think you’re misunderstanding composition.
Composition is an alternative to inheritance in OOP, it is not an alternative to OOP.

instead of:

//inheritance in C++
class Derrived : Base {

}

you compose the derrived object of a base class object:

class Derrived {
public:
   Base base{ };
}

In composition, the base is inside of the derrived class as a member.
So your example would be:

--this example actually has nothing to do with inheritance, idk why I'm trying to even change this example.
function CarEditor:Drive()
    self.Car.Distance += 1 --car is inside of the derrived class
end

You are just passing the target car as an argument, which is not composition, but a more procedural approach, is that what you mean?

Composition itself is alternative to OOP, but someone one day added this concept to OOP

Composition itself is structure taken from Entity Composition System where each frame each entity is updated through editors

1 Like

Omg, I get what you mean.
Composition is an alternative to inheritance in OOP, not an alternative to OOP. You are refering to ECS, which is an architectural pattern in game development.

Can you clarify this:

because OOP composition has existed way before ECS

Oh, then maybe i shortened the naming

But yh, i had ECS in mind, also didn’t knew Composition came first but still it doesn’t matter, thx for correction

1 Like

Both @scavengingbot and @0786ideal made good points.

I analyzed your structure @scavengingbot , but to default to what works for you instead of questioning how to make the idea I have in hand is not help I want to take.

To put frankly, “Nah, Imma do my own thing”

As for @0786ideal , just wanted to mention, only reason I declared self as an argument in the function, it’s cuz I wrote the function within the table as a value. Like :man_facepalming:t6: read to understand, don’t just read to react

Although I don’t fully have it pictured or the example I’ve made doesn’t do service to what I have in mind, thanks to the feedback from @scavengingbot . I still think there something I stand to gain from my initial idea if adjusted.

Like in your version classes or objects only inherit from one parent, and that parent gets one parent

sounds neat as a chaining mechanism, but it’s like IRON BEAM, once you weld pieces to make it longer, it’s going to be a hard rewrite to cut in the middle and add different things, while my version ideally will be recipe, only limitation I see is each component take other existing methods from the object, which is cool, but wondering how do I make more streamlined.

P.S. @scavengingbot it’s not unreadable, I just put no effort to making it pretty, since it’s test code

1 Like

Inheritance in OOP is mostly avoided, and as i mentioned earlier you should aim for Composition, which allows you to build your objects like toy bricks, this way it’s far more flexible and easier than advanced system

Nah Imma do my own thing

stop posting on here broski, it’s not productive when your not adding what’s currently being built and just saying “if it ain’t like how I do it, it aint right”, just cuz there is a set path, and people notice things that set path doesn’t offer, don’t belittle them from trying to experiment and nag them on following the path you perfer, im simply asking if y’all got any ideas, on how can keep what I got but build to making it far closer to the idea at hand

I give you the idea, it’s not how i prefer it, because inheritance is also usefull, key aspect is to use it when you need to, not using it everywhere

There is no perfect way to do something, and Inheritance only makes sense when it’s one level, otherwise it become very complex and hard to maintain, no matter what you’ll do