[OOP] Problem when inheriting methods from gun base-class

I messed up.

I’ve been developing this simple gun framework for a game, but the problem is when inherriting methods from the base-class, which I wasn’t aware of at the time, are called in the same module.

This is problematic for me since I assumed if I create a sub-class I could just change some values, but the module isn’t aware of these modifications and uses the default values.

Two problems are noticable. Firstly, the module can’t require the correct server module. Second I can’t add or change firing modes such as adding auto, burst, ect. becaue changes to the table aren’t accessible and even if I found a way around this, the module will not be able to call the functions.

Should I re-factor into composition? Is there a way this can be salvaged? I want suggestions…

VVV [ CODE ] VVV

BaseClass

This is run by a local script that connects equip and unequip to a tool and keys to CAS

--ClientModule
local ReplicatedFirst = game:GetService("ReplicatedFirst")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TweenService = game:GetService("TweenService")
local UserInputService = game:GetService("UserInputService")
local Auxillary = require(ReplicatedStorage.Modules.Auxillary.Auxillary)
local BetterRobloxAPI = require(ReplicatedStorage.Modules.BetterRobloxAPI)

local actionService = ReplicatedStorage.Remotes.ActionService
local replicateRemote = ReplicatedStorage.Remotes.Replicate

local assets =  ReplicatedFirst.Assets.Gun
local sound = assets.Sound
local casterParams = RaycastParams.new()
local function isMouseButtonDown(inputType)
	for _, button in UserInputService:GetMouseButtonsPressed() do
		if button.UserInputType == inputType then
			return true
		end
	end
	return false
end


local BASE_GUN = {}
BASE_GUN.__index = BASE_GUN

----[{STATS}]----
BASE_GUN.reloadTime = 1
BASE_GUN.firingModes = {
	"Semi"
}
BASE_GUN.timeBetweenShots = 0.02
BASE_GUN.magazineCompacity = 15-- Max amount of bullets in a mag
BASE_GUN.maxAmmo = 96
----[[=====]]----

BASE_GUN.Keys = {
	["Click"] = {["Name"] = "Fire", 
		["Inputs"] = {Enum.UserInputType.MouseButton1}, 
	},
	["R"] = {["Name"] = "Reload", 
		["Inputs"] = {Enum.KeyCode.R}, 
	},
	["C"] = {["Name"] = "Crouch",
		["Inputs"] = {Enum.KeyCode.C}, 
	},
	["Q"] = {["Name"] = "PeakLeft",
		["Inputs"] = {Enum.KeyCode.Q}, 
	},
	["E"] = {["Name"] = "PeakRight",
		["Inputs"] = {Enum.KeyCode.E}, 
	},
	["V"] = {["Name"] = "FiringMode",
		["Inputs"] = {Enum.KeyCode.V}, 
	},
}

function BASE_GUN.new(User:Player, AuxObjects, tool:Tool)
	local self = setmetatable({
		Janitor = Janitor.new()	
	}, BASE_GUN)

	self.user = User
	self.character = User.character
	self.tool = tool
	self.mouse = User:GetMouse()

	self.CameraShaker = AuxObjects.CameraShaker
	self.StatusManager = AuxObjects.StatusManager 
	
	self.spareAmmo = BASE_GUN.maxAmmo--How much of the max ammo did we start out with is left
	self.firingMode = BASE_GUN.firingModes[1]
	self.bulletsLeft = 0
	self.timeLastFired = os.clock()

	self.Disabled = false
	self.Equipped = false

	return self
end

--Equipping
function BASE_GUN:Equip()
	local serverArgs = {
		["Module"] = script.Name,
		["Action"] = debug.info(1, "n"),
		["Tool"] = self.tool,
	}
	
	actionService:InvokeServer(serverArgs)
	self.Equipped = true
end

function BASE_GUN:Unequip()
	self.Equipped = false
end

function BASE_GUN:FiringMode()
	if
		self.tool.Parent == self.character --gaurd check just to be sure the clients not screwing us over
		and self.Equipped
		and not self.Disabled
		--and not self.StatusManager:GetStatus().Stun
	then	
		local numb = self.firingMode
		local serverArgs = {
			["Module"] = script.Name,
			["Action"] = debug.info(1, "n"),
			["Index"] = numb,
		}

		self.firingMode = actionService:InvokeServer(serverArgs)
	end
end

