1. Superclass ignores overridden methods from subclass
--Base.luau
local Base = {}
Base.__index = Base
function Base.new()
local self = setmetatable({}, Base)
self:Init()
return self
end
function Base:Init()
print('Base initialized')
end
return Base
--Mid.luau
local Base = require('./Base')
local Mid = {}
Mid.__index = Mid
function Mid.new()
return setmetatable(Base.new(), Mid)
end
function Mid:Init()
print('Mid initialized')
end
setmetatable(Mid, Base)
return Mid
Output: Base initialized
My solution: Using classic utility by rxi (I tried triggering Init function in subclass instead of superclass just triggers more problem in my inheritance chain)
2. Overridden method called by superclass ignores changes in subclass constructor
--Base.luau
local Object = require('./classic')
local Base = Object:extend()
function Base:new()
self.mythicValue = "Waits for being overridden"
self:Init()
end
function Base:Init()
print(`Base initialized mythic value is: {self.mythicValue}`)
end
return Base
--Mid.luau
local Base = require('./Base')
local Mid = Base:extend()
function Mid:new()
Mid.super.new(self)
self.mythicValue = 15
end
function Mid:Init()
print(`Mid initialized mythic value is: {self.mythicValue}`)
end
return Mid
Output: Mid initialized mythic value is: Waits for being overridden
My solution: task.defer(self.Init, self) but it feels like a weird workaround just to make basic inheritance behave properly
So, while both of my solutions technically solve the issues, they feel weird when this kind of basic behavior should be simpler to implement in any object-oriented structure, does anyone have cleaner solutions for this?
I would say just use ECS, but that has its own problems mostly just organizational (You still end up with a lot of scripts and components to organize and debug).
Problem 1, the base.new() functions calles init first, then the metatable change happens.
Solution: You can create a factory module to autoinitialize your objects for you. Not sure if this is the best but its my most immediate idea.
local autoInit(object)
local obj = object.new()
obj:Init()
end
return autoInit
Problem 2 seems to be the same root problem as problem 1 with the init function inside the constructor.
as mentioned above, luau is a language where I would recommend composition over inheritance. Architectural patterns that benefit from Data Oriented Design such as ECS are a good fit for it, specially since metatables are our way to imitate class inheritance functionality, given we don’t actually have classes.
reason of not initialize class inside constructor because i want override init in my subclasses.
thanks but im not looking for optimization and classic module provides me a very good class i can get same results with typescript classes my 2. problem isn’t just about lua classes its a general oop question so im not looking for classes without metatables
why dont you just have same method check what class do you use and apply initiation acordingly?
Since its metatable OOP it will end up as a better solution anyways.
The overwriting and constructor mechanism of the module I mentioned is far cleaner. I suggest giving the constructor mechanism of the 2.0 in the documentation a try. When the first constructor (base class) is called, the values in the child class already exist and has been overwritten, therefore the base class constructor will always have the object with its values already updated.
I was tried class++ like 2 days ago and results was same i dont want achieve same thing with more complexity i already have solutions i just looking for simpler/cleaner ones the 2. problem also happens in typescript classes something in logic is wrong but i can’t figure it out
I say you should give the new version a go then. It’s not complex at all. It solves what you’re trying to do in a more sequential order, without depending on task.defer(). Though, your choice.
I think class++ just adds limitations and i dont know is it stable or not, while the classic module solves my problem with a single metamethod and one logic trick and classic module is made 11 year ago and a common stable utility for lua scripters. If I had to choose, I’d go with dthecoolest’s solution over task.defer
Your use case is actually a really good one for ECS.
In this example, what if you want to create a hitscan laser that also explodes on impact like a bomb? Under inheritance this complicates things.
Under ECS however, classes only hold data (properties) and all behavior is handled externally through “systems”
local bullet = Entity.new()
Entity.addComponent(bullet, DamageComponent.new(5)) --now deals 5 damage
Entity.addComponent(bullet, BombComponent.new(15)) --now explodes on a radius of 15
Entity.addComponent(bullet, HitScanComponent.new(50)) --now acts under hitscan
BulletSystem.fireBullet(bullet) --performs behavior based on components
code implementation example
--entity.luau
function newEntity()
local e = {
components = {}
}
return e
end
function addComponent(e, c: {[string]: any} & {type: string})
e.components[c.type] = c
return c
end
function getComponent(e: Entity, type: string)
return e.components[type]
end
return {
new = newEntity,
getComponent = getComponent,
addComponent = addComponent,
}
--HitscanComponent.luau
return {
new = function(range: number)
return {
type = 'Hitscan',
range = range
}
end
}
--ProjectileComponent.luau
return {
new = function(velocity: number)
return {
type = 'Projectile',
velocity = velocity
}
end
}
--BulletSystem.luau
function fireBullet(e: Entity)
if e.components['Projectile'] then
--create a projectile moving in Projectile.Velocity direction
elseif e.components['Hitscan'] then
--create a raycast using Hitscan.range
end
end
function bulletImpact(e: Entity)
if e.components['Bomb'] then
--blow up in a radius and apply damage from components['Damage']
end
end
return {
fireBullet = fireBullet,
bulletImpact = bulletImpact
}
I probably will implement :Hit method of bomb to laser just it (for large codebase i would create behavior modules for store methods waiting for get implemented by classes)
I actually was used this logic but this part of system i cant understand because a projectile needs store fields and lifecycles in this logic and this pushed me to use OOP
This logic is good but about extensibility i dont know really. as an example what will happen if a bomb and some other components should move different including projectile, or bomb should move a part of bomb model on every projectile step/update