Trouble Using Custom Methods Created Inside An OOP Class Which Inherits From a Main/Base Class

Hello, thanks for taking the time to read my post.

  1. What do I want to achieve?

I would like to know either what it is that is wrong with my syntax when attempting to create a class structure for a game I am creating, or whether or not what I am attempting to do is even possible.

  1. What is the issue?

When creating a subclass, I have no trouble at all inheriting both properties and methods in the main class. However, when I want to create a new method specific to a subclass, I cannot for the life of me figure out how to actually use it in the context of “local newClass = MainClass:New()”

In the example below, in the main ServerScript, the method that is inherited from the BaseClass works, but the newly created :FIre() method does not, and gives the error “Attempt to call a nil value”, which I assume means that the pistolClass:New() constructor does not contain the newly created :Fire method, and I am unsure how to fix it.

I just created this easier to read example to showcase the issue.

-- Example main "GunClass"

-- Services:
local ServerScriptService						= game:GetService("ServerScriptService")
local ReplicatedStorage							= game:GetService("ReplicatedStorage")

-- Create base Gun Class
local GunClass = {}
GunClass.__index			= GunClass

function GunClass:New()
	local self			= setmetatable({}, self)
	self.FireRate		= 1
	self.BaseDamage		= 15
	self.TestPropety	= "Nooo"
	self.BeastMode		= true
	
	return self
end

function GunClass:BaseFire()
	print(self.BaseDamage)
end


return GunClass

Really nothing special.

-- Example pistol class which inherits the properties and methods, but attempts to create a custom method as well.

-- Services
local ReplicatedStorage							= game:GetService("ReplicatedStorage")
local ServerScriptService						= game:GetService("ServerScriptService")

-- Remote Event Module
local RemoteEventModule							= require(ReplicatedStorage:WaitForChild("RemoteEventModule"))

-- Base Gun Class
local GunClass									= require(script.Parent)

-- Every Pistol Bullet Option
local DefaultBulletClass						= require(script.Parent.Parent:WaitForChild("BulletClass"))
local Tier1PistolBullet							= require(script:WaitForChild("Tier1PistolBullet"))

-- The Module
local PistolClass = {}
setmetatable(PistolClass, {__index = GunClass})

--  Table of functions for each bullet type
PistolClass.BulletTypes = {
	--[DefaultBulletClass] = Tier1PistolBullet;
	["Tier1PistolBullet"] = function()
		return Tier1PistolBullet:New()
	end,
}


-- Create default stats that the bullet will inherit (and edit)
function PistolClass:New()
	local self			= GunClass:New()
	self.FireRate		= 1
	self.BaseDamage		= 20
	
	warn("Created new Pistol Var")
	
	return self
end

function PistolClass:Fire(bullet)
	local bullet		= PistolClass.BulletTypes[bullet]()

	-- Fire the gun hopefully
	bullet:Fire()

end

return PistolClass

And lastly, the ServerScript I have been using to test this:

local ServerScriptService				= game:GetService("ServerScriptService")

-- classes
local GunClass							= require(ServerScriptService.WeaponClasses:WaitForChild("GunClass"))
local pistolClass						= require(ServerScriptService.WeaponClasses.GunClass:WaitForChild("PistolClass"))

task.wait(1)

local Pistol	= pistolClass:New()

Pistol:BaseFire() -- works

Pistol:Fire() -- does not work
  1. What solutions have I tried thus far?

Basically just attempting different ways to write each of the classes. I will not pretend like I am an expert on the subject, but I did as much research as I could, trying various ways, and I am unsure how to get this pretty basic-in-theory idea to work.

Also, if anyone has good resources on OOP that are easily applied to LUAU, I would appreciate if you shared them. I have pretty much seen every YouTube tutorial that relates to Roblox, as well as read a majority of the devforum posts.

thanks again,

The GunClass constructor miraculously works, but you shouldn’t write it like that. Class constructors should be declared using the dot operator, not the colon operator (i.e. function GunClass.New(), not function GunClass:New()).

local gun = GunClass:New()
-- Is equivalent to
local gun = GunClass.New(GunClass)

The GunClass table gets captured in the implict self parameter, so the statement local self = setmetatable({}, self) actually works as intended (it is equivalent to local self = setmetatable({}, GunClass)), to my surprise.

However, the issues are the resulting strange-looking line of code and the fact that you can now call the constructor with any created objects of that class. So you could actually do the following:

local gunObject = GunClass:New()
local anotherGunObject = gunObject:New()

To make things less confusing and avoid that side-effect, rewrite the constructor:

function GunClass.New()
    local self = setmetatable({}, GunClass)
    -- Everything else
end

In PistolClass, you still have to set the subclass’s (PistolClass) __index to itself, and then set the metatable of the subclass to GunClass (the super class).

local PistolClass = {}
PistolClass.__index = PistolClass
setmetatable(PistolClass, GunClass)

And finally, in PistolClass’s constructor (which you should also change to use dot notation), you must set the metatable of self to PistolClass to give the created object access to PistolClass methods.

local self = GunClass.New()
setmetatable(self, PistolClass)

I am aware of the difference between colon and dot notation in constructors, I just prefer to see it with the colon, automatically passing self. However, if it is bad practice, I will change it. However, I actually did manage to find a solution using colon notation, so if you could look at it and let me know what you think, I would appreciate it.

Pistol Class:

-- Gun class is the exact same
-- Services
local ReplicatedStorage							= game:GetService("ReplicatedStorage")
local ServerScriptService						= game:GetService("ServerScriptService")

-- Remote Event Module
local RemoteEventModule							= require(ReplicatedStorage:WaitForChild("RemoteEventModule"))

-- Base Gun Class
local GunClass									= require(script.Parent)

-- Every Pistol Bullet Option
local DefaultBulletClass						= require(script.Parent.Parent:WaitForChild("BulletClass"))
local Tier1PistolBullet							= require(script:WaitForChild("Tier1PistolBullet"))

-- The Module
local PistolClass = {}
PistolClass.__index = PistolClass
setmetatable(PistolClass, GunClass) -->>> changing these two lines

--  Table of functions for each bullet type
PistolClass.BulletTypes = {
	--[DefaultBulletClass] = Tier1PistolBullet;
	["Tier1PistolBullet"] = function()
		return Tier1PistolBullet:New()
	end,
}


-- Create default stats that the bullet will inherit (and edit)
function PistolClass:New()
	local self			= GunClass:New()
	setmetatable(self, PistolClass) -- And changing this line allowed me to access :Fire()
	
	self.FireRate		= 1
	self.BaseDamage		= 20
	
	return self
end

-- TODO: Hopefully this will take the argument which is the bullet the player has equipped
function PistolClass:Fire(bullet)
	local bullet		= PistolClass.BulletTypes[bullet]()

	-- Fire the gun hopefully
	bullet:Fire(self)

-- ^^^ works as intended from the ServerScript
	

end

return PistolClass

Aside from your previous feedback, see anything wrong with the commented lines?

The convention is to use the dot operator for constructors, but if you don’t see an issue with using the colon, I guess that’s fine.

Here’s the official style guide for reference (method declarations use dot operator for typechecking; if you don’t want to use typechecking, stick with colon): Roblox Lua Style guide

Other than that, looks like you’ve made the main changes for the subclass to work.

Convention is to use methods when we need access to the object’s state, and to use functions when we do not.

local class = {}

function class.new(name) -- Does not need access to the state, because we're creating an instance
  local self = {}
    self.Name = name
  return self
end

function class:sayHello() -- Requires current state (name)
  print('Hello, ' .. self.Name)
end