All about Object Oriented Programming


#1

ALL ABOUT OOP!

Prerequisites
  • An understanding of meta-tables (although the required code will be explained)
  • How tables work and a competent grasp of the Lua syntax

Parts

What is OOP?
How does it help me?
How do I make this work in Lua?
Integrating with module scripts
What about inheritance?

What is OOP?

OOP stands for Object Orientated Programming and is a way of laying out code in a more friendly way whilst also keeping large projects organised. You have used objects in programming, even if you didn’t know. Things such as Parts or models are objects. This may seem obvious however most of the ROBLOX api is made up of objects. Even Vectors are objects in the ROBLOX api. Lets define what an object is.

“An object can be a variable, function, or data structure. In the object-oriented programming paradigm, “object” refers to a particular instance of a class where the object can be a combination of variables, functions, and data structures.” -Wikipedia

In simple terms it is simply something which contains data about itself and functions to manipulate that data more easily.

For example lets say a variable “brick” is a Part. It has some data, its size, colour, shape, position and it also have some functions associated with it, Destroy, Clone, GetMass etc. Manipulating parts is easy for us and it intuitive, but have you ever wanted more functions or your own type of part?

How does it help me?

Dealing with objects is a great way to enforce an idea called abstraction. Abstraction is simply being able to do things without worrying about underlying processes. For example when you do something in Lua it is in fact being run in C which is turned into assembly and then machine code. (Yes I know that isn’t totally accurate but that isn’t what this tutorial is about). Although all that is going on you don’t need to worry about it. Likewise when you make a part in ROBLOX you don’t need to worry about it’s physics or rendering it or any of that, due to abstraction. Not only does this clear up code but it allows collaboration to work much more seamlessly.

Lets say you’re making a racing game. It would be useful to be able to do car = Instance.new(“Car”, Workspace) and then be able to find useful information such as car.Position or use methods such as car:Respawn(). Instance.new is part of the ROBLOX api so we wont touch it however we can make something similar, car.new().

How do I make this work in Lua?

In Lua (or at least the system I will be teaching you) an object is a table with some variables in. A car object might look something like this.

car = {"RacePosition" = "3", "Speed" = 50, "Driver" = "Guest12372", "WorldPosition" = vector(25.31, 3.23, 53.86)}

And each car would be a similar table but with different values at each index, depending on where it is. First lets define the function which will let us make new cars. Our own personal Instance.new() sorta function.


function newCar(position, driver, model)

