Concepts: Object Oriented Programming in Roblox

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
  1. General Lua/Luau Knowledge
  2. Basic ModuleScripts
  3. 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.

:warning: Do not forget to seperate items in the table by commas or semicolons because we are inside of the table constructor :warning:

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.

:warning: Remember that function definition and calling are different things. Both can have the same notation types, but are not the same thing. :warning:

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.

image

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 avoid obj.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 :slight_smile: )

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.

Read More - Wikipedia

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 Septemeber 2021

304 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!

12 Likes

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

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

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

6 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!

5 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!

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

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

1 Like

I was looking for a tutorial for object-oriented programming.
It’s an awesome tutorial.
May I translate this post and post it on the korean site?
if you ok, I’ll highlight the original link and post it!

2 Likes

I remember reading an article similar to this before, but this seems more in-depth. Nice job.

1 Like

I don’t see an issue with that, glad you like the tutorial :smiley:

Thanks for allowing :slight_smile:
Good luck!

Very helpful :slightly_smiling_face:

1 Like

As a tip, if you do not want to have an if statement in every single method you create under your “class” as show here:

You can set up your code like this:
(code shown below is for example purposes)

--ModuleScript
local Object = {}
Object.ClassName = "Object"
Object.__index = Object

function Object:Method()
    return self.Name
end

return {
    new = function(name)
        local newObject = setmetatable({}, Object)
        newObject.Name = name or Object.ClassName
        return newObject
    end
}
--Script/LocalScript
local Object = require(path_to_module)

local newObject1 = Object.new("Jon")
local newObject2 = Object.new()

print(newObject1:Method()) --> "Jon"
print(newObject2:Method()) --> "Object"

print(Object:Method()) --> Method would not be a valid member of the table returned

This also prevents “newObjects” from being able to call the “new” method or the constructor.

EDIT: Replaced the old section with this new, better way of setting up the code.

5 Likes

This is a really good tutorial, thank you so much! Here is an example I made following along in this tutorial:

Example

ModuleScript (in ReplicatedStorage)

local Character = {}
Character.__index = Character

function Character:SayHello()
	print("Hello, my name is " .. self.name .. "!")
end

function Character:ShareHobby()
	print("My hobby is " .. self.hobby .. ".")
end

function Character:Age(int)
	self.age += int
	
	print(self.name .. " aged " .. int .. " year(s), and is now " .. self.age .. " year(s) old.")
end

return {
	new = function(name, age, hobby)
		local object = setmetatable({}, Character)
		
		object.name = name or "Character"
		object.age = age or 0
		object.hobby = hobby or "Programming"
		
		return object
	end
}

ServerScript (in ServerScriptService)

local Character = require(game.ReplicatedStorage.Character)

local char1 = Character.new()
local char2 = Character.new("John Doe", 34, "Gaming")

char1:SayHello() --> Hello, my name is Character!
char2:SayHello() --> Hello, my name is John Doe!

char1:ShareHobby() --> My hobby is Programming.
char2:ShareHobby() --> My hobby is Gaming.

char1:Age(1) --> Character aged 1 year(s), and is now 1 year(s) old.
char2:Age(2) --> John Doe aged 2 year(s), and is now 36 year(s) old.
4 Likes

Amazing. Just amazing.

I’ve been trying to learn OOP for a while, then I came to the devforum and found this through a search. You’re the best explainer I’ve ever seen.

When you’re explaining an issue-solving concept, your approach is genius. Instead of straight-up explaining what the concept is, you start with problematic code (for demonstration purposes), and then explain the problem, and then explain how the concept solves this problem.

This kind of explaining makes it stick into your head, by providing an actual example, and showing you what the code would be with and without the concept.

I believe you deserve top contributor for this, it’s honestly a brilliant contribution.

3 Likes

Semicolons and commas both have the function of separating arguments/parameters in tables.
(See here for their different uses)
I even tested this myself, using either one you can use Car:Drive() and Car.Drive()

I believe you are confused. I have stated you can use either semicolons or commas. What you are quoting is unrelated.

Thanks for this tutorial man. I made a pet follow system using OOP and being able to determine owner of a pet by doing self.Owner which I find quite useful! I am going to use OOP for now on :D.

2 Likes