Object Oriented Programming in Roblox

Introduction


The goal of this tutorial is to provide the reader with knowledge on how to use Object-Oriented programming principles with your developing workflow and in your Roblox games/creations. By reading this tutorial you should already have a solid grasp on the things listed below.

What You Should Already Know:

- Tables
- ModuleScripts
- Lua Syntax

Terminology


Expand / Collapse

Table

A data structure that contains a collection of data types known as elements that can be retrieved or indexed using keys. Tables are created using table constructors: {}.

Function

A set of instructions that execute a task, not associated with an object.

local function SayHello()
  print("Hello!")
end

Method

A set of instructions that execute a task, associated with an object:

Part:Clone()

Object-Oriented Programming

A programming paradigm based on objects or data, rather than functions and logic. Like in the real world, these objects have their own properties and behaviors that represent what they are. Objects can have properties (variables), states (variables), and behaviors (methods).
More on Wikipedia

Prototype-Based Programming

A style of object-oriented programming where classes are not explicitly defined, but rather acquired by adding properties and methods to the instance of another class or an empty object.
Lua is Prototype-Based
More on Wikipedia

Let’s Get Started


A lot of people mistake Lua for directly being an Object-Oriented Programming language, while others argue Lua isn’t even Object-Oriented at all. Both of these statements are incorrect; Lua is what is known as a Prototype-Based Programming language. This is a type of Object-Oriented programming, but with a few differences. These differences will be explained later when I go into more detail about Prototype-Based Programming in Lua. For now, I will be discussing Object-Oriented principles and how we can begin using them in code.

Object-Oriented Programming


Object-Oriented quite literally means based around objects. In the real world, wherever you may live, objects exist everywhere around us. From the food you ate earlier, to the clothes you are wearing right now, and even the planet you are living on. All of these are considered real-world objects.

With all these objects that exist, we have ways of classifying them into categories that describe why they are what they are. For example, to keep things simple, a dog could be considered a dog because it has four legs, fur, a tail, and can bark as well as do tricks. The attributes about the dog are considered properties and the actions it can do are considered behavior. Based on these two, objects also have states. For example, if the dog barks, then the dog is barking. So, the state of the dog barking would be true when it barks and false when it isn’t.

You can probably already see how we can translate this into code, but if you can’t let me explain. Every single object has three things about it that describe what the object is and how it behaves: properties, states, and behavior. In code, properties and states can be variables, while behavior can be methods.

In the next section, I will be showing you a basic approach to translate real-world objects into code.

Objects in Code


We already know that all object’s have three things about them: properties, states, and behavior. To avoid typing any more long paragraphs explaining concepts, let’s see these concepts in action through actual code examples. Throughout the tutorial, we will be working with a Car Object.

Car Object

Properties: Model, Color, Speed
States: isDriving
Behavior: Drive, Stop, SetSpeed

Let’s get straight into the code!

-- First we make our properties
local Model = "EpicModel"
local Color = "Blue"
local Speed = 0

-- Now let's create the states of the car
-- States are almost always booleans
local isDriving = false

-- Next up, behavior, or what the car can do
local function Drive()
  -- If the car is driving, change the states
  isDriving = true
end

local function Stop()
  -- Set the speed to 0 and change the states
  Speed = 0
  isDriving = false
end

local function SetSpeed(speed)
  -- First, check if the car is driving with the state
  if isDriving then
    Speed = speed
  end
end

After we have created these variables and functions we can use them:

print(Model) --> EpicModel
print(Speed) --> 0
Drive()
print(isDriving) --> true
SetSpeed(20)
print(Speed) --> 20
Stop()
print(isDriving) --> false
print(Speed) --> 0

As you can see above we successfully translated a real-world object into code… or did we?
Technically you could say we have, but in reality, this is not the best approach to take when translating real-world objects into code.

The above code is not really attached to anything that defines it to actually be a Car Object. Also, let’s say we wanted to have multiple Car Objects, maybe even hundreds, we would have to define new variables and methods for each Car Object you would want to have. You can see how the above code is very inefficient in the examples I provided.

