When should I use OOP In scripts?

Hello, Im learning about OOP, but Im a bit confused when I should use OOP, and why I should use it.

There is a post just like this, but I have more things to ask about OOP, but I want more thing to be clarified.

What are some instances when I need to use the __index function?

Why do people do something like metatable.__index = metatable?

Why can I use .__index without setting a meta table yet?

Why should I use setmetatable()?

Why should I use getmetatable()

Why should I use __call() over a regular function? Whats the difference?

Why should I even use metatable math functions over just using operators?

How does setmetatable() work?

Why is OOP Important?

Do the most popular games on the platform use them?

and thats it.

I feel most of these questions can be solved through a video tutorial like this one below in multiple series.

Yes, see ProfileService and DataStore2

They create objects and create easy to use functions which if you look in the module are pretty complicated, like this coinStore object.

local coinStore = DataStore2("coins", player)
	local productPrice = Products[productName].price

	if coinStore:Get(100) >= productPrice then
		print("Buying product", productName)
		coinStore:Increment(-productPrice)
	end

doing metatable.__index. = metatable is the same doing: metatable.__index = function(t, v) return metatable[v] end – here “t” is the table where the code couldn’t find “v” in “t” so it fires the .__index metamethod. (You first have to do setmetatable(YourTable, MetaTable) in order to use .__index this way)
A metatable is really just the parent table of another table.
Now it returns metatable[v] so it’s now searching for “v” inside our metatable and IF it can find it in there then return that instead. This logic is used for creating methods for custom instances. When you’re trying to perform a method on a custom object then you’re really just trying to search for a function inside a table. If it’s nil then return to the metatable and get the function from there. Now we use a colon ( : ) when performing the method. When you use a colon then the keyword “self” (which functions just like a variable) will be set to the table to the left of the colon. (In this case our custom object) So our metatable function will still know on which object it should perform the code.

I have made a custom Vector3 module that uses OOP and a ton of metamethods. It’s not the best code for sure but the reason why it’s efficient is because the methods don’t get stored inside every object but instead only inside the parent table (the metatable). This saves a lot of memory and that’s the reason why people use metatables in OOP too.

local DataHolder = {}
local module = {}
-- Axis X = i, Axis Y = j, Axis Z = k
setmetatable(module, DataHolder)

function module.calculateMagnitude(Vector) : number
	return math.abs(math.sqrt(Vector.X ^ 2 + Vector.Y ^ 2 + Vector.Z ^ 2))
end

function module.calculateUnitVector(Vector)
	return Vector / Vector.Magnitude
end

function module.calculateVector(Vector) : nil
	Vector.Magnitude = module.calculateMagnitude(Vector)
	if rawget(Vector, "Unit") ~= nil then
		local Estimate = module.calculateUnitVector(Vector)
		if not (Estimate == Vector.Unit) then
			Vector.Unit = Estimate
		end
	end
	if rawequal(Vector, "V") ~= nil then
		Vector.V = Vector3.new(Vector.X, Vector.Y, Vector.Z)
	end
end

function module.new(X : number?, Y : number?, Z : number?) 
	local Vector = {}
	X = X ~= nil and X or 0
	Y = Y ~= nil and Y or 0
	Z = Z ~= nil and Z or 0
	setmetatable(Vector, module)
	Vector.X, Vector.Y, Vector.Z = X, Y, Z
	module.calculateVector(Vector)	
	return Vector
end

function module.xAxis(Mutliplier : number)
	return module.new(1 * Mutliplier, 0, 0)
end

function module.yAxis(Mutliplier : number)
	return module.new(0, 1 * Mutliplier, 0)
end

function module.zAxis(Mutliplier : number)
	return module.new(0, 0, 1 * Mutliplier)
end

