Hi, so what is the difference in terms of efficiency for the two approaches:
In a Module Script, do Object Oriented Programming (OOP) by using metatables to create a class and then creating properties and methods inside that class.
Example from Gemini for the sake of time.
-- ModuleScript
local NPC = {}
NPC.__index = NPC
function NPC.new(name, health, speed)
local self = setmetatable({}, NPC)
self.Name = name
self.Health = health
self.Speed = speed
return self
end
function NPC:TakeDamage(amount)
self.Health -= amount
end
function NPC:Heal(amount)
self.Health += amount
end
return NPC
OR
In a Module Script, create an empty table (for storage purposes) and a template table (cloned for each new object), and then create functions that serve as the methods to edit table values.
Example from myself.
-- ModuleScript
local NPCManager = {}
local _NPCs = {}
NPCManager.NPCNew = function(name, health, speed)
local t = {
Health = health,
Speed = speed
}
_NPCs.name = t
end
NPCManager.NPCTakeDamage = function(name, amount)
_NPCs.name.Health -= amount
end
NPCManager.NPCHeal = function(name, amount)
_NPCs.name.Health += amount
end
return NPCManager
Background
I have read posts and seemed to experience that the first method improves organization and readability. For encapsulation, there is no difference since I will use functions/methods anyway.
The main thing that convinced me OOP could be better in efficiency was inheritance. The first approach supports inheritance, which is the possibility of creating other classes that inherit properties and/or methods from another class. This is harder to do with my second approach, mainly because of the complexity of indexing, storing, and accessing those values.
Anyway, from what I’ve read (in the above topic), the second option is better because the first option goes like this:
You call NPC:TakeDamage, it checks in itself if it has function TakeDamge (self.TakeDamage), it doesn’t have it so it checks in it’s metatable (whic is NPC = {}), NPC (the module) does have it and runs the function NPC.TakeDamage.
Which is much slower than the second option: (I’ll be using my own example for the 2nd option as your 2nd option isn’t too efficient :p):
My Example
local NPC = {}
function NPC.new(health, speed)
local EnemyData = {
Health = health,
Speed = speed
}
return EnemyData
end
function NPC.TakeDamage(EnemyData,Dmg)
EnemyData.Health -= Dmg
end
return NPC
Majority of the time you don’t actually need to use a metatable but there will be times when you will need it (I can’t think of any examples but I remember reading some post about people using metatables because they had to).
Hell if you really wanted to, you can just put the functions inside the table when you created it to.
Another Example
local module = {}
function module.new(health, speed)
local EnemyData = {
Health = health,
Speed = speed
}
function EnemyData.TakeDamage(Dmg)
EnemyData.Health -= Dmg
end
return EnemyData
end
return module
Though doing this is probably slower. You should really only do this if you’re making 2 classes in the same Module
Hi! Thanks for your help! The reason I stored the NPC.new table in another table located in the module is because I may need to access it across multiple scripts. If you only return it, that means you’re only able to use it in one script?
Yes you’re right. However there are only so many strings you can set as an index before it gets hard to, I guess, track the enemy you’re attacking. So depending on your system, I’d just check for all BaseParts inside the NPC and put that in a table as an index which is set to the table you created:
local module = {}
module.PartToEnemyData = {} -- { [Part] = EnemyData }
function module.new(NPC:Model, health, speed)
local EnemyData = {
Health = health,
Speed = speed
}
for _,Part in NPC:GetDescendants() do
if Part:IsA("BasePart") then
module.PartToEnemyData[Part] = EnemyData
end
end
return EnemyData
end
-- other functions
return module
That way, if you were making, let’s say a Gun system, and the gun shoots at an Enemies LeftHand, you can get the Table you created doing module.PartToEnemyData[Part] and it’ll give you the the Table you created, thus allowing you to call NPC.TakeDamage(module.PartToEnemyData[Part],15) using that Gun script.
Do note though you’re gonna have to make function that clears the stuff I guess when the NPC dies so you don’t cause memory leaks.
Hello, I know this is not actually the response to your question. However, I recently discovered something while reading the documentation couple months ago. Just wanted to share since It’s quiet relevant.
I use the second option you gave in your post however, I cast them to “Attribute” system. Which allows me to do calculations on them. When you use attributes, they appear on “Properties” tab on a seleceted item. I think it makes accessing or modifying way fast and simple. I even use attributes for some logic switches. It works fine unless you want to reset/delete or spawn something. For that I just copy the Attributes to a Value sheet and store them before deleting or reseting stuff. Then just cast them back to the item or player. Oh, It’s even more convenient if you want to see immediate changes when you “test” your place over studio.
I can go even further with details or can give examples with my code. Hit me up if it intruges you.
OOP w/ Metatables: Slower in terms of calls, but more memory efficient
Tbls w/ ordinary funcs: Faster calls, but less memory efficient
So basically, if you care about memory and want the most memory efficient utilization approach; use Metatables.
Otherwise, if you care a lot about call speed, but don’t care too much about memory efficiency, then probably use tbls with functions!
The second is option is going towards DOD principle.
I sugest doing 2nd option and completely doing it DOD.
DOD relies on entities and components. Seperating logic from data. Very efficient
I extremely suggest this for your NPC system especially, cause DOD can help you handle hundreds of NPCs
Where’s your reasoning?
I’m saying the closure based approach is faster, but less memory efficient.
The metatable approach is slower, but more memory efficient.
A closure-based approach is what I would recommend in OO-like structures in Luau as well.
Metatables are helpful when you want to create an inheritance-like system or want to create additional access rules, like in modules such as Class++. However, the closure-based approach utilizes upvalues and also enables you to achieve private values without exposing them to the environment.
Yes, I have heard that OOP w/ metatables is slower because it does some “lookup” stuff. Why do you think Tbls w/ ordinary funcs are less memory efficient? I thought both were almost the same and metatables have more overhead. (Of course, you have to propperly manage the funcs in the “tbls w/ ordinary funcs” methods for this to be true.)
local function test()
local newObject = {}
local privateValue = 2
function newObject:setPrivateValue(newValue: number)
assert(typeof(newValue) == number, "the new value cannot be a number!")
privateValue = newValue
end
function newObject:getPrivateValue()
return privateValue
end
return newObject
end
Instead of telling us “this is horrible” and nothing useful, how about you provide us with a source on why? And quote me when I said “__index lookups are slow” specifically.
well the reason is because unlike metatable oop, every object has different memory addresses for the exact same functions, so that’s not very efficient
I am not creating a new function every time I create a member. Please take a look at my second example.
I have written my functions “outside,” and in fact, is not even connected to the data. The data is stored in somewhere and functions are used to edit that data.
And like what @TheRealANDRO said, you can just declare the functions somewhere else and contain references to those functions to serve as “methods”.