All about Object Oriented Programming

[quote] Well I understand metatables fairly well now and get how they work (still dont understand their use), but this whole OOP thing is like a bloody maze. It reminds me of first trying to understand FilterEnabled.

I take it OOP is the hardest thing Lua has to offer, it sure feels like it.
If any of you have some relatively easy OOP related scripts you don’t mind sharing so I can route through them, I’d be delighted to do so. [/quote]

Here you go, a ModuleScript to create your own custom class:

[code]Thing = {}
Thing.__index = Thing

–A function to construct the object (a constructor)
–Ideally, all values you want the object to have would be initialized here.
function Thing.new(value)
local new = {}
new.Value = value

setmetatable(new,Thing)
return new
end

–Define some methods. The object you’re calling it on will be referred to as ‘self’ inside the methods.
function Thing:setValue(value)
self.Value = value
end

function Thing:getValue()
return self.Value
end

return Thing
[/code]

Now, to use it:

[code]Thing = require(script.Parent.ModuleScript)

obj = Thing.new(3)
print(obj:getValue()) --> 3

obj:setValue(1234)
print(obj:getValue()) --> 1234
[/code]

I hope this helps!

28 Likes

I’d like to thank Emma for this guide because it really helped me with Redirection.

4 Likes

I’m having trouble understanding the use case for this stuff, apologies, but it all seems a bit confusing.

5 Likes

Stick with it Kevin. OOP is extremely useful, not just on ROBLOX, but in the industry. One way to figure it out that I’ve found helpful is using ROBLOX objects as sort of your model internally for how you expect the object you make to operate.

11 Likes

Even though this is two years old, still is a useful tutorial about Metatables (Though maybe @magnalite could reformat this to work on this forum ;)) But I do have a question and I rather not make a separate thread as it could easily be answered and added to the tutorial for future readers…

Once you have made an object using OOP, how can you make your own :Destroy() method that’ll remove the object by itself, so I don’t have to do something like:

Inventory.Primary:Destoy()
Inventory.Primary = nil

To completely remove the object, I thought doing something like:

function Object:Destroy()
local n = self.Name
self.ToolModel:Destroy()
self = nil
print(n, “was successfully destroyed!”)
end

Would work, however it only deletes the model and not the object as self is just a path(?) and not the object itself. How would I delete the object from a method inside the metatable?

8 Likes

Your example is pretty spot on for this. You simply create the Destroy method that internally calls Destroy on the necessary items.

However, setting self to nil isn’t going to do anything. What you’re trying to do is remove the object entirely, but we should just let the garbage collector do that for us.

When you call Destroy() on any normal object in ROBLOX, it will only be GC’d once all references to it are gone. The exact same thing goes for tables. Of course, you can configure your table a bit more by making it have weak keys or values.

In summary, just make your Destroy() method destroy the actual objects that need to be destroyed. And then once your other scripts get rid of any references to that object, it will eventually be GC’d.

21 Likes

I think I’m doing this wrong, following the example of OP for creating and inheriting metatables and their methods/functions. Below in the BaseTool section I create what I call a template for my custom Tool object that all other “tools” (such as a sword or a gun) use for a template. I have an odd thing going on, whenever I call Tool:Destroy() on a BaseTool (Tool) object it’ll destroy it, and when I call RangedWeapon:Destroy() it’ll also be destroyed. But when I try to call :Destroy() on the PropaneTank, whether it be done by calling PropaneTank:Destroy() (when the object is spawned by using PropaneTank.new()) or by calling self:Destroy(), the script will error and say that Destroy is a nil value.

I followed step 2.2 of the original post, except the only change I made was by adding __index inside of the PropaneTank.new() function so I could read the properties of the PropaneTank… But if I remove that then my script will also break… Yeah I’m not sure what’s going on anymore. Here’s my code to see what I’m doing.

---------------------
-- ROBLOX Services --
---------------------
local RunService = game:GetService("RunService")
local RS = game:GetService("ReplicatedStorage")
-------------------
-- ModuleScripts --
-------------------
local Utilities = require(RS:WaitForChild("Utilities")) 
---------------
-- Variables --
---------------
local Weapons = {
	Ranged = {},
	Melee = {},
	Other = {},
}

