Roblox Lua Module Method `self` Referencing Issue : Calling Method on Module Instead of Instance?

Hey everyone,

I’m working on a Roblox module script that defines a pistol weapon with OOP-style methods. I have a constructor Pistol.new(character) that returns an instance table with metatable Pistol, and a method Pistol:Fired(player, mouseHit) that should act on that instance.

However, when I call Fired from my server script, the self inside the method unexpectedly points to the module table itself, not the instance object returned by new. This causes fields like self.Debounce or self.Handle to be nil or invalid, breaking the logic.

Here’s my module script :

local Pistol = {}
Pistol.__index = Pistol

local Players = game:GetService("Players")
local StarterPack = game:GetService("StarterPack")

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")

local sendEvent = ReplicatedStorage.Guns.Pistol.ShotClient
local viewEvent = ReplicatedStorage.Guns.Pistol.ViewEvent
local gunExplorer = StarterPack.Pistol

local TweenService = game:GetService("TweenService")


function Pistol.new(character)
	local gun = {
		Cooldown = script.Parent.Cooldown.Value,
		MainTick = 0,
		Handle = character.Pistol.Handle,
		Fire = character.Pistol.Handle.Fire,
		Position = nil,
		Sound = character.Pistol.Handle.GunSound,
		Debounce = false
	}
	
	gun.MainTick = 0
	
	-- Return the self object
	setmetatable(gun, Pistol)
	
	return gun
end

function Pistol:Fired(player: Player, mouseHit)
    local character = player.Character or player.CharacterAdded:Wait()
	print(self)
	if self.Debounce then return end
	if tick() - self.MainTick < self.Cooldown then return end
	self.Debounce = true
	self.MainTick = tick()

	-- shooting logic...
	print(self)
	task.delay(self.Cooldown, function()
		self.Debounce = false
	end)
----------------------------------------------------------------
    -- 1)  Build ray params and direction
    ----------------------------------------------------------------
    local origin     = self.Handle.Fire.Position
    local maxRange   = 300                    -- CHANGE RANGE HERE
    local direction  = (mouseHit - origin).Unit * maxRange

    local params = RaycastParams.new()
    params.FilterType               = Enum.RaycastFilterType.Exclude
	params.FilterDescendantsInstances = {character, self.Handle.Parent}
	print(self)
    ----------------------------------------------------------------
    -- 2)  Cast the ray
    ----------------------------------------------------------------
    local hit = workspace:Raycast(origin, direction, params)

    ----------------------------------------------------------------
    -- 3)  Decide the end‑point & distance
    ----------------------------------------------------------------
    local hitPos    = hit and hit.Position or (origin + direction)
    local distance  = (hitPos - origin).Magnitude

    ----------------------------------------------------------------
    -- 4)  Visual beam (always)
    ----------------------------------------------------------------
	local ray = ReplicatedStorage.Guns.LaserBeam:Clone()
	ray.Size = Vector3.new(0.05, 0.05, distance)
	ray.CFrame = CFrame.new(origin, hitPos)
	ray.EndAttachment.Position = Vector3.new(0, 0, -distance)
	ray.Material = Enum.Material.Neon
	ray.Color = Color3.fromRGB(158, 132, 53)
	ray.CanCollide = false
	ray.Anchored = true
	ray.Parent = workspace

	local tweenInfo = TweenInfo.new(0.1)
	local tween = TweenService:Create(ray.StartAttachment, tweenInfo, {
		Position = ray.EndAttachment.Position
	})
	tween:Play()
	
	game.Debris:AddItem(ray, 0.1)

    ----------------------------------------------------------------
    -- 5)  Muzzle‑flash & client recoil (always)
    ----------------------------------------------------------------
    local flash     = ReplicatedStorage.Guns.Pistol.PistolFlash:Clone()
    flash.CFrame    = self.Handle.Fire.CFrame * CFrame.new(0,0,-0.35)
    flash.Anchored  = false
    flash.CanCollide= false
    flash.CanQuery  = false
    local weld      = Instance.new("Weld")
    weld.Part0      = flash
    weld.Part1      = self.Handle.Fire
    weld.Parent     = flash
    flash.Parent    = self.Handle.Fire
    viewEvent:FireClient(player)              -- tell client to play recoil
	task.delay(0.1, function()
		self.Debounce = false
		flash:Destroy()
		
	end)
	print(flash)
    ----------------------------------------------------------------
    -- 6)  Play self sound (always)
    ----------------------------------------------------------------
    local sound     = self.Sound:Clone()
    sound.Parent    = self.Handle
    sound:Play()
    game.Debris:AddItem(sound, sound.TimeLength)

    ----------------------------------------------------------------
    -- 7)  Damage ONLY if we actually hit a humanoid
    ----------------------------------------------------------------
    if hit then
        local model = hit.Instance:FindFirstAncestorWhichIsA("Model")
        local hum   = model and model:FindFirstChildWhichIsA("Humanoid")
        if hum then hum.Health -= 10 end
    end

  end