--Fireing
function BASE_GUN:Fire()
	if
		self.tool.Parent == self.character --gaurd check just to be sure the clients not screwing us over
		and self.Equipped
		and not self.Disabled
		and os.clock() - self.timeLastFired >= BASE_GUN.timeBetweenShots --Debounce
		--and not self.StatusManager:GetStatus().Stun
	then	
		if self.bulletsLeft <= 0 then
			self:Reload(true)
			return
		end
		print(self.firingMode, self.timeLastFired, self.Disabled)
		self[self.firingMode.."Fire"](self)	
		
		return
	end
end

function BASE_GUN:_ShootBullet() -- Handles the anoying math that needs to be concualted to fire and subtract ammo
	if
		not self.Disabled
	then	
		if self.bulletsLeft <= 0 then -- Do we REALLY need reloading if we can just disable the BASE_GUN?
			--TODO play sound to signify there are no more rounds left
			self:Reload(true)
			return
		end
	
		local serverArgs = {
			["Module"] = script.Name,
			["Action"] = debug.info(3, "n"),
			["Tool"] = self.tool,
			["Hit"] = self.mouse.Hit.Position,
		}
		local callback = actionService:InvokeServer(serverArgs)
		--wait for callback

		self.bulletsLeft -= 1
		
		self.timeLastFired = os.clock()
		self.tool.Name = string.format("M9 [%i][%i] ", self.spareAmmo, self.bulletsLeft)
		
	end
end

function BASE_GUN:SemiFire()
	
	while isMouseButtonDown(Enum.UserInputType.MouseButton1 or Enum.UserInputType.Touch or Enum.KeyCode.ButtonR2) do
		if os.clock() - self.timeLastFired >= BASE_GUN.timeBetweenShots then
			self:_ShootBullet()
			game["Run Service"].Stepped:Wait()
		end
	end
	
end

--Reloading
function BASE_GUN:Reload(noBulletsLeft)
	if
		self.tool.Parent == self.character --gaurd check just to be sure the clients not screwing us over
		and self.Equipped
		and not self.Disabled
		and self.bulletsLeft ~= BASE_GUN.magazineCompacity
		and self.spareAmmo > 0
	then	
		self.Disabled = true
		self.tool.Name = string.format("M9 [%s] ", "reloading...")
		local serverArgs = {
			["Module"] = script.Name,
			["Action"] = debug.info(1, "n"),
			["ReloadTime"] = BASE_GUN.reloadTime,
			["Tool"] = self.tool
		}
		
		if noBulletsLeft then
			local callback = actionService:InvokeServer(
				{
					["Module"] = script.Name,
					["Action"] = "EjectCassing",
					["Tool"] = self.tool	
				})
		end
		local callback = actionService:InvokeServer(serverArgs)
		--wait for callback
		
		local result = math.min(BASE_GUN.magazineCompacity - self.bulletsLeft, self.spareAmmo)
		self.spareAmmo -= result
		self.bulletsLeft += result
		
		self.Disabled = false
		self.tool.Name = string.format("M9 [%i][%i] ", self.spareAmmo, self.bulletsLeft)
		return
	end
end

function BASE_GUN:ReloadEnd()
	return
end

--Fired
function BASE_GUN:FireEnd()
	return
end

function BASE_GUN:FiringModeEnd()
	return
end


--Tool Functionsv 
function BASE_GUN:Disable()
	self.Disabled = false
end

function BASE_GUN:Enable()
	self.Disabled = true
end

function BASE_GUN:Destroy()
	return pcall(function()
		--self.Janitor
		setmetatable(self, nil)
		table.clear(self)
	end)
end󠀁󠀁
󠀲󠀰󠀰󠀁
return table.freeze(BASE_GUN)





InheritExample
local ReplicatedFirst = game:GetService("ReplicatedFirst")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TweenService = game:GetService("TweenService")
local UserInputService = game:GetService("UserInputService")
local Janitor = require(ReplicatedStorage.Modules.Janitor)
local Auxillary = require(ReplicatedStorage.Modules.Auxillary.Auxillary)
local BetterRobloxAPI = require(ReplicatedStorage.Modules.BetterRobloxAPI)
local BASE_GUN = require(script.Parent)

local actionService = ReplicatedStorage.Remotes.ActionService
local replicateRemote = ReplicatedStorage.Remotes.Replicate

local assets =  ReplicatedFirst.Assets.M16
local VFXs = assets.VFX
local sound = assets.Sound
local casterParams = RaycastParams.new()

local function isMouseButtonDown(inputType)
	for _, button in UserInputService:GetMouseButtonsPressed() do
		if button.UserInputType == inputType then
			return true
		end
	end
	return false