So how can we solve this? To answer the first issue, we will be using tables to actually associate the properties, states, and behavior to a Car Object.

Car Object With Tables


Before we address the issue of if we wanted to have multiple Car Objects, let’s first explore how we can associate the properties, states, and behavior to an actual object.

First off, we are going to want to create a table and label it appropriately.

local Car = {}

I will be showing you two approaches to how we can add the properties, states, and behavior to the table; the first approach is creating the keys outside the table constructor and the second approach is creating the keys inside of the table constructor.

First Approach (outside table constructor)

local Car = {}

-- Properties
Car.Model = "EpicModel"
Car.Color = "Blue"
Car.Speed = 0

-- States
Car.isDriving = false

-- Behavior
Car.Drive = function()
  Car.isDriving = true
end

Car.Stop = function()
  Car.Speed = 0
  Car.isDriving = false
end

Car.SetSpeed = function(speed)
  if Car.isDriving then
    Car.Speed = speed
  end
end
Extra Tip

table.method = function(...)
  -- cool stuff
end

is the same as doing

function table.method(...)
  -- even cooler stuff
end

Alright cool! Now we can actually associate our properties, states, and behavior to a Car Object.

print(Car.Model) --> EpicModel
print(Car.Color) --> Blue
Car.Drive()
print(Car.isDriving) --> true
--etc

Second Approach (inside table constructor)

Now you may be wondering why I am showing you an alternate way to do what we just did above. There is a very good reason for it, so it is vital you read everything in this section.

DO NOT FORGET to add semicolons or commas after your function ends since we are inside of the table constructor!!!

local Car = {
  -- Properties
  Model = "EpicModel";
  Color = "Blue";
  Speed = 0;
  
  -- States
  isDriving = false;

  -- Behavior
  Drive = function()
    -- wait a second, how do we get the "isDriving" state here????
    -- read below....
  end;
}

Okay, hold on a second, how are we supposed to change the isDriving state from inside the Drive function. We know we can’t just do isDriving = true or Car.isDriving = true because considering we are still in the table constructor, we can’t actually retrieve that value or index that key yet.

First, let’s analyze how we actually call the function, outside the table constructors. We would do this:

Car.Drive()

Now, what if I told you, there is actually another way you can call this function (or really at this point it can be considered a method). You may have noticed with other built-in Roblox methods you actually use a : (colon) to call it, for example :FindFirstChild(…). We can call the Drive method with colon syntax:

Car:Drive()

Alright, that’s great but what does this actually do? When you call a function or method that is inside a table with colon syntax, the table that calls it, gets automatically passed into the parameters. But now inside of our method we have to actually create a parameter that defines the table that called it:

Drive = function(table)
  -- now we can reference things inside of the table
  table.isDriving = true
end

This will not work if you do not call your method with a colon!!!

Now that we have acquired this knowledge, or maybe it’s just review for you, let’s continue creating our Car Object using the second approach.

local Car = {
  -- Properties
  Model = "EpicModel";
  Color = "Blue";
  Speed = 0;

  -- States
  isDriving = false;
  
  -- Behavior
  Drive = function(table)
    table.isDriving = true
  end;

  Stop = function(table)
    table.Speed = 0
    table.isDriving = false
  end;

  SetSpeed = function(table,speed)
    if table.isDriving then
      table.Speed = speed
    end
  end;
}

Now we can again associate our properties, states, and behavior to a Car Object.

-- Do not forget to call your methods with colons
print(Car.Color) --> Blue
Car:Drive()
print(Car.isDriving) --> true
Car:SetSpeed(45)
print(Car.Speed) --> 45
Car:Stop()
--etc

Important Information About the First Approach

We know that the table that calls a method will get automatically passed into the parameters if we use colon syntax when calling it, but there is another very interesting and helpful way worth mentioning. If we create a function using a colon, something interesting will happen. For example:

function Car:Drive()
  -- alright now what?
end

If we create a new function/method using a colon, we can actually no longer call the method as Car.Drive() we can only do Car:Drive(), but what is the purpose of doing this? When you call the method using the only possible way, with a colon, a keyword called self gets automatically assigned to the table that called the method in the scope of that method. If that doesn’t make sense hopefully the code below will give you a visualization of how it works:

function Car:Drive()
  -- self is the table that calls this method
  self.isDriving = true
end

We no longer have to create a parameter for the table that called the method because of self.

Now self isn’t technically a keyword, because, outside a situation of it being the table that calls a table method, it wouldn’t be equal to anything. Outside of a situation like this, it can be treated as nothing.

Clearly this way can only be done using the first approach because we cannot create a new method inside of the table constructor using colon syntax.

self will come up a lot in future sections!

We Still Have Issues Though


If you don’t remember before, I said:
"Also, let's say we wanted to have multiple Car Objects, maybe even hundreds, we would have to define new variables and methods for each Car Object you would want to have."

We made it a little bit easier because if we wanted let’s say one hundred Car Objects, we would only have to create one hundred new tables, instead of manually creating hundreds of the properties, states, and behaviors. But as you may be thinking this still seems like such a pain to do manually, so why don’t we do it automatically?

If we wanted to do it automatically the best approach we could use is having a function that creates new tables each time you call it. For Example:

local function CreateCar()
  -- to keep things short the ... represents everything we 
  -- put in the Car table previously
  local Car = {...}
  return Car
end

Now instead of having to create new tables ourselves for our hypothetical one hundred Car Objects, all we have to is call the CreateCar function one hundred times. This is great and way more efficient than what we were doing before.

local newCar1 = CreateCar()
print(newCar1.Color) --> Blue

local newCar2 = CreateCar()
print(newCar2.Model) --> EpicModel

In Object-Oriented Programming, this CreateCar function isn’t exactly but can be considered a constructor. According to Wikipedia, “a constructor is a special type of subroutine called to create an object. It prepares the new object for use, often accepting arguments that the constructor uses to set required member variables.”
Constructor on Wikipedia

For the sake of example, the constructor above doesn’t take any parameters, but we will get into that later.

In the next sections, we will begin talking about ModuleScripts and how we can use them in this process of creating new objects.

ModuleScript Review


Alright, alright let’s take a step back and rest our brains for a moment. If you aren’t familiar with ModuleScripts I will briefly explain them, but I am assuming you already know what they are. Even if you do know what they are and how to use them it would probably be best to either read this short excerpt or take a break to avoid gaining too many new concepts at once.

image

ModuleScripts are script objects that return values when retrieved using the global require function. They can return anything, but the most useful, in relation to our tutorial, are tables.

A newly created ModuleScript will look like this inside:

local module = {}

return module

In a Script, LocalScript, or even another ModuleScript we can get the return value by requiring the module:

local myModule = require(path_to_module)
print(myModule) --> table_address

ModuleScript Objects


We already discovered how we can make the process of creating objects easier using a constructor, but believe it or not, ModuleScripts can actually make this even easier. For example, let’s say we wanted to create more Car Objects, but from different scripts. We would have to redefine the constructor in every single script we wanted to create Car Objects in.

Instead of doing that, we can simply use a ModuleScript and just require the module from every script we want to create Car Objects in. This highlights an important practice called DRY: Don’t Repeat Yourself. By using ModuleScripts, we won’t have to create new functions each time we want to create new objects, but instead, we would be referencing the same thing returned from the ModuleScript each time.

First, create a new ModuleScript and name it “Car”, also name the table inside of it “Car”.

local Car = {}

return Car

Now you may be thinking, well why can’t we just put all the properties, states, and behaviors in the table we just created so that each time we require the module, we get the Car Object?

Well, when we return a table from a ModuleScript, it is actually returning the same table in memory each time. So this wouldn’t work because we want different objects aka different tables. So it looks like we have to create a function again, but this time, let’s make the function a key in the “Car” table.

local Car = {}

function Car.new()

end

return Car

Typically, in Object-Oriented coding, new is used when creating new objects. In Lua, new is not a keyword, but we can still use it to be consistent with the Object-Oriented style.

Now let’s create a new table inside of the constructor and add our properties, states, and behavior to that new table.