return Pistol

And, in my server script i do :

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local shotEvent = ReplicatedStorage.Guns.Pistol.ShotClient
local Pistol = require(ReplicatedStorage.Guns.Pistol.PistolModule)

local pistols = {}

shotEvent.OnServerEvent:Connect(function(player, mouseHit)
	local userId = player.UserId
	local character = player.Character or player.CharacterAdded:Wait()

	if not pistols[userId] then
		pistols[userId] = Pistol.new(character)
	end

	pistols[userId]:Fired(player, mouseHit)
end)

But the print(self) inside Fired prints the entire module table, not the instance I created. I’m pretty sure I’m using the colon syntax correctly both in the method declaration and call, but something is off.

What I’ve checked:

  • The constructor sets the metatable properly.
  • The method is declared using colon function Pistol:Fired(...)
  • The method is called using colon pistolInstance:Fired(...)
  • Other fields in the instance are set properly.

What could cause self to be the module table and not the instance? Is there something about how I’m requiring or calling the module?

also, here are the prints for all the print statements :slight_smile: :

Print Statements

1st:

          {
                    ["Fired"] = "function",
                    ["__index"] = "*** cycle table reference detected ***",
                    ["new"] = "function"
                 }  -  Server - PistolModule:38

2nd:

 {
                    ["Cooldown"] = 0.35,
                    ["Debounce"] = true,
                    ["Fire"] = Fire,
                    ["Handle"] = Handle,
                    ["MainTick"] = 1751937976.483309,
                    ["Sound"] = GunSound
                 }  -  Server - PistolModule:45

3rd:

{
                    ["Cooldown"] = 0.35,
                    ["Debounce"] = true,
                    ["Fire"] = Fire,
                    ["Handle"] = Handle,
                    ["MainTick"] = 1751937976.483309,
                    ["Sound"] = GunSound
                 }  -  Server - PistolModule:59

Edit :

It always fires 2 times no matter what. The first one being the local gun table inside Pistol.new()
and the other one is just the Pistol table.

Is a class not just a table? Of course printing self is going to give you the data.

1 Like

Isn’t it supposed to give the gun table since the returned one is gun though? Since we’re storing the .new pistols in a table, retrieving them should give us the gun table and not Pistol???

It’s not just about “printing” self. When performing arithmetic on self, it just errors and prints attempt to perform arithmetic on number and nil since it’s trying to perform in on Pistol I guess.

Are these seperate times that are printing?

Because you do have a debounce, and the reason that one is firing with the actual class and the other isn’t is probably because the debounce prevents it from continuing when fired incorrectly (somewhere, I would try to find it)

If it works fine, I don’t see the issue. In my mind, this is one of those programming black-magic things that you don’t question and just ignore.

Also, just check if self ~= Pistol. Or, you can add more nil-checks. If something is failing because an instance is nil, just check to see if it isn’t nil.

