Efficient Object Oriented Programming Tutorial
This tutorial will not be explaining what OOP (aka Object Oriented Programming) is.
I’ll be showing how to create an efficient and organized OOP System.
- Note: I start to create the efficient OOP system at the “what do we do now?” section below
All module scripts in this tutorial will be parented to ServerStorage
The scripts that we will test module scripts with will be parented to ServerScriptService
Things you should know before continuing this tutorial
- Basic OOP Concepts (objects, classes, inheritance)
- Module Scripts
- Metatables
-
.
vs:
function calls | example:Person.shout()
vsPerson:shout()
Let’s Get Started
There are lots of tutorials on OOP in Roblox Lua. However, there seems to be key issues consisting of some combination of the following:
- Memory usage (including memory leaks)
- Complexity
- Readability
- Flexibility
- Issues with inheritance and event connections
- Lack of using DRY method (Do not repeat yourself)
I’ve been developing Lua OOP systems for a couple years now. They’re not easy to conceptualize. I’ve scrapped countless ideas either having issues with inheritance, memory leaks, or just not liking the structure or looks of the system.
The best post I could find about OOP was a post first written in… 2014. (edited in 2020 Jul.)
I would advise you to look it over first since it’s much better at explaining the concepts of OOP.
Bad OOP Implementation Example
First let’s explore the issues that I’ve seen with other OOP systems
Here’s an example of a very simple Person class represented inside a module script:
local Person = {}
function Person.new(name, age)
local self = {}
self.name = name
self.age = age
function self.shout()
print(self.name .. ": AHHHHHH")
end
return self
end
return Person
This is what I normally see when people try to implement OOP. in this example, we have a Person class, which creates an object through a function called new.
Our object has a:
- name property
- age property
- function which lets the person shout in the output bar called shout.
First thing I want to make clear is that this OOP system does work. It’s great for beginners since it’s simple and easy to read.
However, it sacrifices one thing: Memory usage
Why is that?
The issue at hand is that we have created a function inside the object itself.
Let’s say we create two objects. That means both objects have its own shout function.
Two functions that do exactly the same thing
This uses up memory that could otherwise be avoided.
Here’s what we want:
- To be able to call the function from the object
- Not have the function located directly inside the object
- Only create one function that is used by all objects
- Still is easy to read and execute (in a perfect world)
How do we solve this?
With metatables
Specifically with the __index
metamethod.
Metatables are awesome, but they can get pretty confusing quickly. I hope you already have a basic understanding with the concept of metatables and metamethods.
Instead of adding the shout
function into the Person object, we’ll add them to the Person class, which is the same place the new
function is located. In this example, that would be Person
.
Example:
local Person = {}
function Person.new(name, age)
local self = {}
self.name = name
self.age = age
return self
end
function Person.shout(self)
print(self.name .. ": AHHHHHH")
end
return Person
Notice how the shout function is outside of the new
function.
Also note how the shout function includes self as a parameter. The self lets us target a specific object that calls the function. However, there’s a better way to write it.
This does the exact same thing as the shout function in the above example:
function Person:shout()
print(self.name .. ": AHHHHHH")
end
We replace the .
with a :
in the function definition. This passes self as the first parameter without needing to write it. And when we call the function from our object, well use :
as well
object:shout()
Metatable Time
You might notice with the above example we have no way to call the function directly from a Person object, since the function is located inside the Person class. You would be correct.
Now it’s time to play with metatables.
Like I said before, I’m not here to explain how metatables work. So i’m just going to provide a working scenario here with an updated Person module script
local Person = {}
Person.__index = Person
function Person.new(name, age)
local self = {}
self.name = name
self.age = age
return setmetatable(self, Person)
end
function Person:shout()
print(self.name .. ": AHHHHHH")
end
return Person
Here are the changes:
-
Person.__index = Person
| We added a field__index
to Person -
setmetatable(self, Person)
returnsself
after setting the Person class as its metatable.
To sum it up, whenever you try to access a field that the Person object does not have, it will look inside the Person class to see if it has it.
But is it good enough?
Now we can access the class functions from our objects! And when we create multiple objects, it does not create more functions, reducing the amount of memory we use!
This is very similar to the OOP tutorial I listed above, with a little of my personal coding style mixed in. This system is a great option for developers with larger games, since the metatables save us memory.
We can add inheritance to this system, however, the features of this system are lacking. Here’s problems with this system:
- If you want to add metamethods to classes, you will have to create one in every class.
- The new function (the new function should not be located directly inside the class)
- The inability to use functions of the class inside the new function through
self
- Just not cool enough
What do we do now?
Here’s where the real tutorial starts. If you got through here, good job.
If you jumped here without reading the above, I hope you understand basic OOP systems in Lua.
We are now going to scrap everything we just made
Let’s create a new module script inside ServerStorage. This is going to be where we create the concept of a class. I’m going to call it “Rcade” (random name I thought of).
Lets add a function called class to the module script with the following logic inside:
local Rcade = {}
function Rcade.class(newClass)
newClass.__index = newClass
return setmetatable(newClass, Rcade)
end
return Rcade
Notice how the function receives a parameter called newClass. This is where we receive default values for a class. I’ll explain it more in detail later on. The function also sets Rcade as the metatable. Finally it returns newClass
Now we need to add a metamethod to Rcade. First, we will add a __call
metamethod to Rcade. This will allow us to call a table like it’s a function. In this case, the table would be the class. We are using __call
to replace the new
function
Here is Rcade with a __call
metamethod added:
local Rcade = {}
function Rcade:__call(...)
-- note how self in this case points to the object's class
local object = {}
setmetatable(object, self)
if self.__init then
self.__init(object, ...)
end
return object
end
function Rcade.class(newClass)
newClass.__index = newClass
return setmetatable(newClass, Rcade)
end
return Rcade
Instead of creating an object by calling class.new()
We will now do it by calling class()
You might be confused about the code inside the __call
metamethod.
If you’re confused, I don’t blame you
Here’s a step by step on what it does:
- It receives a variable amount of parameters indicated by the
...
- It creates a table which is our object
- Sets the object’s metatable to be its class.
- Looks to see if the object’s class has a function called
__init
and if it does then it runs it, passing the object and the variable amount of parameters. - Returns the object
But where did this __init
function come from? It’s not a metamethod recognized by Lua.
It’s a function you will define inside each class yourself. The __init
function is where we set an object’s properties.
Lets create an example class called “Person” again that uses Rcade.
Here’s the basic structure:
local Rcade = require(game.ServerStorage.Rcade) -- change this to where ever your Rcade is located
local Person = Rcade.class{}
function Person:__init(name, age)
self.name = name
self.age = age
end
function Person:shout()
print(self.name .. ": AHHHHHH")
end
return Person
Notice how when I called the class function inside Rcade, I did
Rcade.class{}
instead of Rcade.class()
- curly brackets vs parenthesis
What’s the difference?
Rcade.class{}
is equivalent to Rcade.class({})
.
In other words, It passes a table as the first parameter. Just less code to write and easier to read, though either way is fine.
Now let’s actually create an object as an example that it works.
Let’s create a new Script. Lets place the script inside ServerScriptService
with the following code inside:
local Person = require(game.ServerStorage.Person)
local newPerson = Person("Jimmy", 100)
print(newPerson.name)
print(newPerson.age)
newPerson:shout()
This example will print out this in the output bar:
Now we need to add inheritance. With inheritance, we can have a class inherit properties and functions of another class.
To help explain what it means, here’s an example:
- Let’s say we have a class called “Person”
- Let’s say we have another class called “Student” that inherits from Person.
A Student is a Person, but a Person is not always a Student
A Student should have every property and function that a Person has.
However, a Student may also have properties or functions that a Person doesn’t have.
So how do we add Inheritance?
The first step to do this is by adding a __super
property to classes that inherit from another class. (__super refers to the class it inherits from)
Lets create a new class called Student, mimicking the example from before.
Here’s our student class that will inherit from our Person class
local Rcade = require(game.ServerStorage.Rcade)
local Super = require(game.ServerStorage.Person)
local Student = Rcade.class{__super = Super } -- <-- added super class here
function Student:__init(name, age, id)
self.__super.__init(self, name, age)
self.id = id
end
return Student
Here’s what’s different from the Person class:
- Added __super property to the class which points to what it inherits from
- Inside __init we call the superclass’ __init, passing the object and the Person parameters.
- Added a new parameter to the
__init
function called “id” and set it inside the object. - No functions were defined under the Student class
With a student object, we can index:
-
student.name
,student.age
which is inherited from the Person class -
student.id
which is defined inside the Student class
However, we’re not done yet. As of right now, if we have a student object and try to call student:shout()
, the script will error out since there is no shout function inside the Student object.
We want our Student class to look inside our Person class to find the shout function.
Since Rcade is the metatable to all classes we create, we need to add an __index
metamethod to Rcade.
Here is the metamethod
function Rcade:__index(index)
-- self is the class
if rawget(self, "__super") then
return self.__super[index]
end
end
When we call __super on classes, This checks to see if our class has a __super property, and if it does it will return the superclass. If not it will return nil.
So the current full Rcade script will look like this:
local Rcade = {}
function Rcade:__call(...)
-- note how self in this case points to a class
local object = {}
setmetatable(object, self)
if self.__init then
self.__init(object, ...)
end
return object
end
function Rcade:__index(index)
-- self is the class
if rawget(self, "__super") then
return self.__super[index]
end
end
function Rcade.class(newClass)
newClass.__index = newClass
return setmetatable(newClass, Rcade)
end
return Rcade
Now it wont error out when we try Student:shout()
The cool thing with this system is that we can define metamethods for objects directly inside the class, while looking very clean.
If we try to print out a student object, it gives us a string that points to the memory location holding the table.
Useless
- With the new output bar in Roblox Studio, you can now see what’s inside a table when you print it. However, I still don’t like it.
What if we want to have a custom output when you print an object?
All we have to do is add a __tostring
function inside the object’s class.
Here’s an example of the Person class that has a custom print output for Person objects.
local Rcade = require(game.ServerStorage.Rcade)
local Person = Rcade.class{}
function Person:__init(name, age)
self.name = name
self.age = age
end
function Person:__tostring()
return "name: " .. self.name .. " age: " .. self.age
end
function Person:shout()
print(self.name .. ": AHHHHHH")
end
return Person
Now when we print a Person object, it will print it’s name and age.
Here’s an example of printing a Person object and its output using a script inside ServerScriptService:
local Person = require(game.ServerStorage.Person)
local newPerson = Person("Jimmy", 100)
print(newPerson)
And it outputs:
BUT WAIT
If we try to print out a Student, which inherits from our Person class, it won’t use our tostring
metamethod. It will give us that useless memory location string again.
We need a way to have metamethods also be inherited.
First I want to make clear that this section is purely optional. The way I’m going to show you will increase memory usage. I have yet to figure out a way to cleanly do this. However, it works and is not as bad as the first examples of OOP I showed you.
Adding Metamethod Inheritance
To do this, we will need to edit Rcade once more.
We will change Rcade’s class
function to this:
function Rcade.class(class)
if class.__super then
for key, value in pairs(class.__super) do
local sub = string.sub(key,1,2)
if sub == "__" and key ~= "__super" and key ~= "__init" then
class[key] = value
end
end
end
class.__index = class
return setmetatable(class, Rcade)
end
What we added in steps:
- added a check to see if the class in question had a __super property.
- If it does have __super, loop through every property the __super class has
- If the property has “__” as the first two characters (our metamethods) and is not
__super
and__init
, then add it to the class we are creating.
Now our classes inherit metamethods as well!
Listeners
Now for the final aspect of this OOP system. Listeners. What if we want a certain function of a class to fire for a specific event? Like when a player joins, or a character dies.
First let’s add a blank module script to Rcade. I’m going to name it “Utility”
Utility will be a child of Rcade in the Explorer tab, as shown here:
Inside utility we will add three different functions, as well as retrieve the service “RunService” as shown here:
local Utility = {}
local RunService = game:GetService("RunService")
function Utility:eventListener(bindableEvent, method)
bindableEvent:Connect(function(...)
self[method](self, ...)
end)
end
function Utility:remoteListener(remoteEvent, method)
if RunService:IsServer() then
remoteEvent.OnServerEvent:Connect(function(...)
self[method](self, ...)
end)
elseif RunService:IsClient() then
remoteEvent.OnClientEvent:Connect(function(...)
self[method](self, ...)
end)
end
end
function Utility:bindToClose(method)
game:BindToClose(function()
self[method](self)
end)
end
return Utility
I won’t even begin to explain what’s happening here. All you need to know is how to use it at this point.
So let’s use it
First we need to go inside Rcade AGAIN and add a property. So at the top of Rcade, we’ll insert it as such:
local Rcade = {}
Rcade.Utility = require(script.Utility)
Which results in the complete and final version of Rcade:
local Rcade = {}
Rcade.Utility = require(script.Utility)
function Rcade:__call(...)
-- note how self in this case points to a class
local object = {}
setmetatable(object, self)
if self.__init then
self.__init(object, ...)
end
return object
end
function Rcade:__index(index)
-- self is the class
if rawget(self, "__super") then
return self.__super[index]
end
end
function Rcade.class(newClass)
if newClass.__super then
for key, value in pairs(newClass.__super) do
local sub = string.sub(key,1,2)
if sub == "__" and key ~= "__super" and key ~= "__init" then
newClass[key] = value
end
end
end
newClass.__index = newClass
return setmetatable(newClass, Rcade)
end
return Rcade
Now let’s create a new class called “Server”.
It’s going to include functions called onJoin
, onLeave
, and onClose
.
Here’s the basic layout of our Server class:
local Rcade = require(game.ServerStorage.Rcade)
local Server = Rcade.class{}
function Server:__init()
end
function Server:__tostring()
return "Server"
end
function Server:onJoin(player)
print(player.Name .. " joined")
end
function Server:onLeave(player)
print(player.Name .. " left")
end
function Server:onClose()
print("The server is closing")
end
return Server
In this example, we will make it so:
- when a player joins, the
onJoin
function will fire, with the player being the parameter. - When a player leaves, the
onLeave
function will fire with the player being the parameter - When the server requests to shut down, the
onClose
function will fire
To do this, we will add code to the __init
function as such:
function Server:__init()
Rcade.Utility.eventListener(self, game.Players.PlayerAdded, "onJoin")
Rcade.Utility.eventListener(self, game.Players.PlayerRemoving, "onLeave")
Rcade.Utility.bindToClose(self, "onClose")
end
Note how the eventListeners work.
- The first parameter is self (the object)
- The second parameter is the event you want to listen to.
- The third parameter is the name of the method you want to fire represented as a string.
It’s the same for remoteListener
however with bindToClose it only needs self and the method name. (2 parameters)
Now our Server class should do exactly what we planned it to do.
Let’s test it out
Create a script inside ServerScriptService that contains the following code:
local Server = require(game.ServerStorage.Server)
local newServer = Server()
Now start the game.
It should print something when you join, leave and when the server closes now. Here’s what my output prints out when I join and leave in Roblox Studio:
Default Values
Sometimes we might not want to input every parameter a class has. Let’s take a look at our Person class once more.
Let’s say we create an object without giving the person an age. The issue here is that if we were to try to have the person shout, it would error since you can’t concatenate nil with a string.
How can we fix this? The normal way would look something like this
local Rcade = require(game.ServerStorage.Rcade) -- change this to where ever your Rcade is located
local Person = Rcade.class{}
function Person:__init(name, age)
self.name = name or "NO NAME"
self.age = age or -1
end
function Person:shout()
print(self.name .. ": AHHHHHH")
end
return Person
Inside the __init function, when a property is set you can add a or to set a default value that only is used when the given parameter is nil.
The issue is that if we nil the value inside the object later, then the value would be nil when we index it.
I recommend a different method, one that works really well with the system we have set up:
When we create a class through Rcade, inside the table we pass to Rcade, you can add default values for your objects. So for our Person class example, the default values could instead be created via the code:
local Rcade = require(game.ServerStorage.Rcade) -- change this to where ever your Rcade is located
local Person = Rcade.class{name = "NO NAME", age = -1}
function Person:__init(name, age)
self.name = name
self.age = age
end
function Person:shout()
print(self.name .. ": AHHHHHH")
end
return Person
The default values are located at the top of the script where we create the Person class
local Person = Rcade.class{name = "NO NAME", age = -1}
Now if we don’t give an age parameter when creating an object, indexing it will return -1 no matter what unless a name property is added to the object. Even if we do person.age = nil
it will still return -1 when indexed.
Garbage Collector
If you add a roblox object as a property of an object, you need to nil the property inside the object in order for the object to be cleaned by the garbage collector.
As an example, if we created an object every time a player joins the game, and the object contains the player as a property, we must nil out the value when the player leaves to stop memory leaks from occurring. I don’t know why. Just nil the property when you’re done with them. I usually add a destroy function to objects to easily clean them up.
Conclusion
And we’re done! This was a very long tutorial, And I know I’m not a good teacher. For those of you who got to the end, here’s a reward (even scrolling here took an effort):
Final Example link:
Take this and edit it however you want! Just make sure you understand what is happening. Leads to a better user friendly experience.
(I didn’t want to put it at the top of the post since people would just get the model and not read the post)
Final Words
I really hope you learned something from this post. This is my first ever full post so I hope everything if formatted correctly and is understandable. If you think you can improve this OOP system, by all means go for it. If there's any issues or errors with the code, or any typos, let me know and I'll fix it up ASAP.
Soon I’ll be creating a plugin that makes it very fast and simple to create a basic template for a class.
I’ll also likely create a tutorial on how to create an obby with this system.
Cheers