for Index, ModuleScript in pairs(RS:WaitForChild("Weapons"):WaitForChild("Ranged"):GetChildren()) do
	if ModuleScript:IsA("ModuleScript") then
		local DataToCopy = require(ModuleScript)
		local DataToSave = Utilities:DeepCopy(DataToCopy)
		DataToSave.Type = "Weapon"
		Weapons.Ranged[#Weapons.Ranged+1] = DataToSave
	end
end

for Index, ModuleScript in pairs(RS:WaitForChild("Weapons"):WaitForChild("Melee"):GetChildren()) do
	if ModuleScript:IsA("ModuleScript") then
		local DataToCopy = require(ModuleScript)
		local DataToSave = Utilities:DeepCopy(DataToCopy)
		DataToSave.Type = "Weapon"
		Weapons.Ranged[#Weapons.Ranged+1] = DataToSave
	end
end

for Index, ModuleScript in pairs(RS:WaitForChild("Weapons"):WaitForChild("Support"):GetChildren()) do
	if ModuleScript:IsA("ModuleScript") then
		local DataToCopy = require(ModuleScript)
		local DataToSave = Utilities:DeepCopy(DataToCopy)
		DataToSave.Type = "Support"
		Weapons.Other[#Weapons.Other+1] = DataToSave
	end
end

for Index, ModuleScript in pairs(RS:WaitForChild("Weapons"):WaitForChild("Other"):GetChildren()) do
	if ModuleScript:IsA("ModuleScript") then
		local DataToCopy = require(ModuleScript)
		local DataToSave = Utilities:DeepCopy(DataToCopy)
		DataToSave.Type = "Other"
		Weapons.Other[#Weapons.Other+1] = DataToSave
	end
end


local function FindTool(ToolName)
	for FolderName, FolderContents in pairs(Weapons) do
		for Index, Tool in pairs(FolderContents) do
			-- print(Tool.Name)
			if Tool.Name == ToolName then
				return Tool, FolderName
			end
		end
	end
end

	
local function CreatToolAnimationData(Parent, IndexName, OriginalTable, ToolName)
	local Type = type(OriginalTable)
	local copy
	if Type == "table" then
		copy = {}
		for orig_key, orig_value in next, OriginalTable, nil do
			copy[CreatToolAnimationData(Parent, orig_key, orig_key, ToolName)] = CreatToolAnimationData(Parent, orig_key, orig_value, ToolName)
		end
	elseif Type == "number" then		
		local AnimationObject = Instance.new("Animation")
		AnimationObject.Name = ToolName.."_"..IndexName
		AnimationObject.AnimationId = "http://www.roblox.com/asset/?id="..OriginalTable
		AnimationObject.Parent = Parent
		
		copy = {AnimationObject, nil}
	else
		copy = OriginalTable
	end
	return copy
end

local function FindHighestFiringMode(ToolData)
	local FiringModes = ToolData.FiringModes
	if #FiringModes > 1 then
		local SelectedMode = 1
		for Index, Mode in pairs(FiringModes) do
			for _,M in pairs({"Semi", "Burst", "Auto"}) do
				if Mode == M then
					SelectedMode = Index
				end
			end
		end
		return SelectedMode
	else
		return 1		
	end
end


local Service = {}
Service.RetrieveToolData = FindTool
-------------------------
-- Base Tool Class API --
-------------------------
Service.Tool = nil
do
	local Class = {}
	Class.__index = Class
	function Class.new(ToolName, Player)
		local Data = FindTool(ToolName)
		if Data ~= nil then
			local Tool = {}
	    	setmetatable(Tool, Class)	
			Tool = Utilities:DeepCopy(Data)
			Tool.Parent = Player	
			Tool.ToolModel = Tool.ToolModel:Clone()
			Tool.ToolModel.PrimaryPart = Tool.ToolModel.Handle
			Utilities:WeldTool(Tool.ToolModel)
			Tool.SavedWeldData = Utilities:SaveWeldData(Tool.ToolModel)
			Tool.ToolModel.Parent = Tool.Parent
			Tool.Animations = CreatToolAnimationData(Tool.ToolModel, Tool.Name, Tool.Animations, Tool.Name)
			-- Tool.Equipped
			local Equipped = Instance.new("BindableEvent", Tool.ToolModel)
			Class.Equipped = Equipped.Event
			-- Tool.Unequipped
			local Unequipped = Instance.new("BindableEvent", Tool.ToolModel)
			Class.Unequipped = Unequipped.Event
			
			return Tool
		else
			spawn(function() error("Error there is no such tool named "..tostring(ToolName)..".") end)
			return nil
		end
	end
	
	function Class:Destroy()
		local n = self.Name
		self.ToolModel:Destroy()
		self = nil
		print(n, "was successfully destroyed!")
	end
	
	Service.Tool = Class
end
-----------------------------
-- Ranged Weapon Class API --
-----------------------------
Service.RangedWeapon = nil
do
	local Class = {}
	Class.__index = Class
	setmetatable(Class, Service.Tool)
	function Class.new(WeaponName, Player, AmountOfAmmo)
		local RangedWeapon = Service.Tool.new(WeaponName, Player)
		if RangedWeapon == nil then spawn(function() error("Error, BaseWeapon is nil.") end) return end
		
		local MaximumAmountAllowed = (RangedWeapon.MaxAmountOfMagazines * RangedWeapon.MagazineCapacity)
		if AmountOfAmmo >= MaximumAmountAllowed then
			AmountOfAmmo = MaximumAmountAllowed
		end
		
		setmetatable(RangedWeapon, Class)
		RangedWeapon.SelectedFiringMode = FindHighestFiringMode(RangedWeapon)
		RangedWeapon.TotalAmmo = AmountOfAmmo
		RangedWeapon.MagAmmo = RangedWeapon.MagazineCapacity
		
		return RangedWeapon, AmountOfAmmo
	end

	Service.RangedWeapon = Class
end
----------------------------
-- Melee Weapon Class API --
----------------------------
Service.PropaneTank = nil -- this is the only proper metatable...
do
	local Class = {}
	Class.__index = Class
	setmetatable(Class, Service.Tool)
	function Class.new(Parent)
				
		local PropaneTank = Service.Tool.new("Propane Tank")
		
		local DamagableTag = Instance.new("BoolValue")
		DamagableTag.Name = "Damagable"
		DamagableTag.Value = true	
		DamagableTag.Parent = PropaneTank.ToolModel
		
		PropaneTank.BackingTable = {IsDead = false}
		PropaneTank.CreationTimeStamp = tick()
		PropaneTank.Name = nil ; PropaneTank.BackingTable.Name = "Propane_Tank" ; PropaneTank.ToolModel.Name = PropaneTank.BackingTable.Name
		PropaneTank.Health = nil; PropaneTank.BackingTable.Health = 5	
		PropaneTank.Position = nil
		PropaneTank.CFrame = nil
		PropaneTank.Parent = nil ; PropaneTank.ToolModel.Parent = Parent
	
		setmetatable(PropaneTank, Class)	
		
		Class.__index = function(self, Index)
			return self.BackingTable[Index]
		end
	
		Class.__newindex = function(self, Index, Value)
			--rawset(self, Key, Value)
			self.BackingTable[Index] = Value		
			-- rawset(PropaneTank.BackingTable, Key, Value)	
			if Index == "Name" then
				self.ToolModel.Name = Value
			elseif Index == "Parent" then
				self.ToolModel.Parent = Value
			elseif Index == "CFrame" then
				self.ToolModel:SetPrimaryPartCFrame(Value)
				self.BackingTable.Position = Value.p
			elseif Index == "Position" then
				--[[
				local OldCF = self.BackingTable.CFrame
				local angles = OldCF - OldCF.p
				self.BackingTable.Position = Value.p
				--]]
				self.BackingTable.Position = Value.p
				self.ToolModel.PrimaryPart.Position = Value	
			elseif Index == "Health" then
				if self.BackingTable.Health == 0 and not self.BackingTable.IsDead then
					self.BackingTable.IsDead = true
					
					local e = Instance.new("Explosion")
					e.Position = self.ToolModel.PrimaryPart.CFrame.p
					e.DestroyJointRadiusPercent = 0
					e.BlastRadius = 15
					e.Parent = game.Workspace

					for Index, Zombie in pairs(_G.Zombies) do
						local Distance = (e.Position - Zombie.Torso.Position).Magnitude
						if Distance <= e.BlastRadius then
							Zombie:TakeDamage(1000)
						end
					end
					
					if _G.MapData then
						for Index, Tank in pairs(_G.MapData.PropaneTanks) do
							if Tank ~= self and not Tank.BackingTable.IsDead and Tank.ToolModel.PrimaryPart ~= nil then
								local Distance = (e.Position - Tank.ToolModel.PrimaryPart.CFrame.p).Magnitude
								if Distance <= e.BlastRadius then
									Tank.Health = 0
								end
							end
						end
					end
					
					-- only for testing
					self:Destroy() -- delay(0.5, function() self.BackingTable.IsDead = false end)
				end	
			end
			-- warn(self.Name.."."..(Index).." property was changed to "..tostring(Value)..".")
		end

		return PropaneTank
	end
	
	function Class:Destroy()
		self.ToolModel:Destroy()
	end
	
	Service.PropaneTank = Class
end

return Service
4 Likes

@GuestCapone

If you do

PropaneTank.BackingTable.__index = Class

Does that fix it? Im hoping it doesn’t cause a circular loop. If that works I can explain why if you would like me to.

1 Like

That appeared to do nothing.

1 Like

What about if you change

Class.__index = function(self, Index)
	return self.BackingTable[Index]
end

to

Class.__index = function(self, Index)
	return self.BackingTable[Index] and self.BackingTable[Index] or Class
end
1 Like

Still nil.

Did a test and removed Class.__index = blah from the .new function and calling either PropaneTank:Destroy() or self:Destroy() would work, so its something going on in with that __index. …Is it overwriting the Tool metatable’s method? or something?

1 Like

@magnalite - I’ve gone ahead and fixed the formatting of your post to work with Discourse :slight_smile: I kept the same formatting and content, just made it work with Discourse’s formatting which doesn’t support [center] BBCode. I’ve also moved it to the Tutorials section. Thank you for this wonderful tutorial!

7 Likes

@Lilly_S Thanks! Much appreciated :slight_smile:

@GuestCapone

The way .__index works is it is called when an index does not exist. In this case when you create an object you create a new empty table with data about the objects in. It has none of the methods in it. To allow your object to have the appropriate methods called on it a metatable with its .__index pointing to the class table is assigned to the table with the objects data in it. When .__index returns a table lua looks into that table to see if the index exists in that too. So what actually happens when you call a method on an object is this.

Object:DoSomething()
Object[“DoSomething”] is nil, does its metatable have an .__index set?
It does, .__index returned SomeClass.
Does SomeClass[“DoSomething”] exist?
It does! call it!

This allows you to assign the methods without duplicating them into every single object you make.
In your case you are making .__index return a value from BackingTable and so it never has a chance to see if the method exists in the Class table.

I’m not sure why my previous suggestion wouldn’t work though :frowning:

12 Likes

Probably because there was no Class[Index], just Class.
And by doing;

Class.__index = function(self, Index)
		return self.BackingTable[Index] or Class[Index]
end

I was able to get the :Destroy() function to work. ^.^ Thanks!

@Lilly_S Thanks for updating and moving the thread!

2 Likes

^-^ Btw I find it helps if I keep my classes in separate module scripts and require them when I need them.

2 Likes

Thanks for this tutorial! I am trying to learn OOP and I have a quick question for anyone who can answer it.

I have an object that has events connected to it (and some values like Power, Agility, etc.)

Is this all that is necessary for a :Delete() function?

function Thing:Delete() for i,v in pairs(self) do if tostring(v) == "Connection" then v:disconnect() end end self = nil end

Or do I also have to iterate through all the values and set them to nil, like self.Power = nil, or is just setting self = nil fine?

1 Like

This is incredibly helpful! I’ve always been vaguely aware of OOP and have used objects once or twice, but never really understood how it worked or how to properly utilize it. This cleared up a lot of questions!

3 Likes

No, that is sufficient. It will GC (garbage collect) the members when all references are lost to them (and if all references are lost to the object, you’re good to go!)

I like to call setmetatable(self, nil) on my code too. It’s a good defensive programming practice, as it means dead objects don’t have methods to call.


You’ll notice the snippet you used is going to be a pain to type out. My solution is to use Maid. In my deconstructors I’ll just deconstruct Maid which cleans up my signals. Here’s an example:

function CameraControls:Disable()
	if self.Enabled then
		self.Enabled = false
		
		self.Maid:DoCleaning()
		self.Maid = nil
		
		self.LastMousePosition = nil
		UserInputService.MouseBehavior = "Default"
	end
end

Source

13 Likes

-snip-

i have found a solution to my question

2 Likes

Would you recommend having multiple Module Scripts for different things?

So there’s gonna be a lot of functions for weapons in my game; same for AI. Would it be worth having two Module scripts. One called Weapon and one called AI? Or should I just use one?

Only advantage I can think of for using 2 is organisation; but I think that’s important ( to me atleast! ).

1 Like