Greeting Roblox Devs !
So I’m in love with oriented object programming, and now i want to talk about the Roblox usage. So I tried to figuring out how to make my own classes, my own types with custom methods and functions for them, and now it is time to share everything with you on how to make your own types and classes.
So I have read thoses articles :
https://devforum.roblox.com/t/all-about-object-oriented-programming/8585
https://devforum.roblox.com/t/i-need-help-with-my-custom-datatype/1707230/28?page=2
https://www.lua.org/pil/13.html
And both are pretty usefull, you can check them out, but here im going to merge all my knowledge to explain as simplier how to make good types and classes in roblox.
So for people that doesnt know what is a class, it is basically a variable with common functions and variables with other object of the same class.
It looks confusing, but basically every Instance in roblox studio are classes. So basically, you can access like playerService:GetPlayers()
with is a function, or playerService.MaxPlayers
the variables.
Types are specific to luau. It’s basically a pattern for creating classes, so basically allows you to create a pattern with multiple types inside.
Type’s variables
So let’s get into it. Lets say we want to create a type for a person, so we will first have the Name of the Person, and for example his age.
This is how to do it :
type Person = {
Name: string,
Age: number
}
So here, we say “ok so if we have like a person, we want it to have a Name that is a string, and an age which is a number”
So its pretty straight forward.
Now we can create a function in order to create a person, we will call it a Constructor, so basically, we want it to return a new created person, so to do that, we can do
local function PersonConstructor(): Person
local newPerson: Person = {}
return newPerson
end
Here when we call our function, we will get a new person ! Empty with nothing inside, but a new person !
Now for it to be functionnal, we will give arguments to the Constructor and fill our new Person. So we can just provide for example just a name and an age for us to fill our Person object.
local function PersonConstructor(name: string, age: number): Person
local newPerson: Person = {}
newPerson.Name = name
newPerson.Age = age
return newPerson
end
That seems better and usefull ! Now we can create a Person if we call this Constructor ! That is how to do it :
local paul = PersonConstructor('Paul', 18)
local tom = PersonConstructor('Ninon', 12)
print(tom.Age) --> 12
print(paul.Name) --> Paul
Now this is usefull, but we can do even by using functions for each players !
Type’s functions
So in our types, we added variables we can access like Name and Age, but we can also call local functions inside ! So to type the function, this is how basically it works :
type Person = {
Name: string,
Age: number,
Birthday: (self) -> number,
Greets: (self, who: Person) -> ()
}
So basically to explain the types, when it’s a function it is basically between parenthesis the arguments, and then the return value’s type.
So for example with the birthday’s function, we want self as parametter, which is basically the player that calls the functions ( i will explain later about self ) and it returns a number. Here we want it to return the new age after we increments it. So lets create the function :
local function PersonConstructor(name: string, age: number): Person
local newPerson: Person = {}
newPerson.Name = name
newPerson.Age = age
function newPerson:Birthday(): number
self.Age += 1
return self.Age
end
return newPerson
end
So now we can call the function like this !
local paul = PersonConstructor('Paul', 18)
print(paul.Age) --> 18
paul:Birthday()
print(paul.Age) --> 19
So to explain, further, when you create functions, you can have normal functions with .
which have pretty straight forward arguments. Basically you can use them like this :
function Foo.Add(a: number, b: number): number
return a + b
end
But you can also have something called method functions with :
, here, it have another default argument called self
. Basically, it is the object you used when calling the function.
function test:Hello()
return self
end
print(test:Hello()) --> test
Here, the object that calls the function is the test object, so it is also the self.
Note that the
:
can be used with.
if you give self as the first parametter :
function test:Hello()
return self
end
--> Is the same as
function test.Hello(self)
return self
end
--> And also the same as
test.Hello = function(self)
return self
end
So now for the greeting part, we can code it like this :
local function PersonConstructor(name: string, age: number): Person
local newPerson: Person = {}
newPerson.Name = name
newPerson.Age = age
function newPerson:Birthday(): number
self.Age += 1
return self.Age
end
function newPerson.Greets(self, who: Person)
print(self.Name .. ' says Hello to ' .. who.Name)
end
return newPerson
end
local person1 = PersonConstructor('Paul', 18)
local person2 = PersonConstructor('Tom', 12)
person1:Greets(person2) --> Paul says Hello to Tom
person1.Greets(person2, person1) --> Tom says Hello to Paul
Note that in the second usage of Greet, if you call as a normal function, you need to specify the self by hand, and the called, person1, is then useless, unless to provide the method.
Now this stuff is common, but now we can get in stuff EVEN BETTER AND FUNNIER ! Thanks to the wonderfull world of metadatas.
Type’s metadatas
Meta datas are basically stuff that allows you to watch an object. You can basically interract with anything, and metadatas will detect this, and you can do usefull and funny things with it.
For example, if you try now to print our person, what will it do ? Something ugly :
local elon = PersonConstructor('Elon Musk', 52)
print(elon) --> {...}
It will look like you’re print a dictionnary or a table. Now you can change this by using metatables like this :
local function PersonConstructor(name: string, age: number): Person
local newPerson: Person = {}
newPerson.Name = name
newPerson.Age = age
function newPerson:Birthday(): number
self.Age += 1
return self.Age
end
function newPerson.Greets(self: Person, who: Person)
print(self.Name .. ' says Hello to ' .. who.Name)
end
local mt = {
__tostring = function (self)
return "This, is " .. self.Name
end,
}
setmetatable(newPerson, mt)
return newPerson
end
local elon = PersonConstructor('Elon Musk', 52)
print(elon) --> This, is Elon Musk
So im not here to fully explain the metatables, but basically it is hidden tables inside other tables that will check for custom actions before doing anything.
For example here, the custom action is tostring, so whenever we are trying to convert our object to string, it will first look in metatables if there is a string convertion, else it will print the default table.
But we can for example detect you add 2 Person together, to create a baby !
...
local mt = {
__tostring = function (self)
return "This is " .. self.Name
end,
__add = function(self, t2: Person)
return PersonConstructor('Baby ' .. self.Name, 0)
end,
}
setmetatable(newPerson, mt)
return newPerson
end
local john = PersonConstructor('John', 23)
local lea = PersonConstructor('Lea', 22)
local baby = john + lea
print(baby) --> This is baby
You can also detect like for example when a value changes, for example if you want a Visible property:
...
local proxy = {}
local mt = {
__index = proxy,
__newindex = function(_, key, value)
if proxy[key] == nil then proxy[key] = value return end
print("Changed " .. tostring(key) .. " -> " .. tostring(value))
proxy[key] = value
end,
__tostring = function (self)
return "This is " .. self.Name
end,
__add = function (self, t2: Person)
return PersonConstructor('Baby ' .. self.Name, 0)
end,
}
setmetatable(newPerson, mt)
return newPerson
end
local john = PersonConstructor('John', 23)
john.Name = 'Hello' --> prints Name Changed
Note, if you watch carefully, you’ll see that i used proxy metatables. There is a reason because if you dont do that, it won’t detect values already added.