local Car = {}

function Car.new()
  local newCar = {}
  
  -- Properties
  newCar.Model = "EpicModel"
  newCar.Color = "Blue"
  newCar.Speed = 0

  -- States
  newCar.isDriving = false

  -- Behavior
  function newCar:Drive()
    self.isDriving = true
  end

  function newCar:Stop()
    self.Speed = 0
    self.isDriving = false
  end

  function newCar:SetSpeed(speed)
    if self.isDriving then
      self.Speed = speed
    end
  end

  return newCar
end

return Car

Now we can require the “Car” module from other scripts, and create new Car Objects using the “new” constructor.

local Car = require(path_to_module)

local newCar = Car.new()
print(newCar.Color) --> Blue

We can also add parameters to the constructor to allow for the creation of different Car Objects upon creation.

function Car.new(model,color)
  local newCar = {}
  
  -- Properties
  newCar.Model = model or "EpicModel"
  newCar.Color = color or "Blue"

  -- to keep things short I won't include the other
  -- things in this code section
  ...

  end
end
local Car = require(path_to_module)

local BlueCar = Car.new("CoolModel","Blue")
print(BlueCar.Color) --> Blue
local RedCar = Car.new("AwesomeModel","Red")
print(RedCar.Color) --> Red

Bad Practices Worth Mentioning

It may seem like what we are doing now, is highly efficient, considering how awesome Object-Oriented programming can be, but we are actually performing a couple of bad practices.

