local Gun = {}
Gun.__index = Gun
function Gun.New(Damage, Auto, Ammo)
local newGun = {}
newGun.Damage = Damage
newGun.Auto = Auto
newGun.Ammo = Ammo
setmetatable(newGun, Gun)
end
function Gun:Shoot()
print(self)
if self.Auto == false then
self.Ammo -= 1
elseif self.Auto == true then
self.Ammo -= 5
end
end
return Gun
If I understand it correctly, the newGun also has the method of New. That means that New can be called in the object itself.
Is that supposed to happen?
Currently, it seems confusing that the New method is also in the newGun object.
I am no OOP expert, so bear with me here. I tried to separate the New method from the newGun. This is the script I created:
local Gun = {}
local Functions = {}
Functions.__index = Functions
function Functions:Shoot()
if self.Auto == false then
self.Ammo -= 1
elseif self.Auto == true then
self.Ammo -= 5
end
print(self)
end
function Gun.new(Damage : number, Auto : boolean, Ammo : number)
local newGun = {}
newGun.Damage = Damage
newGun.Auto = Auto
newGun.Ammo = Ammo
setmetatable(newGun, Functions)
return newGun
end
return Gun
After testing it out in a separate script, it seems to work. However, as I have said, I am no OOP expert.
So, is this a correct way to use OOP? Is there a flaw in the code I created?
It’s better to think of new() as init() - short for initialize. This is what OOP languages tend to use with their classes.
The Gun object doesn’t exist until we initialize it, so we need to call a method that creates both the object (in this case the metatable) and passes some default parameters to it. In this case, we pass Damage, Auto and Ammo to it.
If we weren’t to initialize the object before use, there wouldn’t be an object in the first place!
Updated for clarity and rephrased to remove a statement that raised some doubt.
The first example is missing a return newGun.
First of all, congratulations on making your own version. You essentially created two tables, one that is returned by require(), and the other that stores the methods. It’s fine if you use that. Performance wise, I can’t speak of a notable difference.
When you call require(), the returned table in the module script is saved to the variable. Then the constructor (function) is called, standardly named .new. A smaller table is returned, with a metatable pointing to the class. If an indexed element of the object is nil (such as :Shoot, lua will search for this element in the object before returning nil and erroring.
In your version, the metatable is pointing to the Functions table. Potential benefit? The returned table module returns is slightly smaller, because it doesn’t store all the other functions.
In general Lua OOP, the Gun and the Functions are combined into the same table. Nevertheless, I’ve seen implementations like yours before, especially with very large scripts.
I am very sorry, but the explanation you guys provided still does not answer my questions. It feels as though I am missing an information, or I am misunderstanding something about these “Class”. I’ll strive to understand and analyze your guys’ explanation, but for now I am still open to other dev’s explanation.
If my OOP understanding is (hopefully) correct, am I not just attaching the methods of Functions to the newGun object? Not sure if I am misunderstanding here, but it seems you are pointing out that I have two metatables.
You can think of your object as 2 tables. A data table that makes up the state of the object like Gun.Damage, Gun.Ammo type of stuff, and a function table that holds all the functions that use that data. When you setmetatable(newGun, Functions), you’re effectively telling your script that if it can’t find a thing called .Shoot in newGun, it should look inside Functions for a .Shoot and call that one if it exist. In this way you can make a single table have every function that all the objects share because they don’t need their own copy of the data. Then when you make a new gun it just makes a table with all the guns data and when you call gunDataTable:Shoot(), it’s secretly doing GunDataTable.Shoot(GunDataTable). The : just passes the table it’s called on as the first argument (usually called self).
It’s not wrong if you prefer it that way. Generally the initialized function is part of the function table, so it’s not how it’s usually done, but it’s not wrong.
Lua OOP is pretty versatile, As a developer the answer is if you want it to intentionally error or not for anyone using the OOP.
So if I am correct your confusion is that the current code can do this:
local Gun = {}
Gun.__index = Gun
function Gun.New(Damage, Auto, Ammo)
local newGun = {}
newGun.Damage = Damage
newGun.Auto = Auto
newGun.Ammo = Ammo
setmetatable(newGun, Gun)
return newGun
end
local gun = Gun.New(2,4,5).New(1,2,3)
print(gun) --Prints out a gun object
Which is pretty weird compared to Roblox built in datatype format such as Vector3:
local vector = Vector3.new().new() --error
So yes you can use your method of separating the function of New from newGun if you want to follow the same behavior. From this you can infer that Roblox has done this to prevent the first scenario from happening to avoid potential confusion and such.
From my limited computer science/software knowledge this should fall under OOP Encapsulation as it is a method to restrict the properties and methods you can access from an object.
Personally, I’ll stick with method 1 as it’s easier and I can avoid doing .new().new()
I’m sorry I didn’t answer your question adequately, @Quwanterz. In lua, one attaches a metatable to the object constructed with .new() function in order to link it to the class. FusionOak put it nicely. The metatable will point to the class instead of returning nil right away, if an element is not found in the returned object.
Please take this post solely as an extension of existing answers.
There indeed is one metatable in both cases - set in the constructor function. On the other hand is the metamethod __index of the class pointing to the class itself. getmetatable(Gun) in the first example (respectively getmetatable(GunMechanics) in the second) will return nil, if you attempt to print them.
Technically, following the traditional approach, a class is created via object.new(). And as dthecoolest pointed out, Roblox made certain members of the class private, locked the metatables and prevented indexing of .new() on the returned object. The difference is that the engine runs in C and the workflow is a bit different with LuaVM and userdata accessing C functionality.
Because .new is a standard constructor, many don’t bother and simply ignore it as if it were not available in the returned instance.
Nevertheless, we can simulate this in lua with the help of some type notation. I’m happy to be the first to share this underappreciated article by blobbyblob about hiding private members (normally prefixed with an underscore), and metamethods.
Thank you for all the responses! There are still some cracks here and there, but I think I can draw a conclusion in my head, especially the link from @waves5217 to another Community Resources. Thank you for taking your time in helping me in my problem!