It doesn’t really work fine unfortunately, I’ve replaced self with gunnow, it works when in singleplayer but if you’re in team testing or whatever, for some reason the damage and everything (including rays) just doubles or plays two times.

new module script :

module script
local Pistol = {}
Pistol.__index = Pistol

local Players = game:GetService("Players")
local StarterPack = game:GetService("StarterPack")

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")

local sendEvent = ReplicatedStorage.Guns.Pistol.ShotClient
local viewEvent = ReplicatedStorage.Guns.Pistol.ViewEvent
local gunExplorer = StarterPack.Pistol

local TweenService = game:GetService("TweenService")


function Pistol.new(character)
	local gun = {
		Cooldown = script.Parent.Cooldown.Value,
		MainTick = nil,
		Handle = character.Pistol.Handle,
		Fire = character.Pistol.Handle.Fire,
		Position = nil,
		Sound = character.Pistol.Handle.GunSound,
		Debounce = false
	}
	
	gun.MainTick = 0
	
	-- Return the gun object
	setmetatable(gun, Pistol)

	return gun
end

function Pistol:Fired(player: Player, gun, mouseHit)
    local character = player.Character or player.CharacterAdded:Wait()

    -- cooldown
    if tick() - gun.MainTick < gun.Cooldown then return end
    gun.MainTick = tick()

   if not gun.Debounce then
	
		gun.Debounce = true
----------------------------------------------------------------
    -- 1)  Build ray params and direction
    ----------------------------------------------------------------
    local origin     = gun.Handle.Fire.Position
    local maxRange   = 300                    -- CHANGE RANGE HERE
    local direction  = (mouseHit - origin).Unit * maxRange

    local params = RaycastParams.new()
    params.FilterType               = Enum.RaycastFilterType.Exclude
    params.FilterDescendantsInstances = {character, gun}

    ----------------------------------------------------------------
    -- 2)  Cast the ray
    ----------------------------------------------------------------
    local hit = workspace:Raycast(origin, direction, params)

    ----------------------------------------------------------------
    -- 3)  Decide the end‑point & distance
    ----------------------------------------------------------------
    local hitPos    = hit and hit.Position or (origin + direction)
    local distance  = (hitPos - origin).Magnitude

    ----------------------------------------------------------------
    -- 4)  Visual beam (always)
    ----------------------------------------------------------------
	local ray = ReplicatedStorage.Guns.LaserBeam:Clone()
	ray.Size = Vector3.new(0.05, 0.05, distance)
	ray.CFrame = CFrame.new(origin, hitPos)
	ray.EndAttachment.Position = Vector3.new(0, 0, -distance)
	ray.Material = Enum.Material.Neon
	ray.Color = Color3.fromRGB(158, 132, 53)
	ray.CanCollide = false
	ray.Anchored = true
	ray.Parent = workspace

	local tweenInfo = TweenInfo.new(0.1)
	local tween = TweenService:Create(ray.StartAttachment, tweenInfo, {
		Position = ray.EndAttachment.Position
	})
	tween:Play()

	game.Debris:AddItem(ray, 0.1)

    ----------------------------------------------------------------
    -- 5)  Muzzle‑flash & client recoil (always)
    ----------------------------------------------------------------
    local flash     = ReplicatedStorage.Guns.Pistol.PistolFlash:Clone()
    flash.CFrame    = gun.Handle.Fire.CFrame * CFrame.new(0,0,-0.35)
    flash.Anchored  = false
    flash.CanCollide= false
    flash.CanQuery  = false
    local weld      = Instance.new("Weld")
    weld.Part0      = flash
    weld.Part1      = gun.Handle.Fire
    weld.Parent     = flash
    flash.Parent    = gun.Handle.Fire
    viewEvent:FireClient(player)              -- tell client to play recoil
	task.delay(0.1, function()
		gun.Debounce = false
		flash:Destroy()
		
	end)

    ----------------------------------------------------------------
    -- 6)  Play gun sound (always)
    ----------------------------------------------------------------
    local sound     = gun.Sound:Clone()
    sound.Parent    = gun.Handle
    sound:Play()
    game.Debris:AddItem(sound, sound.TimeLength)

    ----------------------------------------------------------------
    -- 7)  Damage ONLY if we actually hit a humanoid
    ----------------------------------------------------------------
    if hit then
        local model = hit.Instance:FindFirstAncestorWhichIsA("Model")
        local hum   = model and model:FindFirstChildWhichIsA("Humanoid")
        if hum then hum.Health -= 10 end
    end
	end	
  end