function module:setX(NewX : number) self.X = NewX; module.calculateVector(self) end
function module:setY(NewY : number) self.Y = NewY; module.calculateVector(self) end
function module:setZ(NewZ : number) self.Z = NewZ; module.calculateVector(self) end
function module:IncrementX(Increment : number) self.X += Increment; module.calculateVector(self) end
function module:IncrementY(Increment : number) self.Y += Increment; module.calculateVector(self) end
function module:IncrementZ(Increment : number) self.Z += Increment; module.calculateVector(self) end
function module:DecrementX(Decrement : number) self.X -= Decrement; module.calculateVector(self) end
function module:DecrementY(Decrement : number) self.Y -= Decrement; module.calculateVector(self) end
function module:DecrementZ(Decrement : number) self.Z -= Decrement; module.calculateVector(self) end
function module:Increment(IncrementVector) self.X += IncrementVector.X; self.Y += IncrementVector.Y; self.Z += IncrementVector.Z; module.calculateVector(self) end
function module:Decrement(DecrementVector) self.X -= DecrementVector.X; self.Y -= DecrementVector.Y; self.Z -= DecrementVector.Z; module.calculateVector(self) end

function module:Dot(Vector) : number
	return self.X * Vector.X + self.Y * Vector.Y + self.Z * Vector.Z
end

function module:Cross(Vector)
	local DeterminantI = self.Y * Vector.Z - self.Z * Vector.Y
	local DeterminantJ = self.X * Vector.Z - self.Z * Vector.X
	local DeterminantK = self.X * Vector.Y - self.Y * Vector.X
	local X, Y, Z = DeterminantI, DeterminantJ == -0 and 0 or -DeterminantJ, DeterminantK
	local CrossVector = module.new(X, Y, Z)
	return CrossVector
end

function module:Lerp(GoalVector, alpha : number)
	local Lerp = self
	if alpha ~= 0 and alpha ~= 1 then Lerp = GoalVector * alpha end
	if alpha == 1 then Lerp = GoalVector end
	return Lerp
end

function module.CalculateFuzzy(a, b, Epsilon)
	return a == b or math.abs(a - b) <= (math.abs(a) + 1) * Epsilon
end

function module:FuzzyEq(Vector, Epsilon)
	for _, Axis in ipairs({"x", "Y", "Z"}) do
		if not module.CalculateFuzzy(self[Axis], Vector[Axis], Epsilon) then
			return false
		end
	end
	return true
end

function module:Min(...)
	local args = {self, ...}
	local X, Y, Z = math.huge, math.huge, math.huge
	for _, Vector in args do
		if Vector.X < X then X = Vector.X end
		if Vector.Y < Y then Y = Vector.Y end
		if Vector.Z < Z then Z = Vector.Z end
	end
	return module.new(X, Y, Z)
end

function module:Max(...)
	local args = {self, ...}
	local X, Y, Z = -math.huge, -math.huge, -math.huge
	for _, Vector in args do
		if Vector.X > X then X = Vector.X end
		if Vector.Y > Y then Y = Vector.Y end
		if Vector.Z > Z then Z = Vector.Z end
	end
	return module.new(X, Y, Z)
end

--<< Custom >>--

function module:GetAngle(Vector)
	return math.acos(math.clamp(self.Unit:Dot(Vector.Unit), -1, 1))
end

function module:AverageVector3s(...)
	local Vectors = {self, ...}
	local sum = module.zero
	for _, Vector in pairs(Vectors) do
		sum = sum + Vector
	end
	return sum / #Vectors
end

function module:MapRange(MinVector, MaxVector, MinRange, MaxRange)
	return MinRange + (self - MinVector) * (MaxRange - MinRange) / (MaxVector - MinVector)
end

function module:Clone()
	return module.new(self.X, self.Y, self.Z)
end

--<< Stuff >>--
function module.__index(t, Value)
	if module[Value] == nil then
		if Value == "Unit" then t.Unit = module.calculateUnitVector(t); return t.Unit end
		if Value == "V" then t.V = Vector3.new(t.X, t.Y, t.Z); return t.V end
	else
		return module[Value]
	end
end

function DataHolder.__index(t, Value)
	if t == module then 
		if Value == "zero" then return module.new(0, 0, 0) end
		if Value == "one" then return module.new(1, 1, 1) end
	else
		return module[Value]
	end
end

