Help understanding OOP syntax (self, __index… etc)

I want to understand OOP syntax’s and just OOP in general that’s basically it any help will be super appreciated Thank you!!

just giving a brief overview:

  • It’s important to note that OOP isn’t possible in Lua as it is in other languages; that it is not natively supported. The OOP you see is a common workaround to allow complex data structures like this to exist.
self

self is a variable used in Lua to refer to a table from the table itself. That’s… literally all it does. It’s used in OOP to refer to the current object.

calling a function with a colon

a colon in Lua indicates that the item itself should be passed as a parameter. If you define a function with colon notation, self will automatically be defined as the first parameter.

function Class:Method(otherParam)
    --self is defined here
end

--other cose:
Object:Method(3) --> this tells Lua to pass Object as the first parameter to Method
Object.Method(Object, 3) --> does the same thing as the above statement
__index

__index is known as a metamethod and is used as a part of a metatable. A metatable is a table about a table; it defines certain behaviour of the table. The __index metamethod is used in Lua to basically incorporate OOP and make it work.

__index is invoked whenever a key in a table isn’t found. You can use it to return a custom value, or just direct Lua to another table to search for the key. Since you don’t define any methods in the object table, they will always be nil. So, whenever you try and call one, it’ll always be redirected to the Class table, where all of the methods are.

function Class.new()
    local self = setmetatable({}, {__index = Class}) --> setmetatable returns the first parameter
    -->now, whenever we try and locate a key in self that doesn't exist, Lua will also try to find it in the Class table
    return self
end

In summary, Lua uses workarounds to create the same functionality as OOP.

Any questions, please ask.

Hmm I understand it more now, but I don’t really know how I would use this in scripting. Could you maybe give me an example of code in ways I could use this? Is OOP something that you should use when u get the chance, or is there really just no point to it? How does it benefit you in scripting? Sorry I have a lot of questions.

it can be extremely useful when you need to create data structures with similar functionality but with different data.

Take for example a notification system. You want all your notifications to look the same and behave the same, but each one should have different data.

so each notification object could look like this:

{
    NotificationItem: Frame, --the notification itself
    Content: string, --the message to be displayed
    Duration: 5 --the duration in seconds
}

and then you could create tweens and apply changes to each object through the same piece of code, the method created for the class.


let’s take a real-life example - a car.

Each car is it’s own car, but it’s still known as a car. Think of a car as an object, and the idea of a car itself as the class. Each car would have its own data, such as its max gear, number of seats, et cetera. Each car would respond to methods, like accelerating and braking. But, each car is just an object.

Cars are (probably) manufactured from blueprints, right? It gives instructions on how a car should be constructed, and then since each car is slightly different, they might have different niche things like the colour or the number of doors. So, each car is it’s own object; that it is self-contained and responds to functionality from an outside source (like you pressing the accelerator, just how an object responds to methods when you call them through that object)


Finally, don’t force yourself to use OOP. Don’t try to find places to fit it in. Using it incorrectly can make your systems a lot more complicated than they need to be (I learnt that the hard way).

If you see situations like those I’ve described, 100% use it if it’s feasable.

Ok I’m getting there I think lol, but going back to the car thing you said. How would that look in code such as each car having different acceleration and braking? Would you make a function for the braking and acceleration? And just using the values based on the value in the table of car braking and acceleration? Or am I misunderstanding how to use OOP?

yes, there would be functions for them; when you accelerate a car, it doesn’t just show a higher value. it has logic to increase the car’s velocity as well, so it needs logic in code too!

function Car.new(colour: Color3, doors: number --[[etc.]])
    local self = setmetatable({}, {__index = Car})
    self.Colour = colour
    self.CarPart = --[[PrimaryPart of a Car model for example]]
    self.Doors = doors
    self.Speed = 0
    return car
end

function Car:Accelerate()
    --accelerating the car by 5 speed
    self.Speed = math.clamp(self.Speed, 0, 100)

    --the logic would probably be a little more complex but here's the jist of it
    self.CarPart.AssemblyLinearVelocity = self.CarPart.LookVector * self.Speed
end