DRY Don`t Repeat Yourself

In the code examples above, we are creating new functions/methods for each new Car Object we create. This is inefficient considering there is a way that we can actually solve this issue. The solution to this issue is by simulating classes in Lua.

In the next section, I will discuss the difference between a Class and an Object.

Classes vs. Objects


An important concept in Object-Oriented programming is classes. Classes are basically the blueprints or mold for an object. For example, “Shirt” could be a class and you can use that class’ constructor function to create a new object that inherits all of the properties, states, and behavior of the class. As said before, Lua isn’t directly an Object-Oriented Programming language, but rather a type of it. Lua does not have classes, because it is a Prototype-Based Programming language. However, what it does have is prototypes which be created using these special things called metatables.

As stated on Wikipedia, “Prototype-based programming is a style of object-oriented programming in which behavior reuse is performed via a process of reusing existing objects that serve as prototypes.”

At the same time as when I describe metatables and how they can be used to create a prototype/class system, I will also show you how they can clean up the bad practices we mentioned above.

Metatables & Metamethods


It seems as if the word itself: metatable tends to frighten people away from learning them, but in reality, they are really quite simple. Metatables are regular Lua tables that can hold metamethods and be attached to other normal tables. But what are metamethods? Metamethods are literally just events that get fired when a certain action is done upon the table. That’s it.

First, let me explain how to attach a metatable to a normal table, and then I will discuss how we can use metamethods in order to accomplish a “class/prototype system”.

In order to attach a metatable to a normal table, we use a global function called setmetatable.

This function takes the normal table as the first parameter and the metatable as the second parameter. It also returns the normal table with the attached metatable.

local myTable = {}
setmetatable(myTable,{})

-- OR

local myTable = setmetatable({},{})

All Metamethods in Roblox

There are some metamethods, that exist in vanilla Lua, but not in Roblox’s version of the language if you are wondering about that.

Link to a separate thread explaining all the metamethods in detail will be listed here when made.

The main metamethod we will be focusing on for this tutorial is __index. Let’s analyze how this metamethod works.

“Fires when table[index] is indexed, if table[index] is nil.” (forget the second sentence for now).

So this tells us that the __index metamethod is fired when you attempt to index a value that is nil inside of your table. For example, assuming there is a metatable attached with an __index metamethod:

local myTable = {
  A = "Hi",
  B = "Cool"
}

local attempt = myTable.C --> __index metamethod fires
print(attempt) --> nil

Now let’s see an example where we actually attach the metatable with the __index metamethod connected to a function:

local myTable = {
  A = "Hi",
  B = "Cool"
}
setmetatable(myTable{
  __index = function(table,key)
    print(table,key)
  end
})

local attempt = myTable.C --> __index metamethod fires: table_address "C"
print(attempt) --> nil

We set the __index metamethod to a function that takes the table the metatable is attached to as well as the key that was trying to be indexed. For example purposes, I just decided to print out the table and the key inside the function body.
IMPORTANT: Setting the __index metamethod to a function will have a negative impact on performance.

Now that we understand this way, let’s look at the second sentence of the definition for when the __index metamethod fires: “Can also be set to a table, in which case that table will be indexed”.

If you don’t understand this let me explain. If we attempt to index a key that is nil in Table1, and we have the __index metamethod set to another Table, let’s say Table2, then it will attempt to find the key in Table2 before returning nil. Let’s see this in action:

local table1 = {
  Key1 = "Awesome",
  Key2 = "Cool"
}

local table2 = {
  What = "Woah"
}

setmetatable(table1,{__index = table2})

print(table1.What) --> __index metamethod fires: Woah

How Does This Relate?

Considering Lua is a Prototype-Based programming language we can say that table2 is a prototype for table1. This will be very important when we create custom “classes” in the next section.

Creating a Car Class


Welcome back to ModuleScripts. We are going to be using ModuleScripts as well as metatables in order to create a Class for our Car object that we keep mentioning.

First let’s create out basic ModuleScript structure, in terms of “Car” but let’s refer to “Car” as a class now (even though it’s not technically a class).

local Car = {}

-- constructor
function Car.new()

end

return Car

Ok so we set up our constructor, now let’s use the concept of prototypes via metatables to accomplish making the Car table a prototype for our newly created Car objects:

local Car = {}
Car.__index = Car

-- constructor
function Car.new()
  local newCar = setmetatable({},Car)

  return newCar
end

return Car

Alright, woah, woah wait a second. Let me explain to you what I just did if none of the code above makes any sense. So I created a new table, our class, called “Car”. Inside of Car, I set a new key “__index” equal to the table itself. Interesting how __index happens to be a metamethod, remember that. Next, inside my constructor, I create a new table with an attached metatable that is the “Car” table aka the class. As mentioned before, the __index metamethod will attempt to find a key in a second table if the key is nil in the first table.

“Car” is literally just a table, which contains a metamethod set equal to the table itself. So whenever we try to index something nil in the “newCar” table it will attempt to find it in the “Car” table before returning nil.

Let’s see this in action. We are also about to solve the bad practice of repeating ourselves in the code below:

local Car = {}
Car.__index = Car

-- constructor
function Car.new()
  local newCar = setmetatable({},Car)
  
  -- Properties
  newCar.Model = model or "EpicModel"
  newCar.Color = color or "Blue"
  newCar.Speed = 0
  
  -- States
  newCar.isDriving = false

  return newCar
end

-- Behavior
function Car:Drive()
  self.isDriving = true
end

function Car:Stop()
  self.Speed = 0
  self.isDriving = false
end

function Car:SetSpeed(speed)
  if self.isDriving then
    self.Speed = speed
  end
end

return Car

Now we can still use the same methods on our “newCar” object thanks to metatables. How did we solve the bad practice of repeating ourselves though? Well, instead of creating new functions for each new object we create, we are instead referencing already created methods apart of the “Car” class.

To visualize, how the __index metamethod works we can do this:

-- THIS IS BAD, for example purposes
local newCar = setmetatable({},{__index = Car})

HOWEVER, DO NOT DO THE SMALL CODE ABOVE The reason being is, we create a new metatable table each time we create a new object. We can instead just leave it like the code below so that we are referencing only one table each time we create a new object.

-- THIS IS GOOD
local newCar = setmetatable({},Car)

What if the Class Tries to Call Itself

Another thing worth mentioning, or pointing out, is with the current code, the “class” itself would be able to call its own methods.

Car:Drive()

There is a very simple solution to fix this. Since we are declaring our methods with colons, we know self gets automatically assigned in the scope of the method as the table that called it. So we can simply check if the metatable of self is equal to the Class table, “Car” in our situation.

function Car:Drive()
  if getmetatable(self) ~= Car then return error("[Car]: Attempt to call method Drive on an invalid Car object") end
  self.isDriving = true
end

You can choose what will happen in the case that the Class tries to call a method on itself or something along those lines. Typically erroring would be best so that you know which parts of your code are having issues.


We have successfully created a “Class” in Lua, using the concept of prototypes and the usage of metatables with the __index metamethod. Feel free to mess around with this code as much as you would like. In the next sections, we will be discussing how we can apply the four pillars of Object-Oriented programming into our code.

Four Pillars of Object-Oriented Programming


In Object-Oriented programming, there are four principles that we use when going about making classes for objects. Those include:

Abstraction | Encapsulation | Inheritance | Polymorphism

Abstraction in Code


Abstraction

A process of only showing the necessary features of an object to an outside viewer while hiding how it manages to function. For example, everyone knows that a power button shuts something on or off, no need to know how it changes the state, only that it does.

This pillar of Object-Oriented programming is perhaps the easiest. It just tells us that what doesn’t need to be seen by a user is hidden under layers. 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.

Encapsulation in Code


Encapsulation

A system that is used to hide data that doesn’t need to be seen from outside itself. For example, properties and methods apart of a class that doesn’t need to be shown will be declared as private, while methods to access those private things would be public. This is done with getter and setter methods.

This pillar of Object-Oriented programming helps us organize which things should be public and which things should be private in our classes. But what does this mean? Public variables and methods can be accessed and changed by other scripts, while private variables and methods cannot be accessed outside of the script they are inside of. An easy way to do this in Lua is simply creating local variables outside of the tables.

--Private
local default_name = "John Doe"

--Public
local Object = {}
Object.__index = Object

function Object.new(name)
  local newObject = setmetatable({},Object)
  
  newObject.Name = name or default_name

  return newObject
end

return Object

Inheritance in Code


Inheritance

When a new class is created with the same behavior as a pre-existing class. The new class can be referred to as a sub-class/child class of the super-class/parent class and will inherit all of the parent class’ contents. For example, if a class called Car existed, Truck could be a subclass of Car.

This pillar of Object-Oriented programming is expressed through a system of classes and subclasses. Below I will show how to create “subclasses in Lua”:

local Object = {}
Object.__index = Object

function Object.new()
  local newObject = setmetatable({},Object)
  return newObject
end

return Object
local Object = require(path_to_module)

local SubObject = setmetatable({},Object)
SubObject.__index = SubObject

function SubObject.new()
  local newObject = setmetatable({},SubObject)

  return newObject
end

return SubObject

Polymorphism in Code


Polymorphism

The provision of a single interface to entities of different types or the use of a single symbol to represent multiple different types.

This pillar of Object-Oriented programming can be used to create abstract classes in Lua. So instead of the idea of subclasses, you could have multiple classes of different types that can inherit methods from a single general class.

local BaseClass = {}

function BaseClass:DoSomething()
  print("Hello World")
end

return BaseClass
local BaseClass = require(path_to_module)

local Object = setmetatable({},BaseClass)
Object.__index = Object

function Object.new()
  local newObject = setmetatable({},Object)
  return newObject
end

return Object

Practical Usecases


When created, I will link a thread of use cases. I have hit the character limit.

Resources


Roblox Devforum
https://devforum.roblox.com/t/all-about-object-oriented-programming/8585
https://developer.roblox.com/en-us/articles/Metatables

Lua
https://www.lua.org/pil/16.html

Wikipedia
https://en.wikipedia.org/wiki/Object-oriented_programming
https://en.wikipedia.org/wiki/Prototype-based_programming

Conclusion


Thank you for reading this rather lengthy thread discussing how you can use Object-Oriented programming principles in your code. If you see any issues or things you would like to discuss or add feel free to use the comments section for that purpose.

Thank you :smile:
- JonByte

Last Major Edit: September 19, 2020

77 Likes

Fantastic guide. Enough detail for someone to understand with little programming experience, and advanced enough for competent developers to expand their skill set.

I’ve always been an advocate of OOP on Roblox, this guide is a great resource for those who are interested!

5 Likes

Best introduction of OOP in lua which also explains practical uses of it in simple terms.

4 Likes

There are a number of bad practices in this thread that I think you should address immediately as they can be detrimental to script behavior and performance.

First things first: Under the ModuleScript Objects header, you construct the Car class in the following manner:

local Car = {}
function Car.new()
	local newCar = {}
	newCar.Property = xyz
	
	function newCar:Method()
	
	end
end

Defining methods in-line like this is very wasteful as it redefines the function for every single individual car object. Real OOP setups in other languages technically uses all static methods (* in most cases, all cases that I’m aware of at least) with an invisible variable passed in (usually called this).

So in a C-based language:

class Car {
    public int Speed = 30; // This is a member, so it only exists on car objects

    public Car() {
        // ctor
    }

    public void Method() {
        // Some code here
    }
}

That Method thing is basically just

public static void Method(Car this) {

}

This can be emulated in Lua using the self variable coupled with metatables. Here is a proper object in Lua-OOP

local MyObject = {}
MyObject.__index = MyObject

function MyObject.new()
    local objectInstance = {}
    objectInstance.Property = xyz
    return setmetatable(objectInstance, MyObject)
    -- Since MyObject.__index = MyObject, anything we try to referece on objectInstance
    -- that doesn't exist will instead be searched for in the MyObject table.
    -- This behavior is 100% required for this setup to work.
end

function MyObject:Method()
    -- This method can either be static or a member method (called on an instance of this type)
    -- A good practice for member methods like this that MUST be called on something
    -- created by MyObject.new() is this:
    assert(getmetatable(self) == MyObject, "Attempt to statically call method Method. Call this on an instance of MyObject created via MyObject.new()")
    -- If someone literally writes down MyObject:Method(), it will error
    -- But if they do something like local obj = MyObject.new() obj:Method() then it will work fine.
    -- Then, to access properties of the object instance itself, use self
    print(self.Property) -- And this will print whatever xyz is up top.
    -- It may be important to mention that `self` is created only for Lua methods
    -- "Lua methods" being functions called with a colon (:) rather than a period.
end

While you do address part of this in the “Creating a Car Class” section, the fact that you avoid this behavior in the section up above can cause confusion and is misleading.

Additionally, you propose that it is good to do setmetatable(objectInstance, {__index = Proto}) but in reality this is less ideal than the method I used above, as the method you provide creates a new metatable for every instance. Metatables for objects should generally be shared across that entire object type, and is what allows that instance check assert in my example to work in the first place.

The next thing is that when demoing metamethods, you use a function for __index in a usage example of metamethods but fail to mention that doing this is incredibly detrimental to performance (as is using __newindex functions) on the level where it does have a notable impact on the behavior of the script en-masse, so this isn’t some negligible performance loss that some people complain about because they’re losing 0.0002 seconds of runtime. I believe it is vital that you explain that setting __index and __newindex to functions can cause pretty nasty problems for script performance.

Overall it’s a solid guide but you should probably address the bad practices too since it’s pretty easy to botch scripts entirely by accident simply on the count of not knowing any better.

17 Likes

You bring up good points, I appreciate the help on this thread and I will be sure to address your points as soon as I have the time.

2 Likes

This tutorial is amazing. I love it. It goes in depth and explains everything thoroughly. It helps someone who is confused with OOP and someone who is trying to learn it. 100/10 for me, amazing job!

2 Likes

UPDATE: I have made massive changes to this thread, highlighting issues brought to my attention by @EtiTheSpirit and other people who gave feedback. I have unfortunately hit the character limit of 32k, so I will have to create separate threads for descriptions of all the metamethods as well as practical use cases. Thank you for reading this!

2 Likes

Very helpful!

But as an OOP newbee, I still don’t understand how to use it in real roblox game.

There is a gap between what OOP is and how to apply it in real case.

2 Likes

There are many cases where OOP is useful . I once made a Gun System and I used OOP for its Setting and Other stuffs . Its based on how you think you can implement OOP to something.