return Pistol

new server script :

server script
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local StarterPack = game:GetService("StarterPack")


local tool = script.Parent


local shotEvent = ReplicatedStorage.Guns.Pistol.ShotClient
local viewEvent = ReplicatedStorage.Guns.Pistol.ViewEvent

local equippedEvent = ReplicatedStorage.Guns.Pistol.ShotClient
local Pistol = require(ReplicatedStorage.Guns.Pistol.PistolModule)

local pistoledPlayers = {}

shotEvent.OnServerEvent:Connect(function(player, mouseHit)
	
	
	local character = player.Character or player.CharacterAdded:Wait()
	local mouse = player:GetMouse()
	
	local userId = player.UserId
	
	
	if not pistoledPlayers[userId] then
		pistoledPlayers[userId] = Pistol.new(character)
	elseif pistoledPlayers[userId] then	
		pistoledPlayers[userId]:Fired(player, pistoledPlayers[userId], mouseHit)
	end
	
	
end)

It’s basically the same but it has gun rather than self. And it has a major problem like I’ve stated: the script plays are scaled according to the player count for some reason : if there’s one player then it plays once; if two players are in, it plays twice and so on. The main problem here’s gotta be in the server script but i honestly couldn’t find it.

Can you please elaborate on how and why it scales with player count? Also, please consider what I said above about doing nil-checks for the various instances that are erroring, if that’s still an issue.

Well, I’ve actually fixed the scaling player count issue somehow but the script still plays twice no matter what.

Also, the nil checks aren’t needed now i think since I haven’t really come across an error yet with this setup rather than this playing twice thing.

btw, here’s my shortened local script :slight_smile: :

local Cooldown = 0.35

local mouse = player:GetMouse()

local mainTick = 0

tool.Activated:Connect(function()
	
	if tick() - mainTick > Cooldown then
		
		mainTick = tick()
		shotEvent:FireServer(mouse.Hit.Position)
		
		end
end)

1 Like
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local StarterPack = game:GetService("StarterPack")

local tool = script.Parent

local shotEvent = ReplicatedStorage.Guns.Pistol.ShotClient
local viewEvent = ReplicatedStorage.Guns.Pistol.ViewEvent

local equippedEvent = ReplicatedStorage.Guns.Pistol.ShotClient
local Pistol = require(ReplicatedStorage.Guns.Pistol.PistolModule)

local pistoledPlayers = {}

function Get(Player: Player)
    local Found = pistoledPlayers[userId]

   if Found and Found.Cooldown then -- extra nil checks idk
       return Found
    else
       local Class = Pistol.new(character)

       pistoledPlayers[userId] = Class
       return Class
    end
end

shotEvent.OnServerEvent:Connect(function(player, mouseHit)
	local character = player.Character or player.CharacterAdded:Wait()
	local mouse = player:GetMouse()
	
	local Found = Get(player)
	
    if Found and Found.Cooldown then -- extra nil checks idk
       pistoledPlayers[userId]:Fired(player, pistoledPlayers[userId], mouseHit)
    else
       pistoledPlayers[userId] = Pistol.new(character)
    end
end)

Try this

Nope, still the same outcome. Thanks anyway :slight_smile:

Oh wait, I think i found something…

Yeah, there was another pistol tool in the workspace which interfered with the module scripts, my bad for wasting your time :roll_eyes:.

2 Likes

:eyes: :eyes: :eyes: :eyes:

it happens

2 Likes