--<< Operator Functions >>--
function module.__add(t, Value)
	if type(Value) == "number" then
		return module.new(t.X + Value, t.Y + Value, t.Z + Value)
	elseif type(Value) == "table" or typeof(Value) == "Vector3" then
		return module.new(t.X + Value.X, t.Y + Value.Y, t.Z + Value.Z)
	end
	error("Vectors can only be added by numbers or other vectors!")
end

function module.__sub(t, Value)
	if type(Value) == "number" then
		return module.new(t.X - Value, t.Y - Value, t.Z - Value)
	elseif type(Value) == "table" or typeof(Value) == "Vector3" then
		return module.new(t.X - Value.X, t.Y - Value.Y, t.Z - Value.Z)
	end
	error("Vectors can only be subtracted by numbers or other vectors!")
end

function module.__mul(t, Value)
	if type(Value) == "number" then
		return module.new(t.X * Value, t.Y * Value, t.Z * Value)
	elseif type(Value) == "table" or typeof(Value) == "Vector3" then
		return module.new(t.X * Value.X, t.Y * Value.Y, t.Z * Value.Z)
	end
	error("Vectors can only be multiplied by numbers or other vectors!")
end

function module.__div(t, Value)
	if type(Value) == "number" then
		return module.new(t.X / Value, t.Y / Value, t.Z / Value)
	elseif type(Value) == "table" or typeof(Value) == "Vector3" then
		return module.new(t.X / Value.X, t.Y / Value.Y, t.Z / Value.Z)
	end
	error("Vectors can only be divided by numbers or other vectors!")
end

function module.__eq(t, Value)
	if type(Value) == "table" then
		return (t.X == Value.X and t.Y == Value.Y and t.Z == Value.Z) and true or false
	else
		error(Value .. " is not the same datatype!")
	end
end

--<< Printing >>--
function module.__tostring(self)
	return table.concat({self.X, self.Y, self.Z}, ", ")
end

--<< Convertion >>--
function module:ToNativeVector3()
	return Vector3.new(self.X, self.Y, self.Z)
end

return module

I use colons on some functions inside the script. Those just automatically set the “self” variable to the first parameter given inside the function. If we also use the colon outside of the module then it would look something like this: CustomObject:SomeFunction() in this case the first argument will be CustomObject. And inside the module script “self” will be set to that first argument.

1 Like

In my script I actually use 2 metatables. DataHolder is the metatable of “module” and when you create a custom Vector3 then the metatable of that “custom object” will be module. The reason I have 2 is because I had to make unique tables when doing module.zero or module.one and couldn’t just give the same table stored in the script. So i need to return module.new() whenever I try to call “zero” or “one”. So that’s what the function DataHolder.__index(t, Value) does.

I try my best to avoid it. Especially in Lua, OOP usually causes me to do something more that I’m supposed to do. That leads to spaghetti code, code with a difficult-to-understand flow.

Luau has types, and that is usually enough to replace OOP. I store all data as dictionary-like tables in another table and provide functions to do logic with them. In my coding style no function in the tables should be shared; so almost all functions in these are more like callbacks rather than traditional OOP methods. I put these functions into modules. These were my personal opinions by the way, I don’t know if this is what most people do.

If you care about memory (you should), always. However, the __index metamethod should be used carefully if the class’s structure contains nested tables. That table will be shared among all objects (because the nested table is still itself, it’s never shared, and when indexing will be done it won’t be a copy).

The __index metamethod means “when I index this, but if the value I indexed turns out to be nil (doesn’t exist), index this instead” when it is a table. That does not mean that failing indexing metatable will attempt indexing metatable again, that’s what setmetatable will do (I’ll also explain that).

Because it’s a regular table key. Just like you can set table.abc, you can also set table.__index and it actually behaves the same! Everything happens with the setmetatable call.

setmetatable takes the second table and sets that as the metatable of the first table. This means that every metamethod the second table has will “work” with the first table. For example, what we state in __index will work with the first table.

If you are talking about metamethods like __add, then these are completely unrelated. The metamethod __add means that if we try to use + with two tables both having the same metatable, the function in the __add metamethod will be called and the returned value will be what the expression evaluates to.

1 Like