Which is more efficient? OOP vs Table with Functions

Hi, so what is the difference in terms of efficiency for the two approaches:

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

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

2 Likes

Here’s a topic you might want to look into: Deeper understanding of metatables

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

Call NPC.TakeDamage(EnemyData,15), runs instantly basically.

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

2 Likes

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?

1 Like

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.

2 Likes

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. :slight_smile:

1 Like

Heres what I know:

  • 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!

1 Like

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

1 Like

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.

1 Like

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.

2 Likes

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

However, the closure-based approach utilizes upvalues and also enables you to achieve private values without exposing them to the environment.

Can you clarify what you mean here? Do you mean closure-based approach as in the ability to implement private/hidden values?

Because with metatables, you are pointing towards an already existing table, versus creating more tables.

1 Like

Yes.

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
2 Likes

this is horrible, worse than metatables, also I might not be a fan of metatables but get your facts straight when saying __index lookups are slow.

you can have private functions / methods and variables within your module already

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.

1 Like

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

1 Like

You know you can just, define the functions outside of the local function and just set a reference to them, right?

then the functions won’t be able to access the private members…?

1 Like

Then those private values can be put into a scope that both allows for memory efficiency and still gives the ability to use the private values.

Also, the memory efficiency of not having the same memory addresses for the same functions is rarely relevant.

1 Like

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