end

local M16 = {}
setmetatable(M16, {__index = BASE_GUN})

----[{STATS}]----
M16.reloadTime = 1
M16.firingModes = {
	"Semi",
	"Burst",
}
M16.timeBetweenShots = 1
M16.magazineCompacity = 30-- Max amount of bullets in a mag
M16.maxAmmo = 200
----[[=====]]----
M16.Keys = {
	["Click"] = {["Name"] = "Fire", 
		["Inputs"] = {Enum.UserInputType.MouseButton1}, 
	},
	["R"] = {["Name"] = "Reload", 
		["Inputs"] = {Enum.KeyCode.R}, 
	},
	["C"] = {["Name"] = "Crouch",
		["Inputs"] = {Enum.KeyCode.C}, 
	},
	["Q"] = {["Name"] = "PeakLeft",
		["Inputs"] = {Enum.KeyCode.Q}, 
	},
	["E"] = {["Name"] = "PeakRight",
		["Inputs"] = {Enum.KeyCode.E}, 
	},
	["V"] = {["Name"] = "FiringMode",
		["Inputs"] = {Enum.KeyCode.V}, 
	},
}

function M16.new(User:Player, AuxObjects, tool:Tool)
	local self = setmetatable(BASE_GUN.new(User, AuxObjects, tool), {__index = M16})

	return self
end


function M16:BurstFire()
	while isMouseButtonDown(Enum.UserInputType.MouseButton1 or Enum.UserInputType.Touch or Enum.KeyCode.ButtonR2) do
		if os.clock() - self.timeLastFired >= M16.timeBetweenShots then
			self:_ShootBullet()
			game["Run Service"].Stepped:Wait()
			self:_ShootBullet()
			game["Run Service"].Stepped:Wait()
			self:_ShootBullet()
			game["Run Service"].Stepped:Wait()
		end
	end
end


return table.freeze(M16)





I think since __index is set to BASE_GUN it will check BASE_GUN before checking the sub-class’s index (which you changed)
To make the values “localized” to the created sub-class the values should be initialized in the .new function.

Similarly if you wanted a changeable function, you should initialize the function in the .new function. And if a BASE_GUN method wants to index any of these values, simply use the self variable.

For example:

function BASE_GUN:GetLookVector()
	return self.tool.Model.Barrel.CFrame.LookVector
end

^ here, it looks for “tool” in BASE_GUN first and then looks in the actual table.

2 Likes

I apolagize for not being active on this post, and the solution works fine thanks to you, but I am encountering a bug where when I fire a subclass wepon both auto and burst freeze when holding down. Yet this doesn’t occour when using the base wepon.

–Video comparing the basegun to the m16 subclass

– Baseclass (doesnt freeze)

function BASE_GUN:SemiFire()
	
	while isMouseButtonDown(Enum.UserInputType.MouseButton1 or Enum.UserInputType.Touch or Enum.KeyCode.ButtonR2) do
		if os.clock() - self.timeLastFired >= self.timeBetweenShots then
			self:_ShootBullet()

		end
	end
	
end

–M16 Subclass (freezes when being held)

function M16:BurstFire()
	while isMouseButtonDown(Enum.UserInputType.MouseButton1 or Enum.UserInputType.Touch or Enum.KeyCode.ButtonR2) do
		if os.clock() - self.timeLastFired >= self.timeBetweenShots then
			self:_ShootBullet()
			self:_ShootBullet()
			self:_ShootBullet()
		end
	end
end

^ this is equivalent to

isMouseButtonDown(Enum.UserInputType.MouseButton1)

did you mean to use commas?

With back-to-back calls, there might be a thread yield inside of that function (I don’t know since you didn’t send them). But a solution to that would be something like this:

task.spawn(self._ShootBullet, self)
task.spawn(self._ShootBullet, self)
self:_ShootBullet() -- i dont put this in a task.spawn since I want it to yield (you need a yield of some kind since it will be an unyielding infinite while loop otherwise)

or

task.delay(0.01, self._ShootBullet, self)
task.delay(0.03, self._ShootBullet, self)
self:_ShootBullet()

You can do it this way since

my_table.my_function = function(self)

end
-- is the same as
function my_table:my_function()

end
-- is also the same as
function my_table.my_function(self)

end

FYI you can do this with Roblox definited methods as well:
Player:LoadCharacter()

task.delay(1.0, Player.LoadCharacter, Player)