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. Letās 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 letās 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.
Letās 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?
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 letās 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. Letās 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 metamethod we want to use is the .__index metamethod. 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. Letās 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. Simply 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 behaviours from another class. So if we made a new class (type of object) for special types of cars, letās 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 behaviour whilst cars cannot. Letās 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.
Thanks for reading my tutorial on object oriented programming in Roblox, hope it helped!