end[

However this will get quite annoying very fast. Lonely functions are not a happy sight in OOP. So in Lua we should have a table to contain all of our functions. I will name this table Car (it will become obvious why later). Every function related to the Car object will be in this table. So now the code will look like.

Car = {}
Car["new"] = (function(position, driver, model)

end)

The problem with this is it doesn’t look very readable. Using some special Lua syntax candy we can do this instead.

Car = {}

function Car.new(position, driver, model)

end

This looks much nicer. Just by looking at it we can see what the function is designed to do. Since this function is creating new objects it is a special type of function. It is known as the constructor. It constructs new Car objects (yeah programmers are creative). Also note, here is a class called Car which makes Car objects. The constructor and functions are the class whereas the things made by the constructor are the objects.

Since each Car object is just a table, a new table will need to be constructed which all the relevant data in it.

Car = {}

function Car.new(position, driver, model)
    local newcar = {}

    newcar.Position = position
    newcar.Driver = driver
    newcar.Model = model

    return newcar
end

Now when we call Car.new() with the relevant arguments it will give us back a nice table. It’s organised but not very useful. There are no functions. It might be tempting just to put the functions in the table when it is constructed however this is both inefficient and messy. Since the functions are the same for every Car object they only need to be made once so it is better to provide Lua with the functions when it is trying to find a variable but cannot find it. Lets say we are trying to get the driver of a car. We would do driver = Car.Driver. Lua looks in the car and finds the variable straight away without any extra variables in that table getting in the way. It would be useful if we could detect if the variable we are trying to find is a function, and in fact using meta-tables we can do exactly that.

Meta-tables are tables with a set of methods which perform various special tasks on other tables. I won’t go too much into detail on this. The meta-method we want to use is the .__index meta-method. It is fired whenever Lua tries to find an index in a table but it has a nil value. We can just redirect Lua to a new table which have all the functions in. Conveniently our Car table has exactly that!

Car = {}
Car.__index = Car

function Car.new(position, driver, model)
    local newcar = {}
    setmetatable(newcar, Car)

    newcar.Position = position
    newcar.Driver = driver
    newcar.Model = model

    return newcar
end

Now if we did newcar = Car.New() and then tried to call a function on car is would look through the Car table too, not just newcar. Lets add a function to our car object to make it more useful.

Car = {}
Car.__index = Car

function Car.new(position, driver, model)
    local newcar = {}
    setmetatable(newcar, Car)

    newcar.Position = position
    newcar.Driver = driver
    newcar.Model = model

    return newcar
end

function Car:Boost()
    self.Speed = self.Speed + 5
end

Now we can do.

newcar = Car.new(Vector3.new(1,0,1), "Guest1892", game.ReplicatedStorage.F1Car)
newcar:Boost()

This creates a new car object and then calls :Boost() on it. What happens behind the scenes is Lua tries to find newcar[“Boost”] however this does not exist, sees that Car is at newcars meta-table’s .__index and then tries to find Car[“Boost”] which does exist! There is also a neat little feature in Lua where if you do function table:Method() self is auto declared. It is the same as doing function table.Method(self). Remember how boop:Beep() is the same as calling boop.Beep(boop). It passes the object to the function allowing the function to perform actions on that individual object. In this case :Boost() can perform the speed increase on individual cars.

Integrating with module scripts

This method of OOP works extremely well with module scripts. Simple put return Car at the bottom of the Car script and the module script will return the Car table ready for use, allowing you to do things like this.

--module script called Car in game.ReplicatedStorage
Car = {}
Car.__index = Car

function Car.new(position, driver, model)
    local newcar = {}
    setmetatable(newcar, Car)

    newcar.Position = position
    newcar.Driver = driver
    newcar.Model = model

    return newcar
end

function Car:Boost()
    self.Speed = self.Speed + 5
end

return Car

--main script

Car = require(game.ReplicatedStorage.Car)

newcar = Car.new()
newcar:Boost()

This gives you a way of neatly splitting potentially big scripts into little chunks which are easy to understand and change if needed.

What about inheritance?

A quick explanation of inheritance to those new to OOP. Inheritance is where a class can ‘inherit’ functions and behaviors from another class. So if we made a new class (type of object) for special types of cars, lets say trucks. A truck is pretty similar to a car so there is no need to rewrite all of the code. Instead you would make truck ‘inherit’ all the methods of car. For the sake of scripting trucks can have power ups will allow special behavior whilst cars cannot. Lets make the truck constructor.

Truck = {}
Truck.__index = Truck

function Truck.new(position, driver, model, powerup)
    local newtruck = {}
    setmetatable(newtruck, Truck)

    return newtruck
end

return Truck

If we did this we would still need to put all of the declaring code into the constructor. This is pretty redundant so instead we would just create a car object inside of the constructor.

Car = require(game.ReplicatedStorage.Car)

Truck = {}
Truck.__index = Truck

function Truck.new(position, driver, model, powerup)
    local newtruck = Car.new(position, driver, model)
    setmetatable(newtruck, Truck)

    newtruck.Powerup = powerup

    return newtruck
end

return Truck

Great. Now the truck is properly constructed however if we tried to do

newtruck = Truck.new()
newtruck:Boost()

It would error saying something along the lines of “attempt to call method ‘Boost’, a nil value”. This is because is looks at newTruck[“Boost”] and sees nil and then looks at Truck[“Boost”] and sees nil. What we really want is for it then to look at Car[“Boost”] as that is where the method is. To do this we simple add a metatable to the Truck table to point Lua to Car. Like so.

Car = require(game.ReplicatedStorage.Car)

Truck = {}
Truck.__index = Truck
setmetatable(Truck, Car)

function Truck.new(position, driver, model, powerup)
    local newtruck = Car.new(position, driver, model)
    setmetatable(newtruck, Truck)

    newtruck.Powerup = powerup

    return newtruck
end

return Truck

Now we could use :Boost() on a truck.

Since Lua will look at the Truck table first it means if you re declared :Boost() in the Truck table it will override the method declared in the Car table. Pretty fun stuff. You can also inherit through as many classes as you wish with very little impact on performance.


Thank you for reading my tutorial on OOP. Obviously it is not perfect and feedback would be great, especially on what I can improve or what you don’t understand! I do not claim to be the best at writing tutorials but I just want to help others to understands useful concepts!


Crossing server-client boundary with custom OOP?
Most efficient way to have semi-large amount of NPCs
Optimal way to do hit detection
Triggering replication of custom objects with __newindex?
Module script help
Best way to store game items?
Data Structure Organization
How far do you go with making objects?
Game Development Resources [MEGA THREAD]
Advice on how to set up script hierarchy for turned based combat system enemies
Codebase involving ModuleScripts (not exactly modular) - requesting feedback
Inheriting in a pseudo-class-based structure
Creating A Furniture Placement System
ModuleScripts General queries?
#2

This is also pretty useful for OOP; http://www.lua.org/pil/16.html

It shows some pretty nifty ways of using inheritance, which I think is pretty cool.


#3

This tutorial pretty much explains that in huge detail.


#4

Nice tut, was clear since I've had courses in OOP but rather Java instead.


#5

What? No audio? HAX


#6

Interesting. I never called it OOP. I just called it framework and metatables.

Glad to know there's a more confusing name for everything I script :stuck_out_tongue:


#7

Never considered setting a metatable to a metatable before. Nice work.


#8

Great introduction to metatables


#9

I want to understand this, but I don't understand this and don't know what to do.

I feel so below everyone here.

I need to stop being a moron with all this theory stuff and get better with all this table stuff, but god damnit it's hard to find where to start.

I only came here cause there's another thread discussing OOP and I'm just here like 'wtf is everyone talking about?' and feeling really stupidly out of place.


#10

http://hastebin.com/oyivihiraf.lua


#11

Probably the best place to learn about metatables would be the Minecraft mod, Computercraft.

It's where I learned all about metatables. By accident too, I was just looking at the vector API to be able to create my own stuff similar to it.


#12

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.


#13

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

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

Now, to use it:

Thing = require(script.Parent.ModuleScript)
obj = Thing.new(3)
print(obj:getValue()) --> 3
obj:setValue(1234)
print(obj:getValue()) --> 1234

I hope this helps!


#14

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


#15

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


#16

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.


#17

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 :wink:) 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?


#18

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.


#19

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

#20

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