December 8, 2022: It has been a while, but I am working on a modern rewrite of this tutorial. The post will be edited with the new article and the old one will be archived if you ever want to look back on it. Right now there is no estimated time for finishing the rewrite, but it will be soon (December - January).
October 8, 2024: Well… I’m a little late on that “estimated time.” Been busy IRL. I do plan on updating this though. No new estimated time for now.
INFORMATION
The goal of this tutorial is to act as a guide and resource for those who want to learn about the principles of Object Oriented Programming in Roblox. The language used throughout the tutorial is Roblox’s Luau which is built off of Lua.
Skills Required
- General Lua/Luau Knowledge
- Basic ModuleScripts
- Tables
INTRODUCTION
To start, we first need to understand that Object Oriented Programming is a type of programming paradigm. A programming paradigm is a way to describe programming languages based on the features they have to offer. Languages can have multiple paradigms and Lua/Luau is a part of this multi-paradigm family. Lua/Luau isn’t the typical class-based object oriented style, but rather another style called Prototype-Based. Trying to explain the differences between these two styles would be pointless unless I explain some necessary concepts first, like what an object is.
OBJECT ORIENTED
The idea of being object oriented quite literally translates to being based around objects. An object for this tutorial can be thought of as a thing that holds data. The data essentially describes the object as well as what that object can do. It can be partitioned into, generally, two parts: fields (properties/states) and behavior (functions/methods). We can also generalize this data to be called the members of said object. For example, an object labelled as a computer may have properties such as the operating system, system name, system type, etc. and also possess behavior such as turning on/off, restarting, sleeping, etc. Something to keep in mind is just because objects can have fields and behavior does not necessarily mean all objects will have both; some might have one or the other, or even none (empty object).
Another important idea is the way in which we classify objects. They can generally be classified under two descriptions: concrete or abstract. A concrete object is relatively specific and can logically exist on its own, while an abstract object is more general and logically cannot exist solely on its own. For example, an apple could be considered a concrete object because it is capable of existing on its own, but an object labelled as a fruit is more general and can be better used to describe the apple object (because an apple is a fruit). The idea of concrete and abstract objects start to give a little intuition for a concept known as inheritance which will be show towards the end of the tutorial.
OBJECTS IN CODE
I think it’s safe to say objects are essentially the core of Object Oriented Programming, but using Luau we need a way to actually represent these objects. We could technically just create variables and functions inside an arbitrary script, but this already presents multiple issues. The first issue is we would have to mentally remember which data represents which object; it would be nicer if we could actually group the data together in computer memory. Another issue is if we wanted multiple, or even worst case hundreds of objects we would have to create new data for each new object. Representing objects in this manner is not only highly inefficient, but it also severely increases the complexity of your code.
That being said, we need a better way represent objects: tables. The table data structure (specifically dictionaries) in Luau is perfect for the representation of objects because it can hold multiple data of multiple types, and also keep all the data stored under one entity. Having the data grouped together is logically better because we can associate the data as being related in the sense that they are all apart of the same table. Tables, however, do not solve the issue of having multiple objects, because we would still have to inefficiently define X amount of tables, where X is the amount of objects we want.
Before we solve this multi-object issue, now would be a good time to show how tables can benefit us for the representation of objects through actual code.
OBJECT EXAMPLE
For the next few sections, we will be working with a Car object described below:
Car
Fields:
Brand, Color, Speed, Driving
Behavior:Drive, SetSpeed, Stop, GetColor
USING TABLES
There are actually two ways you could go about creating objects via tables: outside and inside of the table constructor. You might be confused as to why this even matters, but both methods do not follow the same construction procedure. The first way (outside) is very straightforward, but the second way (inside) introduces concepts that will be useful later and in general.
Outside
local Car = {}
-- fields
Car.Brand = "EpicBrand"
Car.Color = "Blue"
Car.Speed = 0
Car.Driving = false
-- behavior
Car.Drive = function()
Car.Driving = true
end
Car.SetSpeed = function(value)
Car.Speed = value
end
Car.Stop = function()
Car.SetSpeed(0)
Car.Driving = false
end
Car.GetColor = function()
return Car.Color
end
Inside
Alright now comes the fun part: creating an object inside of the table constructor. This procedure will introduce important concepts related to function definition and calling that will be important for later.
Do not forget to seperate items in the table by commas or semicolons because we are inside of the table constructor
local Car = {
-- fields
Brand = "EpicBrand",
Color = "Blue",
Speed = 0,
Driving = false,
-- behavior
Drive = function()
-- how do we get the Driving field here???
-- read below...
end
}
As you can see, we clearly can’t just index the Driving field in the function because the table has technically has not been created yet. So how can we solve this? One easy solution is if we passed the table itself as a parameter of the function, and indexed the Driving field:
Drive = function(tbl)
tbl.Driving = true
end
We would then call the function like so:
Car.Drive(Car)
The only issue with this is it just looks bad in terms of calling the function. Lucky enough there is actually a way we can make this look a lot better. It might be a surprise that using . (dot) notation is not the only way we can call functions. We can also use : (colon) notation, but what does this actually do?
Calling a function with : (colon) notation will automatically pass the data that called the function as the first parameter. For example, we could keep the function definition the same, but call it like this instead:
Car:Drive()
Side Note
You may have seen : (colon) notation being used for function calls outside of this example, but also may have not given it much thought. In Luau, Object:Method(...)
is simply just syntactical sugar for Object.Method(Object, ...)
.
For example, the FindFirstChild method is usually called with : (colon) notation.
local child = Instance:FindFirstChild("childName")
If we were to call FindFirstChild with . (dot) notation instead, we would have to manually pass the Instance that called the function like so:
local child = Instance.FindFirstChild(Instance, "childName")
WHAT IS SELF
This section will be covering, yet again, another way to do something, but it is only really applicable with the creation of objects outside of the table constructor. Before, we defined the function with . (dot) notation, but we can also define the function with : (colon) notation. For example:
function Car:Drive()
-- and what do we do here???
end
Functions that are defined with : (colon) notation are handled a bit differently. If you also call the function with : (colon) notation, the data that called the function will, as expected, get automatically passed into the function, but instead of having to reserve a parameter, it will be assigned to a variable in the scope of the function called self
.
function Car:Drive()
self.Driving = true
end
"self"
is not a keyword, but is used for something in this specific case and does get syntactically highlighted. The name of this variable is an accurate descriptor of something referencing itself; other languages might use "this"
.
You can also call the function with . (dot) notation, but you will have to pass the data manually yourself. The first data you pass will get automatically assigned to the self
variable.
Remember that function definition and calling are different things. Both can have the same notation types, but are not the same thing.
MORE ISSUES
The good part is we now have a logical way to represent objects in Luau, however the creation of said objects is still highly inefficient. If we wanted X amount of objects we would have to manually create X amount of tables ourselves. So why don’t we just do this automatically?
If we wanted to do it automatically, the best approach would be to have a function that returns a new table object each time it is called. This function would be considered a constructor.
local function ConstructCar()
-- to keep things short the ... represents
-- fields and behavior for each Car object
local Car = {...}
return Car
end
local car1 = ConstructCar()
local car2 = ConstructCar()
Now instead of having to manually create X amount of tables, we just have to call the constructor X amount of times, which is much more efficient. The example above did not take any parameters, but technically it could. You would use these parameters for the fields instead of just having default values.
WHAT NEXT
At this point we are pretty much done with applying the principles of object oriented programming in Luau. The paradigm on its own is solely focused on objects which we successfully represented through code. However, if you remember at the beginning, I briefly mentioned something about class-based and prototype-based styles. Lua/Luau is not class-based but is prototype-based. If you want you can stop here… or continue and learn about prototypes and how we can use them in code. In order to get into these new concepts we first need to review module scripts and eventually a concept known as metatables.
MODULES BASICS
While I do expect you to know what ModuleScripts are, I will briefly go over them again to ensure we are on the same page.
ModuleScripts are script objects that return data when retrieved using the global require
function. They must return a single value of pretty much any valid data type, but the most useful for this tutorial would be, yet again, tables. A newly created ModuleScript will look exactly like this:
local module = {}
return module
In a Script, LocalScript, or even another ModuleScript we can access the return value by requiring the module like so:
local data = require(path_to_module)
MODULESCRIPT OBJECTS
We already discussed how using a constructor can make the process of creating objects much more efficient, but believe it or not ModuleScripts provide even more benefits. For example, let’s say we wanted to create new Car objects, but from multiple scripts. With our current setup, we would have to redefine the constructor function in each script we want to create Car objects inside of. Instead of doing that, we can simply use a ModuleScript and require the module from every script that we want to create Car objects.
First, create a new ModuleScript and name it Car; also set the variable name of the table to Car (you don’t have to, but for readability this is good).
-- inside ModuleScript
local Car = {}
return Car
Now you might be thinking we can just shove all the fields and behavior into this table, but this would not be the best approach considering every time you require a module that returns a table, it will return the same table in memory. Instead we can define the constructor function inside of the Car table.
-- inside ModuleScript
local Car = {}
function Car.new(brand, color)
local newCar = {}
-- fields
newCar.Brand = brand or "EpicBrand"
newCar.Color = color or "Blue"
newCar.Speed = 0
newCar.Driving = false
-- behavior
function newCar:Drive()
self.Driving = true
end
function newCar:SetSpeed(value)
self.Speed = value
end
function newCar:Stop()
self:SetSpeed(0)
self.Driving = false
end
function newCar:GetColor()
return self.Color
end
return newCar
end
return Car
Typically in object oriented coding, “new” is used for the constructor name and in other languages it is a keyword (but not in Lua/Luau).
We can now require the Car module from any script and create new car objects using the constructor function:
-- any script
local Car = require(path_to_module)
local car1 = Car.new("AwesomeBrand", "Red")
local car2 = Car.new("RunningOutOfNamesBrand")
BAD PRACTICES
Upon construction of a new object, with our current setup, each new table contains newly created functions, but these functions do exactly the same thing across all objects of the same type. This practice violates the idea of DRY (Don’t Repeat Yourself) because we are using up more memory with duplicate logic. The solution to this issue can be achieved by understanding the idea of what a class is, and then applying the principles of prototype-based object oriented programming (prototypes) by using metatables.
CLASSES VS. PROTOTYPES
While Lua/Luau does not actually have what are known as classes, understanding them makes it easier for understanding what prototypes are. By definition, classes are essentially the blueprints for objects where you provide initial values for fields as well as defining behavior. For example, a “Shirt” class could serve as a blueprint or template for objects that are of the Shirt type. Objects that are created from a class can also be called instances or class instances.
As stated in the Introduction, Lua/Luau is prototype-based, which is not the same thing as class-based. Prototype-based languages do not have the idea of classes or instances, instead they just have objects. A prototype is simply just an object that serves as the blueprint or template for new objects.
In the previous sections, our ModuleScript Car table could be considered a prototype, but implementing this prototype-based system requires knowledge about metatables, which will be described in the next section.
METATABLES
I’ve noticed that people tend to associate metatables with being some complex thing, but in reality the intuition behind them is actually quite simple. Metatables are regular lua tables, yep that’s it. Now that the exciting definitions are out of the way how do we create them and what can they actually be used for?
As stated, metatables are regular tables, but what gives them this fancy name? To break it down, the word meta can hold multiple meanings, but for the purposes of this tutorial think of it as something that can represent forms of itself. Doesn’t make sense? One way I like to think about it is metatables are tables that can be used to describe other tables. Metatables unlock powerful functionality that can be applied to tables through what are known as metamethods. In order for a table to be considered a metatable, we have to attach it to another table; we can use the global setmetatable
function do so.
<table> setmetatable (tbl1, tbl2)
@param tbl1: the table to attach the metatable to @param tbl2: the table to make the metatable @return: the first table with the attached metatable
For Example
local tblA = {}
local tblB = {}
setmetatable(tblA, tblB)
-- OR
local tblB = {}
local tblA = setmetatable({}, tblB)
This example shows two ways in which we can attach tblB
as a metatable for tblA
.
METAMETHODS
Metamethods are powerful tools in which we can expand the functionality of tables by implementing them inside of metatables. We can do this by defining new keys with specific names and assigning them to, usually, functions (there is one exception to this). For simplicity, you can think of them as events that get triggered when a certain operation is done on a table. In total, there are 18 useable metamethods in Luau, but for this tutorial we will only be using one of them known as the __index
metamethod.
Let’s try to understand this by analyzing each sentence in the description. The first sentence states that it “fires when table[index] is inedexed, if table[index] is nil.” This statement is assuming the __index metamethod is set to a function. For example:
local myTable = {
A = "Hello",
B = "World"
}
-- assign myTable a metatable with an __index metamethod
setmetatable(myTable, {
__index = function(self, key)
print(self, key)
return "none"
end
})
-- attempt to index a nil value in myTable
local x = myTable.Z --> __index metamethod fires: table_address Z
print(x) --> none
While this does seem useful, there are performance issues with setting the __index
metamethod to a function which are described below.
Fast Method Calls
Luau specializes method calls to improve their performance through a combination of compiler, VM and binding optimizations. Compiler emits a specialized instruction sequence when methods are called through
obj:Method
syntax (while this isn’t idiomatic anyway, you should avoidobj.Method(obj)
). When the object in question is a Lua table, VM performs some voodoo magic based on inline caching to try to quickly discover the implementation of this method through the metatable.For this to be effective, it’s crucial that
__index
in a metatable points to a table directly. For performance reasons it’s strongly recommended to avoid__index
functions as well as deep__index
chains; an ideal object in Luau is a table with a metatable that points to itself through__index
.
If you are a little confused, that’s okay because we don’t need the __index
metamethod to be set to a function; I simply just showed how it worked for introduction purposes. The __index
metamethod is the only exception to what metamethods can be set to. Normally, they should be set to a function, but the __index
metamethod can be set to either a function or a table; the latter is what we will be doing.
With that out of the way, now we can look at the second sentence in the description, which states it “can also be set to a table, in which case that table will be indexed.” If you don’t understand this let my try to explain. Let’s say we have two tables: table1 and table2. If we try to index a nil value inside of table1, it will attempt to index it in table2 first before returning nil, IF the __index
metamethod is set to table2.
local table1 = {
A = "Epic",
B = "Hello"
}
local table2 = {
C = "World"
}
setmetatable(table1, {__index = table2})
print(table1.C) --> __index metamethod fires: World
NEXT STEPS
Whether the sections above described new ideas or stuff you already knew, it might be easy to lose track of what our end goal is. We are trying to implement object oriented programming through a prototype-style using metatables. Now that you hopefully understand what metatables and metamethods are, we can finally start working on the prototype-based system!
IMPLEMENTATION
We are going to be using ModuleScripts and metatables to create a prototype for all of our car objects. First, let’s create the basic structure again.
-- inside ModuleScript
local Car = {}
function Car.new()
end
end
Before, I described this main Car table as just being a container for the constructor function, but we can do so much more with it now that we unlocked knowledge of metatables. We can now define this Car table as being a prototype object for the new car objects we create. In the next code example I am going to show the code and then explain it after.
-- inside ModuleScript
local Car = {}
Car.__index = Car
function Car.new()
local newCar = {}
return setmetatable(newCar, Car)
end
return Car
First, let’s focus on the code inside of the constructor. We are creating a new table for each new car object as shown in older examples. But this time we are assigning the metatable of the newCar
table to the Car
table.
I also mainly see people getting confused with why we are defining the __index
metamethod inside of the Car table and assigning it to the Car table itself. Well, since the Car table is the metatable of the newCar table there is no issue with putting the __index
metamethod inside of it. What this is essentially stating is if we attempt to index a nil value inside of the newCar table, it will attempt to index the Car table first, before returning nil.
This way of setting up the code solves the issue of having duplicate logic by each new car object having it’s own functions in memory. Now, we can simply just define the functions in the prototype Car table and let the metatables work their magic.
FINAL RESULT
The code example below will show the final result using all of the concepts we learned throughout this tutorial.
-- inside ModuleScript
local Car = {}
Car.__index = Car
function Car.new(brand, color)
local newCar = {
Brand = brand or "EpicBrand",
Color = color or "Blue",
Speed = 0,
Driving = false
}
return setmetatable(newCar, Car)
end
function Car:Drive()
self.Driving = true
end
function Car:SetSpeed(value)
self.Speed = value
end
function Car:Stop()
self:SetSpeed(0)
self.Driving = false
end
function Car:GetColor()
return self.Color
end
return Car
-- inside arbitrary script
local Car = require(path)
local carA = Car.new("RoCar", "Orange")
print(carA:GetColor()) --> Orange
QA SECTION
What if the “class” or prototype table tries to call its own methods?
To avoid this from happening you can setup your code like so:
-- inside ModuleScript
local Object = {}
Object.__index = Object
function Object:Method()
return self.Name
end
return {
new = function(name)
local newObject = {
Name = name or Object.ClassName
}
return setmetatable({}, Object)
end
}
This also prevents newly created objects from being able to call the constructor method since the constructor method now has a different container than the prototype table.
Pillars of Object Oriented Programming
ABSTRACTION
This idea is not specifically bound to object oriented programming and is perhaps the most simplest to understand. It essentially just means that the complexity and processes of a system are hidden under layers, the top layer being most user friendly. For example, in a shopping application, when the customer clicks a check out or buy button, they don’t need to know everything that is happening within the code, they only need to know that they successfully bought the product.
In object oriented programming, methods provide layers of abstraction because all the coder has to do is call the method instead of worrying about the internals of how it works (unless they made it )
ENCAPSULATION
This idea in object oriented programming defines how object data can be accessed. In languages, such as Java, this is implemented through what are known as access modifiers, but Lua/Luau does not have these. In general, the two main access modifiers are private
and public
. Public data can be accessed by other scripts, while private data cannot. It’s easy to implement the idea of public accessible data in Lua/Luau objects, but there are two ways we can go about implementing private accessible data.
The first way is to have private fields for objects, that is fields that shouldn’t be used outside of internal usage. This can also be extended to having private methods. These keys should be prefixed with an _ (underscore) per the standard style.
-- inside ModuleScript
local Object = {}
Object.__index = Object
function Object.new(name)
local newObject = {
Name = name or "Object",
_secret = 27
}
return setmetatable(newObject, Object)
end
return Object
Although we can clearly access and modify this _secret
key in other scripts, the underscore is an idicator that it shouldn’t be.
The second way is to have data that isn’t necessarily attached to an object, but rather can be shared by all objects. For example:
-- inside ModuleScript
-- Private
local secretNumber = 100
-- Object
local Object = {}
Object.__index = Object
function Object.new(name)
local newObject = {
Name = name or "Object",
_secret = 27
}
return setmetatable(newObject, Object)
end
function Object:GetSecret()
return self._secret * secretNumber
end
return Object
INHERITANCE
This idea is perhaps one of the most popular, but also controversial ideas of object oriented programming. Inheritance is the idea of new objects inheriting fields and behavior from other classes, or prototypes in our case. Remember way back at the beginning of this tutorial when I was talking about the difference between concrete and abstract objects? Those ideas can be applied to this one because we can think of abstract objects as being the parent prototypes of concrete prototypes.
-- inside ModuleScript
local Fruit = {}
Fruit.__index = Fruit
function Fruit.new(fruit)
return setmetatable({
Type = fruit,
Fresh = true
}, Fruit)
end
function Fruit:Rot()
self.Fresh = false
end
return Fruit
-- inside another ModuleScript
local Fruit = require(path)
local Apple = setmetatable({}, Fruit)
Apple.__index = Apple
function Apple.new()
local newApple = Fruit.new("Apple")
return setmetatable(newApple, Apple)
end
return Apple
The example shown above is only inheritance that is one layer deep, and although you could have many more layers, it is strongly encouraged that you do not do this. There are a couple reasons for this; the first one is that the behavior of all sub-prototypes depend on the parent-prototypes that come before it. If one of these parent-prototypes gets messed up, it could potentially mess up all of its sub-prototypes which would not be good. So not only does it increase complexity, but even Roblox themselves discourage deep inhertance, or deep __index
metamethod chains which you can read about here.
Alternative: Composition - Wikipedia
POLYMORPHISM
Polymorphism is another concept that is not specifically bound to object oriented programming. The word itself means “many forms.” An example of polymorphism, as it relates to this tutorial, would be subtype polymorphism, or simply just inheritance. While there are various other examples, I myself am still learning more about this concept and would feel better giving a resource to learn more about it than trying to explain it myself.
EXTRA NOTES
The code used throughout the tutorial was purely for example purposes and I realize you may not understand how to actually implement object oriented programming in real projects. For now, I have only published this post which aims to explain the concepts, but I will be working very hard to hopefully release another tutorial on use cases for object oriented programming using real examples.
The current major version of this tutorial is V3, meaning I have rewritten this three times now. This tutorial is not the final version, but the structure of V3 provides a better structure for making edits in the future.
RESOURCES
Roblox
https://luau-lang.org/
https://developer.roblox.com/en-us/articles/Metatables
https://devforum.roblox.com/t/all-about-object-oriented-programming/8585
https://roblox.github.io/lua-style-guide/#metatables
Lua
https://www.lua.org/pil/16.html
External
https://en.wikipedia.org/wiki/Object-oriented_programming
https://en.wikipedia.org/wiki/Object-oriented_programming#Objects_and_classes
https://en.wikipedia.org/wiki/Prototype-based_programming
CONCLUSION
If you made it to the end, thank you for reading my rather lengthy tutorial covering object oriented programming in Roblox. If you see any issues, have an questions, or just anything you want to add to this post feel free to leave a comment!
Last Updated: 13 September 2021