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 :
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.