function Car:Brake()
    --remove 5 speed from the car
    self.Speed = math.clamp(self.Speed - 5, 0, 100)

    --apply the same speed logic here because it's the same in this scenario
    --it might not be in other scenarios
    self.CarPart.AssemblyLinearVelocity = self.CarPart.LookVector * self.Speed
end

--[[
now, for other things like the number of doors,
you don't need any specific logic. All the logic for it
can depend on other pieces of code that need to
utilise it when-and-where; think of it like a variable in
a standard script. the logic in the script just does things
based off of that variable. Well, it's the same here,
but it's called an attribute in OOP.
]]

Yes thank you I know how to do the logic in the functions but I wasn’t sure how you would go about creating functions with the like
Car:FunctionNameHere()
What does the colon mean?

It is syntatic sugar for table.function(table), that looks ugly, so instead they made it so functions in a table, defined with the colon pass the table implicitly as a first argument. So these are equivalent:

table:f()
table.f(table)

--example with other args
table:f(1, 2, 3)
table.f(table, 1, 2, 3)

Colon just means the table is passed implicitly as the first argument which you can access inside of the function using self.

--equivalent function definitions
function table:f()

end

function table.f(self)

end

I don’t think I will ever learn OOP never I’m to dumb for this lol thank yall for helping though

1 Like

Even if you don’t learn it now, it’s a great paradigm to know and use so definitely learn it at some point.

Gonna show you a super simple example.

Let’s start with making our metatable.

local animal = {}

animal.__index = animal

animal.sound = "No sound"

function animal:make_sound()
 print( self.sound ) -- Prints "No sound"
end

This is our metatable.

The line animal.__index = animal, here you should see __index as a instruction.
animal is now our metatable.

Now we make a new and empty table like so.

local dog = setmetatable( {}, animal ) -- Creates a table and immediately sets it's metatable to "animal"

(Longer, simpler version of the above code would be this.)

local dog = {}

setmetatable( dog, animal ) -- Does the same thing.

Now we set dog’s metatable to animal.

Because of the __index instruction, Lua will now look inside of the animal table for any functions or properties if it does not exist inside dog.

Now if we call a function like so…

dog:make_sound()

The dog table, does NOT have a function named make_sound(), so it will look if that function exists inside animal instead and use THAT function.

Now you may also have noticed that we used the keyword self.

function animal:make_sound()
 print( self.sound ) -- Prints "No sound"
end

The self keyword is basically just syntax sugar.
self will always refer to the table that the function was used on.

Without self, our code would look like this nonsense below.

function animal.make_sound( my_animal )
 print( my_animal.sound ) -- Prints "No sound"
end

...

dog.make_sound(dog)

But there’s a problem here.
Our dog doesn’t dog, it still makes the “No sound” sound.

Let’s change that!

dog.sound = "Meow, whatever sound a dog makes"

Now if we call this function here…

dog:make_sound()

Now our dog will make the appropriate sound just as mother nature intended.

Our dog table now looks like this on the inside.

dog = {
  sound = "Meow, whatever sound a dog makes"
}

So self.sound will now return that instead.

Our full code would look like this below.

local animal = {}

animal.__index = animal

animal.sound = "No sound"

function animal:make_sound()
 print( self.sound ) -- Prints "No sound"
end



local dog = setmetatable( {}, animal )

dog.sound = "Meow, whatever sound a dog makes"

dog:make_sound() -- Prints "Meow, whatever sound a dog makes"

That’s the basics of OOP, you can now make classes and re-usable code!
I kept it as simple as possible to make it easy to understand.

I encourage asking questions because that’s how you learn things, it’s okay to not understand something.
That’s why you have all these nice and kind people on forums giving answers!

1 Like

You have explained this is the most noob way possible aka making me able to understand it thank you so much for this I understand it now

But would I use this for maybe like a gun system with different weapons is that a good way to use it to practice on?

1 Like

Not just thank you for the solution but everyone who has helped me understand OOP greatly appreciated

It can be used for anything you want really!
Gun systems, enemies, items, inventory, cars, literally anything.

Typically you do this sort of thing inside module scripts so that any script can basically “inherit” it.

Lua actually doesn’t really have OOP, it’s more of a functional language.
But Metatables (as we call them) can replicate OOP-like behavior and allow setting default values for empty tables.

For instance…

local gun = {}
gun.__index = gun

