Great introduction to metatables
I want to understand this, but I don’t understand this and don’t know what to do.
I feel so below everyone here.
I need to stop being a moron with all this theory stuff and get better with all this table stuff, but god damnit it’s hard to find where to start.
I only came here cause there’s another thread discussing OOP and I’m just here like ‘wtf is everyone talking about?’ and feeling really stupidly out of place.
[quote] I want to understand this, but I don’t understand this and don’t know what to do.
I feel so below everyone here.
I need to stop being a moron with all this theory stuff and get better with all this table stuff, but god damnit it’s hard to find where to start.
I only came here cause there’s another thread discussing OOP and I’m just here like ‘wtf is everyone talking about?’ and feeling really stupidly out of place. [/quote]
http://hastebin.com/oyivihiraf.lua
[quote] I want to understand this, but I don’t understand this and don’t know what to do.
I feel so below everyone here.
I need to stop being a moron with all this theory stuff and get better with all this table stuff, but god damnit it’s hard to find where to start.
I only came here cause there’s another thread discussing OOP and I’m just here like ‘wtf is everyone talking about?’ and feeling really stupidly out of place. [/quote]
Probably the best place to learn about metatables would be the Minecraft mod, Computercraft.
It’s where I learned all about metatables. By accident too, I was just looking at the vector API to be able to create my own stuff similar to it.
Well I understand metatables fairly well now and get how they work (still dont understand their use), but this whole OOP thing is like a bloody maze. It reminds me of first trying to understand FilterEnabled.
I take it OOP is the hardest thing Lua has to offer, it sure feels like it.
If any of you have some relatively easy OOP related scripts you don’t mind sharing so I can route through them, I’d be delighted to do so.
[quote] Well I understand metatables fairly well now and get how they work (still dont understand their use), but this whole OOP thing is like a bloody maze. It reminds me of first trying to understand FilterEnabled.
I take it OOP is the hardest thing Lua has to offer, it sure feels like it.
If any of you have some relatively easy OOP related scripts you don’t mind sharing so I can route through them, I’d be delighted to do so. [/quote]
Here you go, a ModuleScript to create your own custom class:
[code]Thing = {}
Thing.__index = Thing
–A function to construct the object (a constructor)
–Ideally, all values you want the object to have would be initialized here.
function Thing.new(value)
local new = {}
new.Value = value
setmetatable(new,Thing)
return new
end
–Define some methods. The object you’re calling it on will be referred to as ‘self’ inside the methods.
function Thing:setValue(value)
self.Value = value
end
function Thing:getValue()
return self.Value
end
return Thing
[/code]
Now, to use it:
[code]Thing = require(script.Parent.ModuleScript)
obj = Thing.new(3)
print(obj:getValue()) → 3
obj:setValue(1234)
print(obj:getValue()) → 1234
[/code]
I hope this helps!
I’d like to thank Emma for this guide because it really helped me with Redirection.
I’m having trouble understanding the use case for this stuff, apologies, but it all seems a bit confusing.
Stick with it Kevin. OOP is extremely useful, not just on ROBLOX, but in the industry. One way to figure it out that I’ve found helpful is using ROBLOX objects as sort of your model internally for how you expect the object you make to operate.
Even though this is two years old, still is a useful tutorial about Metatables (Though maybe @magnalite could reformat this to work on this forum ;)) But I do have a question and I rather not make a separate thread as it could easily be answered and added to the tutorial for future readers…
Once you have made an object using OOP, how can you make your own :Destroy() method that’ll remove the object by itself, so I don’t have to do something like:
Inventory.Primary:Destoy()
Inventory.Primary = nil
To completely remove the object, I thought doing something like:
function Object:Destroy()
local n = self.Name
self.ToolModel:Destroy()
self = nil
print(n, “was successfully destroyed!”)
end
Would work, however it only deletes the model and not the object as self is just a path(?) and not the object itself. How would I delete the object from a method inside the metatable?
Your example is pretty spot on for this. You simply create the Destroy method that internally calls Destroy on the necessary items.
However, setting self
to nil
isn’t going to do anything. What you’re trying to do is remove the object entirely, but we should just let the garbage collector do that for us.
When you call Destroy() on any normal object in ROBLOX, it will only be GC’d once all references to it are gone. The exact same thing goes for tables. Of course, you can configure your table a bit more by making it have weak keys or values.
In summary, just make your Destroy() method destroy the actual objects that need to be destroyed. And then once your other scripts get rid of any references to that object, it will eventually be GC’d.
I think I’m doing this wrong, following the example of OP for creating and inheriting metatables and their methods/functions. Below in the BaseTool section I create what I call a template for my custom Tool object that all other “tools” (such as a sword or a gun) use for a template. I have an odd thing going on, whenever I call Tool:Destroy() on a BaseTool (Tool) object it’ll destroy it, and when I call RangedWeapon:Destroy() it’ll also be destroyed. But when I try to call :Destroy() on the PropaneTank, whether it be done by calling PropaneTank:Destroy() (when the object is spawned by using PropaneTank.new()) or by calling self:Destroy(), the script will error and say that Destroy is a nil value.
I followed step 2.2 of the original post, except the only change I made was by adding __index inside of the PropaneTank.new() function so I could read the properties of the PropaneTank… But if I remove that then my script will also break… Yeah I’m not sure what’s going on anymore. Here’s my code to see what I’m doing.
---------------------
-- ROBLOX Services --
---------------------
local RunService = game:GetService("RunService")
local RS = game:GetService("ReplicatedStorage")
-------------------
-- ModuleScripts --
-------------------
local Utilities = require(RS:WaitForChild("Utilities"))
---------------
-- Variables --
---------------
local Weapons = {
Ranged = {},
Melee = {},
Other = {},
}
for Index, ModuleScript in pairs(RS:WaitForChild("Weapons"):WaitForChild("Ranged"):GetChildren()) do
if ModuleScript:IsA("ModuleScript") then
local DataToCopy = require(ModuleScript)
local DataToSave = Utilities:DeepCopy(DataToCopy)
DataToSave.Type = "Weapon"
Weapons.Ranged[#Weapons.Ranged+1] = DataToSave
end
end
for Index, ModuleScript in pairs(RS:WaitForChild("Weapons"):WaitForChild("Melee"):GetChildren()) do
if ModuleScript:IsA("ModuleScript") then
local DataToCopy = require(ModuleScript)
local DataToSave = Utilities:DeepCopy(DataToCopy)
DataToSave.Type = "Weapon"
Weapons.Ranged[#Weapons.Ranged+1] = DataToSave
end
end
for Index, ModuleScript in pairs(RS:WaitForChild("Weapons"):WaitForChild("Support"):GetChildren()) do
if ModuleScript:IsA("ModuleScript") then
local DataToCopy = require(ModuleScript)
local DataToSave = Utilities:DeepCopy(DataToCopy)
DataToSave.Type = "Support"
Weapons.Other[#Weapons.Other+1] = DataToSave
end
end
for Index, ModuleScript in pairs(RS:WaitForChild("Weapons"):WaitForChild("Other"):GetChildren()) do
if ModuleScript:IsA("ModuleScript") then
local DataToCopy = require(ModuleScript)
local DataToSave = Utilities:DeepCopy(DataToCopy)
DataToSave.Type = "Other"
Weapons.Other[#Weapons.Other+1] = DataToSave
end
end
local function FindTool(ToolName)
for FolderName, FolderContents in pairs(Weapons) do
for Index, Tool in pairs(FolderContents) do
-- print(Tool.Name)
if Tool.Name == ToolName then
return Tool, FolderName
end
end
end
end
local function CreatToolAnimationData(Parent, IndexName, OriginalTable, ToolName)
local Type = type(OriginalTable)
local copy
if Type == "table" then
copy = {}
for orig_key, orig_value in next, OriginalTable, nil do
copy[CreatToolAnimationData(Parent, orig_key, orig_key, ToolName)] = CreatToolAnimationData(Parent, orig_key, orig_value, ToolName)
end
elseif Type == "number" then
local AnimationObject = Instance.new("Animation")
AnimationObject.Name = ToolName.."_"..IndexName
AnimationObject.AnimationId = "http://www.roblox.com/asset/?id="..OriginalTable
AnimationObject.Parent = Parent
copy = {AnimationObject, nil}
else
copy = OriginalTable
end
return copy
end
local function FindHighestFiringMode(ToolData)
local FiringModes = ToolData.FiringModes
if #FiringModes > 1 then
local SelectedMode = 1
for Index, Mode in pairs(FiringModes) do
for _,M in pairs({"Semi", "Burst", "Auto"}) do
if Mode == M then
SelectedMode = Index
end
end
end
return SelectedMode
else
return 1
end
end
local Service = {}
Service.RetrieveToolData = FindTool
-------------------------
-- Base Tool Class API --
-------------------------
Service.Tool = nil
do
local Class = {}
Class.__index = Class
function Class.new(ToolName, Player)
local Data = FindTool(ToolName)
if Data ~= nil then
local Tool = {}
setmetatable(Tool, Class)
Tool = Utilities:DeepCopy(Data)
Tool.Parent = Player
Tool.ToolModel = Tool.ToolModel:Clone()
Tool.ToolModel.PrimaryPart = Tool.ToolModel.Handle
Utilities:WeldTool(Tool.ToolModel)
Tool.SavedWeldData = Utilities:SaveWeldData(Tool.ToolModel)
Tool.ToolModel.Parent = Tool.Parent
Tool.Animations = CreatToolAnimationData(Tool.ToolModel, Tool.Name, Tool.Animations, Tool.Name)
-- Tool.Equipped
local Equipped = Instance.new("BindableEvent", Tool.ToolModel)
Class.Equipped = Equipped.Event
-- Tool.Unequipped
local Unequipped = Instance.new("BindableEvent", Tool.ToolModel)
Class.Unequipped = Unequipped.Event
return Tool
else
spawn(function() error("Error there is no such tool named "..tostring(ToolName)..".") end)
return nil
end
end
function Class:Destroy()
local n = self.Name
self.ToolModel:Destroy()
self = nil
print(n, "was successfully destroyed!")
end
Service.Tool = Class
end
-----------------------------
-- Ranged Weapon Class API --
-----------------------------
Service.RangedWeapon = nil
do
local Class = {}
Class.__index = Class
setmetatable(Class, Service.Tool)
function Class.new(WeaponName, Player, AmountOfAmmo)
local RangedWeapon = Service.Tool.new(WeaponName, Player)
if RangedWeapon == nil then spawn(function() error("Error, BaseWeapon is nil.") end) return end
local MaximumAmountAllowed = (RangedWeapon.MaxAmountOfMagazines * RangedWeapon.MagazineCapacity)
if AmountOfAmmo >= MaximumAmountAllowed then
AmountOfAmmo = MaximumAmountAllowed
end
setmetatable(RangedWeapon, Class)
RangedWeapon.SelectedFiringMode = FindHighestFiringMode(RangedWeapon)
RangedWeapon.TotalAmmo = AmountOfAmmo
RangedWeapon.MagAmmo = RangedWeapon.MagazineCapacity
return RangedWeapon, AmountOfAmmo
end
Service.RangedWeapon = Class
end
----------------------------
-- Melee Weapon Class API --
----------------------------
Service.PropaneTank = nil -- this is the only proper metatable...
do
local Class = {}
Class.__index = Class
setmetatable(Class, Service.Tool)
function Class.new(Parent)
local PropaneTank = Service.Tool.new("Propane Tank")
local DamagableTag = Instance.new("BoolValue")
DamagableTag.Name = "Damagable"
DamagableTag.Value = true
DamagableTag.Parent = PropaneTank.ToolModel
PropaneTank.BackingTable = {IsDead = false}
PropaneTank.CreationTimeStamp = tick()
PropaneTank.Name = nil ; PropaneTank.BackingTable.Name = "Propane_Tank" ; PropaneTank.ToolModel.Name = PropaneTank.BackingTable.Name
PropaneTank.Health = nil; PropaneTank.BackingTable.Health = 5
PropaneTank.Position = nil
PropaneTank.CFrame = nil
PropaneTank.Parent = nil ; PropaneTank.ToolModel.Parent = Parent
setmetatable(PropaneTank, Class)
Class.__index = function(self, Index)
return self.BackingTable[Index]
end
Class.__newindex = function(self, Index, Value)
--rawset(self, Key, Value)
self.BackingTable[Index] = Value
-- rawset(PropaneTank.BackingTable, Key, Value)
if Index == "Name" then
self.ToolModel.Name = Value
elseif Index == "Parent" then
self.ToolModel.Parent = Value
elseif Index == "CFrame" then
self.ToolModel:SetPrimaryPartCFrame(Value)
self.BackingTable.Position = Value.p
elseif Index == "Position" then
--[[
local OldCF = self.BackingTable.CFrame
local angles = OldCF - OldCF.p
self.BackingTable.Position = Value.p
--]]
self.BackingTable.Position = Value.p
self.ToolModel.PrimaryPart.Position = Value
elseif Index == "Health" then
if self.BackingTable.Health == 0 and not self.BackingTable.IsDead then
self.BackingTable.IsDead = true
local e = Instance.new("Explosion")
e.Position = self.ToolModel.PrimaryPart.CFrame.p
e.DestroyJointRadiusPercent = 0
e.BlastRadius = 15
e.Parent = game.Workspace
for Index, Zombie in pairs(_G.Zombies) do
local Distance = (e.Position - Zombie.Torso.Position).Magnitude
if Distance <= e.BlastRadius then
Zombie:TakeDamage(1000)
end
end
if _G.MapData then
for Index, Tank in pairs(_G.MapData.PropaneTanks) do
if Tank ~= self and not Tank.BackingTable.IsDead and Tank.ToolModel.PrimaryPart ~= nil then
local Distance = (e.Position - Tank.ToolModel.PrimaryPart.CFrame.p).Magnitude
if Distance <= e.BlastRadius then
Tank.Health = 0
end
end
end
end
-- only for testing
self:Destroy() -- delay(0.5, function() self.BackingTable.IsDead = false end)
end
end
-- warn(self.Name.."."..(Index).." property was changed to "..tostring(Value)..".")
end
return PropaneTank
end
function Class:Destroy()
self.ToolModel:Destroy()
end
Service.PropaneTank = Class
end
return Service
If you do
PropaneTank.BackingTable.__index = Class
Does that fix it? Im hoping it doesn’t cause a circular loop. If that works I can explain why if you would like me to.
That appeared to do nothing.
What about if you change
Class.__index = function(self, Index)
return self.BackingTable[Index]
end
to
Class.__index = function(self, Index)
return self.BackingTable[Index] and self.BackingTable[Index] or Class
end
Still nil.
Did a test and removed Class.__index = blah from the .new function and calling either PropaneTank:Destroy() or self:Destroy() would work, so its something going on in with that __index. …Is it overwriting the Tool metatable’s method? or something?
@magnalite - I’ve gone ahead and fixed the formatting of your post to work with Discourse I kept the same formatting and content, just made it work with Discourse’s formatting which doesn’t support [center] BBCode. I’ve also moved it to the Tutorials section. Thank you for this wonderful tutorial!
@Lilly_S Thanks! Much appreciated
The way .__index works is it is called when an index does not exist. In this case when you create an object you create a new empty table with data about the objects in. It has none of the methods in it. To allow your object to have the appropriate methods called on it a metatable with its .__index pointing to the class table is assigned to the table with the objects data in it. When .__index returns a table lua looks into that table to see if the index exists in that too. So what actually happens when you call a method on an object is this.
Object:DoSomething()
Object[“DoSomething”] is nil, does its metatable have an .__index set?
It does, .__index returned SomeClass.
Does SomeClass[“DoSomething”] exist?
It does! call it!
This allows you to assign the methods without duplicating them into every single object you make.
In your case you are making .__index return a value from BackingTable and so it never has a chance to see if the method exists in the Class table.
I’m not sure why my previous suggestion wouldn’t work though
Probably because there was no Class[Index], just Class.
And by doing;
Class.__index = function(self, Index)
return self.BackingTable[Index] or Class[Index]
end
I was able to get the :Destroy() function to work. ^.^ Thanks!
@Lilly_S Thanks for updating and moving the thread!
^-^ Btw I find it helps if I keep my classes in separate module scripts and require them when I need them.