gun.damage = 20
gun.accuracy = 85

gun.ammo = 12
gun.capacity = 12
gun.max_ammo = 120

gun.sound = "Pew!"



function gun:shoot( target )

 if not self.ammo > 0 then
  print("Ah shoot, not enough ammo!")
  return -- Immediately stops the entire function.
 end

 print( self.sound )

 print( "Dealt " .. self.damage .. " damage to " .. tostring(target) )

 self.ammo -= 1

end


function gun:reload()

 self.ammo = self.capacity

 print("Reloaded!")

end



local revolver = setmetatable( {}, gun )

revolver.damage = 50

revolver.capacity = 6 -- Revolvers typically only fire 6 bullets.

revolver.ammo = 6

revolver.accuracy = 98

revolver:shoot("Zombie")

Oh, we can also simplify the entire process of making revolver and do this instead.

If we add this piece of code to our gun metatable, we can make revolvers quicker and easier to make.

function gun:new(ammo, damage, accuracy)

 local new_gun = setmetatable( {}, self ) -- self refers to "gun" metatable here.

 new_gun.ammo = ammo
 new_gun.capacity = ammo
 new_gun.damage = damage
 new_gun.accuracy = accuracy

 return new_gun

end

Also notice the use of self here again, this keyword will ALWAYS refer to the table that it was called on, in this case our gun table because we are going to do gun:new().

We use self so we can write this:

local revolver = gun:new()

Instead of this:

local revolver = gun.new(gun)

Now that we have this function, we can write less code and just create a new revolver like so.

local revolver = gun:new(6, 50, 98)

revolver:shoot("Zombie")

Our complete code would now look like this.

local gun = {}
gun.__index = gun

gun.damage = 20
gun.accuracy = 85

gun.ammo = 12
gun.capacity = 12
gun.max_ammo = 120

gun.sound = "Pew!"



function gun:shoot( target )

 if not self.ammo > 0 then
  print("Ah shoot, not enough ammo!")
  return -- Immediately stops the entire function.
 end

 print( self.sound )

 print( "Dealt " .. self.damage .. " damage to " .. tostring(target) )

 self.ammo -= 1

end


function gun:reload()

 self.ammo = self.capacity

 print("Reloaded!")

end


function gun:new(ammo, damage, accuracy)

 local new_gun = setmetatable( {}, self ) -- self refers to "gun" metatable here.

 new_gun.ammo = ammo
 new_gun.capacity = ammo
 new_gun.damage = damage
 new_gun.accuracy = accuracy

 return new_gun

end





local revolver = gun:new(6, 50, 98)

revolver:shoot("Zombie")

Metatables can even inherit from each other, although this practice is generally not recommended (by me personally) because Lua is not very optimized for that sort of thing and it can make code more complicated than necessary hence why I won’t explain it in this post.

I hope this clarifies the essentials for you, this is honestly usually the only thing you need.

Once you understand OOP, I also recommend looking into OTHER metatable functions.
It’s in fact even possible to write functions that allow you to add/subtract/multiply tables together and customize what happens to them!

But that’s also something for another day, as to not overwhelm you with too much new information.

Ok so based on the code you have showed I managed to get this

Local Script Controlling the Module:

local testmodule = require(script.Test)

local Ar =  testmodule.gun:CreateNewGun("Ar", 30, 20, 0.1, 1, "Auto", 300)
print(Ar)

Module Script I Have:

local GunModule = {}

GunModule.gun = {}
GunModule.gun.__index = GunModule.gun

function GunModule.gun:CreateNewGun(gunname, ammo, damage, firerate, shotsperclick, firemode, startingammo)
	
	local created_Gun = setmetatable( {}, self)
	created_Gun.Ammo = ammo;
	created_Gun.Damage = damage;
	created_Gun.Firerate = firerate;
	created_Gun.ShotsPerClick = shotsperclick;
	created_Gun.FireMode = firemode
	created_Gun.StartingAmmo = startingammo;
	
	return created_Gun
	
end

return GunModule

Please let me know if I am doing something wrong in this code

I do have a few questions about this though

  1. What is the point in doing this? What does this do?
GunModule.gun.__index = GunModule.gun
  1. How would I use something like this to position the ViewModel?