2 Modules using OOP for a Shooter. Pretty messy, there is probably a better way

Hi! Right now, I am developing a third person Shooter game.
This is my first time using OOP and my first time scripting a “gun system”.

The 2 modules are:
GunTools:
A module intended for functions like equipping guns, unequipping guns, and maybe more.
Right now it has the local function getPlayerGunJoint and the returned module has the function equipGun.

baseGun:
A class, all guns will inherit. Right now, it has a constructor and a Reload function. I am quite unsure if I should play the reload animation in that function, so I would like to also hear about that.

The modules are in the ShooterStorage folder, in the ServerStorage.

Why in the ShooterStorage folder?

I am using packages to keep the Scripts synced between all my places.

Here’s the file:
ShooterReviewModules.rbxl (18,5 KB)

Thank you in advance for testing. :slight_smile: - Sonnenroboter

3 Likes

Could you post the two scripts you want reviewed to this post?
Make sure to format them with ```s on the either end of the script,

resulting in your code --being formatted and colour-coded 

Ta

I really don’t understand your need for that, since they are in the file I uploaded, but here you go:
ServerStorage.ShooterStorage.Modules.GunTools:

--[[ GunTools A module with functions for guns like welding to the character, etc. Found at: ServerStorage.ShooterStorage.Modules.GunTools --]] local gunTools = {} local ServerStorage = game:GetService("ServerStorage") local Storage = ServerStorage:WaitForChild("ShooterStorage") local Modules = Storage:WaitForChild("Modules") local animTools = require(Modules:FindFirstChild("AnimationTools")) --// This function finds, creates one if it isn't yet and returns the Motor6D used for welding the guns. local function getPlayerGunJoint(plr) local character = plr.Character if not character then return end local gunJoint = character:FindFirstChild("gunJoint") if not gunJoint then gunJoint = Instance.new("Motor6D") gunJoint.Part0 = character:FindFirstChild("RightHand") end return gunJoint end function gunTools.equipGun(plr, gun) local gunJoint = getPlayerGunJoint(plr) if not gunJoint then return end gun.Player = plr gunJoint.Part1 = gun.Handle animTools.playAnimation(plr, gun.HoldAnim) end return gunTools 

ServerStorage.ShooterStorage.Weapons.Classes.baseGun

--[[ baseGun The class, every gun inherits from. --]] local ServerStorage = game:GetService("ServerStorage") local Storage = ServerStorage:WaitForChild("ShooterStorage") local Modules = Storage:WaitForChild("Modules") local animTools = require(Modules:FindFirstChild("AnimationTools")) local baseGun = {} baseGun.__index = baseGun function baseGun.new(name, maxAmmo, model, reloadTime, holdAnim, reloadAnim, gunHandle) local newGun = {} setmetatable(newGun, baseGun) newGun.Name = name newGun.MaxAmmo = maxAmmo newGun.Model = model newGun.Ammo = maxAmmo newGun.ReloadTime = reloadTime newGun.Reloading = false newGun.HoldAnim = holdAnim newGun.ReloadAnim = reloadAnim newGun.Player = nil newGun.Handle = gunHandle return newGun end function baseGun:Reload() self.Reloading = true if(self.Player) then animTools.playAnimation(self.Player, self.ReloadAnim) end wait(self.ReloadTime) self.Ammo = self.MaxAmmo self.Reloading = false end return baseGun 
5 Likes

I made a similar post a few days ago (except I had twin gun objects, one on server, and the other on client)

I’m unsure if we are both right, or both wrong with this approach, so hopefully someone with a greater understanding of the subject can help both of us

and since this is code review I do have a tip that can make your code a tad bit neater, you’re using a ton of arguments in the constructor, I recommend having a table with the information for all guns, and pass the table as a argument

2 Likes

Thanks for the tip. Do you mean something like this?

function baseGun.new(args) local newGun = {} newGun.Name = args.Name newGun.MaxAmmo = args.MaxAmmo --etc return newGun end 
4 Likes

I can say there is in fact a better way, called be lazy and use Roblox’s default tools and some remotes. However this doesn’t work for all games. In my game for example: Project Blu Development - Roblox I’m using completely custom characters, and the player doesn’t have a character under their Player.Character property. I’m not even using humanoids so I couldn’t even activate the default tool object and had to create my own object, which ran into a ton of problems on the server and on other clients because of replication issues.

The very first issue I had, was source code control. You just have to suck it up and put tool data where it can be stolen, otherwise its not going to be a pleasant scripting experience for you.

After that I had the issue of wanting to have a Tool Model on the Server and Client, that was a real hassle to work with, sometimes one or the other wouldn’t get welded and would fall to the deletion plane and the game broke. Sometimes things didn’t replicate properly. Tried doing server only, same problem. Eventually found out all I really needed was having the tool model on the client only, not the server, which is really all you need.

Third, and biggest problem, was telling the server (and other clients) what Player1 was doing. For simplicity I just replicated every thing the player did, and if was something important event like Equipping, Uneqipping, Activating, Deactivating the tool, everyone ran their tools function for that specific event. This resulted in some really good behaviors that nullified a lot of replication issues I was having.

Here’s some code from my game, I don’t expect you to full understand it as parts of the code is missing, but the concept should be clear enough to get you a general idea.

ToolClass (The base object)
--------------------- -- Roblox Services -- --------------------- local ReplicatedStorage = game:GetService("ReplicatedStorage") local RunService = game:GetService("RunService") local Players = game:GetService("Players") local LocalPlayer = Players.LocalPlayer ------------ -- Reverb -- ------------ local Reverb = require(ReplicatedStorage:WaitForChild("Reverb")) local Utilities = require(Reverb.Utility) ------------- -- Network -- ------------- local ToolEvent = script:WaitForChild("ToolEvent") ------------- -- ------------- local Cache = {} local DefaultTransitionTime = 0.1 -------------------------------- -- Class Functions, Variables -- -------------------------------- local Class = {} Class.__index = Class local function CreateEvent(Tool, EventName)	local Event = Utilities.NewEvent(EventName)	Event:Connect(function(...)	if RunService:IsClient() and Players[Tool.Owner.Name] == LocalPlayer then	ToolEvent:FireServer(Tool.ID, EventName, ...)	elseif RunService:IsServer() then	ToolEvent:FireAllClients(Tool.ID, EventName, ...)	end	end)	return Event end function Class.new(ID, Owner, ToolModel, ToolGrip, AnimationList)	local Tool = setmetatable({	Name = (ToolModel and ToolModel.Name) or "Tool_" .. tostring(ID),	ID = ID, -- To keep track of Tools	Owner = Owner, -- What entity (Character/Rig) owns the model.	Handle = ToolModel:FindFirstChild("Handle") or ToolModel.PrimaryPart,	ToolModel = ToolModel, -- ToolModel	ToolGrip = ToolGrip, -- ToolGrip (Motor6D/Weld)	AnimationList = AnimationList or {}, -- The animations the tool has access to.	IsEquipped = false,	InputDirection = Vector3.new(),	}, Class)	-- Fired when the Tool is equipped.	Tool.Equipped = CreateEvent(Tool, "Equipped")	-- Fired when the Tool is unequipped.	Tool.Unequipped = CreateEvent(Tool, "Unequipped")	-- Fired when Tool.Active = true	Tool.Activated = CreateEvent(Tool, "Activated")	-- Fired when Tool.Active = false	Tool.Deactivated = CreateEvent(Tool, "Deactivated")	-- Fired when the Tool is dropped.	Tool.Dropped = CreateEvent(Tool, "Dropped")	-- To allow for custom events to be replicated.	Tool.CustomEvent = Utilities.NewEvent("CustomEvent")	Tool.CustomEvent:Connect(function(EventName, DataToSend, LocalEvent)	if Tool[EventName] == nil then warn("Warning: There is no method called " .. tostring(EventName) .. "for " .. Tool.Name) return end	if LocalEvent then return end	if RunService:IsClient() and Players[Tool.Owner.Name] == LocalPlayer then	ToolEvent:FireServer(Tool.ID, "CustomEvent", {EventName, DataToSend})	elseif RunService:IsServer() then	ToolEvent:FireAllClients(Tool.ID, "CustomEvent", {EventName, DataToSend})	end	end)	Tool.Destroyed = Utilities.NewEvent("Tool.Destroyed")	Cache[ID] = Tool -- Add the Tool to the Cache for easy tracking.	-- Keeps things clean in the event the ToolModel's parent is deleted, nice little auto clean up. ^w^	if ToolModel then	ToolModel:GetPropertyChangedSignal("Parent"):Connect(function()	if ToolModel.Parent == nil then	Tool:Destroy()	end	end)	end	return Tool end function Class:Equip(NewOwner)	if NewOwner == nil then warn("Warning: cannot equip tool to a nil owner.") return end	if NewOwner == self.Owner then return end	-- Unload animations first.	if self.Owner then	self:UnloadAnimations()	end	-- Set ToolGrip's Part1 next.	if self.ToolModel and self.ToolGrip then	self.ToolGrip.Part1 = NewOwner.Model.HumanoidRootPart	self.ToolModel.PrimaryPart.Anchored = false	end	self.Owner = NewOwner -- Set owner next.	self:LoadAnimations() -- Then load animations.	self.Equipped:Fire(NewOwner.UID)	self.IsEquipped = true --	warn(self.Owner.Name .. " has equipped " .. self.Name) end function Class:Unequip()	self.Unequipped:Fire()	self:Deactivate()	self.IsEquipped = false --	warn(self.Owner.Name .. " has unequipped " .. self.Name) end function Class:Drop()	self:Deactivate()	self.IsEquipped = false	self:UnloadAnimations()	-- Set ToolGrip's Part1 to nil since there is no Owner.	if self.ToolModel then	if self.ToolGrip then	self.ToolGrip.Part1 = nil	end	self.ToolModel.PrimaryPart.Anchored = true	local _, Position, Normal = Utilities.RaycastWithWhiteList(self.ToolModel.PrimaryPart.Position, Vector3.new(0, -100, 0), {game.Workspace.Map})	Position = Position + Vector3.new(0, 1, 0)	local CF = CFrame.new(Position, Position + Normal) * CFrame.Angles(math.rad(-0), 0, 0)	self.ToolModel:SetPrimaryPartCFrame(CF)	if RunService:IsServer() then	self.ToolModel.Parent = game.Workspace.RayIgnore.ToolModels	end	end	self.Dropped:Fire(self.Owner)	self.Owner = nil end -------------------- -- Input Handling -- -------------------- function Class:Activate()	if self.Owner == nil or not self.IsEquipped then return end	self.Active = true	self.Activated:Fire() --	warn(self.Owner.Name .. " has activated " .. self.Name) end function Class:Deactivate()	self.Active = false	self.Deactivated:Fire() --	warn(self.Owner.Name .. " has deactivated " .. self.Name) end function Class:InputBegan(Input)	if self.Owner == nil or not self.IsEquipped then return end --	warn(" [" .. self.Owner.Name .. "].InputBegan = " .. tostring(Input)) end function Class:InputEnded(Input)	if self.Owner == nil or not self.IsEquipped then return end --	warn(" [" .. self.Owner.Name .. "].InputBegan = " .. tostring(Input)) end ------------------------ -- Animation Handling -- ------------------------ function Class:LoadAnimations()	if self.Owner == nil then return end	local AnimationController = self.Owner.AnimationController	if AnimationController == nil then return end	self.AnimationController = AnimationController	if self.Animations == nil then --	warn("Loading animations for", self.Name, ".")	local Animations = {}	for Index, Track in pairs(self.AnimationList) do	local Tracks = {}	for TrackIndex, TrackData in pairs(Track) do	local AnimationObject = self.ToolModel:FindFirstChild(Index.."_"..tostring(TrackIndex))	if not AnimationObject then	AnimationObject = Instance.new("Animation")	AnimationObject.AnimationId = "rbxassetid://" .. TrackData.Id	AnimationObject.Name = Index.."_"..tostring(TrackIndex)	AnimationObject.Parent = self.ToolModel	end	Tracks[TrackIndex] = {	Track = AnimationController:LoadAnimation(AnimationObject),	Weight = TrackData.Weight,	AnimationObject = AnimationObject,	}	end	Animations[Index] = {	Tracks = Tracks,	ActiveTrack = nil	}	end	self.Animations = Animations	else	local AnimationController = self.Owner.AnimationController	for Index, Data in pairs(self.Animations) do	for TrackIndex = 1, #Data do	local TrackData = Data[TrackIndex]	if TrackData.Track then	TrackData.Track:Destroy()	end	TrackData.Track = AnimationController:LoadAnimation(TrackData.AnimationObject)	end	end	end end function Class:UnloadAnimations()	if self.Animations then	for Index, Data in pairs(self.Animations) do	for TrackIndex = 1, #Data.Tracks do	local TrackData = Data.Tracks[TrackIndex]	if TrackData.Track then	TrackData.Track:Stop()	TrackData.Track:Destroy()	end	end	end	self.AnimationController = nil	end end local function RollTrack(Tracks)	return Tracks[math.random(1, #Tracks)] end function Class:PlayAnimation(AnimationName, IsLooped)	if self.Owner == nil then return end	local AnimationData = self.Animations[AnimationName]	if AnimationData then	local Data = RollTrack(AnimationData.Tracks)	AnimationData.ActiveTrack = Data.Track	AnimationData.Looped = IsLooped or false	AnimationData.ActiveTrack:Play(nil, Data.Weight)	if RunService:IsClient() and Players[self.Owner.Name] == LocalPlayer then	ToolEvent:FireServer(self.ID, "PlayAnimation", AnimationName)	elseif RunService:IsServer() then	ToolEvent:FireAllClients(self.ID, "PlayAnimation", AnimationName)	end	return AnimationData.ActiveTrack	else	warn("Warning: " .. self.Name .. ".Animations[" .. tostring(AnimationName) .. "] is missing or nil.")	end end function Class:AdjustAnimation(Rig, AnimationName, AnimationLength, Weight, TransitionTime)	if self.Owner == nil then return end	local AnimationData = self.Animations[AnimationName]	if AnimationData and AnimationData.ActiveTrack then	local Track = AnimationData.ActiveTrack	TransitionTime = TransitionTime or DefaultTransitionTime	Track:AdjustSpeed( (AnimationLength and Track.Length ~= 0) and Track.Length/AnimationLength or nil )	Track:AdjustWeight(Weight, TransitionTime)	if RunService:IsClient() and Players[self.Owner.Name] == LocalPlayer then	ToolEvent:FireServer(self.ID, "AdjustAnimation", AnimationName, AnimationLength, Weight, TransitionTime)	elseif RunService:IsServer() then	ToolEvent:FireAllClients(self.ID, "AdjustAnimation", AnimationName, AnimationLength, Weight, TransitionTime)	end	end end function Class:StopAnimation(AnimationName)	if self.Owner == nil then return end	local AnimationData = self.Animations[AnimationName]	if AnimationData and AnimationData.ActiveTrack then	AnimationData.ActiveTrack:Stop()	if RunService:IsClient() and Players[self.Owner.Name] == LocalPlayer then	ToolEvent:FireServer(self.ID, "StopAnimation", AnimationName)	elseif RunService:IsServer() then	ToolEvent:FireAllClients(self.ID, "StopAnimation", AnimationName)	end	else	warn("Warning: " .. self.Name .. ".Animations[" .. tostring(AnimationName) .. "] is missing or nil.")	end end function Class:StopAllAnimations()	for Index, AnimationData in pairs(self.Animations) do	if AnimationData and AnimationData.ActiveTrack then	AnimationData.ActiveTrack:Stop()	end	end	if RunService:IsClient() and Players[self.Owner.Name] == LocalPlayer then	ToolEvent:FireServer(self.ID, "StopAllAnimations")	elseif RunService:IsServer() then	ToolEvent:FireAllClients(self.ID, "StopAllAnimations")	end end ----------------------- -- Clean up ------------------------ function Class:Destroy()	Cache[self.ID] = nil	if self.ToolModel then	self.ToolModel:Destroy()	self:UnloadAnimations()	end	self.Destroyed:Fire() end ----------------------- -- Replication Logic -- ----------------------- if RunService:IsServer() then	ToolEvent.OnServerEvent:Connect(function(Player, ...)	for _, OtherPlayer in pairs(Players:GetPlayers()) do	if OtherPlayer ~= Player then	ToolEvent:FireClient(OtherPlayer, ...)	end	end	end) else	ToolEvent.OnClientEvent:Connect(function(ID, EventName, Data)	local Tool = Cache[ID]	if Tool then	if EventName == "Equipped" then	Tool:Equip(Data)	elseif EventName == "Unequipped" then	Tool:Unequip(Data)	elseif EventName == "PlayAnimation" then	Tool:PlayAnimation(Data)	elseif EventName == "StopAnimation" then	Tool:StopAnimation(Data)	elseif EventName == "StopAllAnimations" then	Tool:StopAllAnimations()	elseif EventName == "Dropped" then	Tool:Drop()	elseif EventName == "CustomEvent" then	Tool[Data[1]](Tool, (Data[2] and typeof(Data[2]) == "table" and #Data[2] > 1 and unpack(Data[2])) or Data[2])	end	else	warn("Cache[".. tostring(ID) .."] is missing or nil.")	end	end) end return Class 
RangedWeaponClass (inherited from ToolClass)
--------------------- -- Roblox Services -- --------------------- local RunService = game:GetService("RunService") local ReplicatedStorage = game:GetService("ReplicatedStorage") local SoundService = game:GetService("SoundService") -------------------- -- Reverb Modules -- -------------------- local Reverb = require(ReplicatedStorage:WaitForChild("Reverb")) local Utilities = require(Reverb.Utility) local Core = require(ReplicatedStorage:WaitForChild("Core")) local WorldData = require(Core.WorldData) -------------------- -- Networking -------------------- local IsServer = RunService:IsServer() local Replicate = script:WaitForChild("Replicate") local function ReplicateShot(Tool, Shots)	-- figure out why the spread isn't theree.	Tool.GearManagerInterface.CreateProjectile(Tool, Shots, Tool:GetIgnoreList())	if not IsServer then	Replicate:FireServer(Tool.ID, "ShotFired", 1)	else	Replicate:FireAllClients(Tool.ID, "ShotsFired", 1)	end end ------------------------ -- Local Cache ------------------------ local Cache = {} ------------------------ -- Class Construction -- ------------------------ local ToolClass = require(script.Parent.ToolClass) local Class = {} Class.__index = Class setmetatable(Class, ToolClass) ----------------------------- -- Generic Class Functions -- ----------------------------- function Class.new(ID, Owner, ToolModel, ToolGrip)	local Template = require(ToolModel:WaitForChild("ToolData"))	local Tool = setmetatable(ToolClass.new(ID, Owner, ToolModel, ToolGrip, Template.AnimationList), Class)	-- Hacky, but works.	local F = {Equip = true, Unequip = true, Activate = true, Deactivate = true, Update = true, InputBegan = true, InputEnded = true}	for Index, Value in pairs(Template) do	if F[Index] then	Tool[Index] = function(self, ...)	Class[Index](self, ...)	Value(self, ...)	end	else	Tool[Index] = Value	end	end	if ToolModel then	local Handle = ToolModel.Handle	if Tool.HasLaser then	local Attachment0 = Utilities.Create("Attachment", {Name = "LaserOrigin", Parent = Handle})	local Attachment1 = Utilities.Create("Attachment", {Name = "LaserEndPoint", Parent = Handle})	local BeamObject = Utilities.Create("Beam", {	Name = "LaserAttachment",	FaceCamera = true,	Color = ColorSequence.new(Color3.fromRGB(255, 0, 0), Color3.fromRGB(255, 0, 0)),	LightEmission = 1,	Transparency = NumberSequence.new(0.2),	Attachment0 = Attachment0,	Attachment1 = Attachment1,	Width0 = 0.05,	Width1 = 0.05,	Enabled = false,	Parent = Handle,	})	Tool.Laser = {Attachment0 = Attachment0, Attachment1 = Attachment1, Beam = BeamObject}	end	if Tool.HasFlashLight then	local FlashlightAttachment = Utilities.Create("Attachment", {Name = "FlashlightAttachment", Parent = Handle})	local SpotLight = Utilities.Create("SpotLight", {	Name = "FlashLight",	Angle = 45,	Brightness = 5,	Range = 45,	Color = Color3.fromRGB(255, 249, 225),	Shadows = true,	Enabled = false,	Parent = FlashlightAttachment	})	Tool.Flashlight = {Attachment0 = FlashlightAttachment, Light = SpotLight}	end	end	if Owner then	Tool:LoadAnimations()	end	Tool:LoadSounds()	Tool.UserInterface = nil	Tool.LastAttack = 0	Cache[ID] = Tool	return Tool end function Class:Equip(NewOwner, UserInterface, camShake)	ToolClass.Equip(self, NewOwner)	if self.Laser then	self.Laser.Beam.Enabled = true	self.LaserIgnoreList = self:GetIgnoreList()	end	if self.Flashlight then	self.Flashlight.Light.Enabled = true	end	if UserInterface then	UserInterface.GetElement("WeaponHud").Toggle(true)	UserInterface.GetElement("WeaponHud").UpdateWeaponIcon(self)	self.UserInterface = UserInterface	self:UpdateAmmoCounter()	end	self.camShake = camShake end function Class:Unequip()	ToolClass.Unequip(self)	if self.Laser then	self.Laser.Beam.Enabled = false	self.LaserIgnoreList = nil	end	if self.Flashlight then	self.Flashlight.Light.Enabled = false	end end function Class:Destroy()	ToolClass.Destroy(self)	if self.Laser then	self.Laser.Attachment0:Destroy()	self.Laser.Attachment1:Destroy()	self.Laser.Beam:Destroy()	end	if self.Flashlight then	self.Flashlight.Attachment0:Destroy()	self.Flashlight.Light:Destroy()	end	Cache[self.ID] = nil end -------------------- -- Audio Handling -- -------------------- function Class:LoadSounds()	if self.ToolModel then	local Sounds = {}	local Objects = self.ToolModel:GetDescendants()	for Index = 1, #Objects do	local Object = Objects[Index]	if Object:IsA("Sound") then	Sounds[Object.Name] = Object	Object.SoundGroup = SoundService.Tools	end	end	self.Sounds = Sounds	self.ActiveSounds = {}	end end function Class:PlaySound(SoundName)	if self.Sounds then	if self.Sounds[SoundName] then	self.CustomEvent:Fire("PlaySound", SoundName)	local Sound = self.Sounds[SoundName]:Clone()	Sound.SoundGroup = self.Sounds[SoundName].SoundGroup	Sound.Pitch = Sound.Pitch + math.random(-100,100)/1000	Sound.PlayOnRemove = false	Sound.Parent = self.Handle	Sound:Play()	coroutine.resume(coroutine.create(function()	Sound.Ended:Wait()	Sound:Destroy()	end))	return Sound	else	warn(self.Name.. ".Sounds[" .. tostring(SoundName) .. "] is missing or nil.")	end	end end -------------------------------------------- -- Ranged Weapon Specific Class Functions -- -------------------------------------------- function Class:GetIgnoreList(Extras)	local CharactersFolder = game.Workspace.Characters	local IgnoreList = {game.Workspace.RayIgnore}	local LocalMachineCharacter	local OtherMachineCharacterFolder	if IsServer then	OtherMachineCharacterFolder = CharactersFolder.Client	LocalMachineCharacter = CharactersFolder.Server:FindFirstChild(self.Owner.Name)	else	OtherMachineCharacterFolder = CharactersFolder.Server	LocalMachineCharacter = CharactersFolder.Client:FindFirstChild(self.Owner.Name)	end	if LocalMachineCharacter then	IgnoreList[#IgnoreList+1] = LocalMachineCharacter	end	if OtherMachineCharacterFolder then	IgnoreList[#IgnoreList+1] = OtherMachineCharacterFolder	end	if Extras then	for _, Value in pairs(Extras) do	IgnoreList[#IgnoreList+1] = Value	end	end	return IgnoreList end function Class:UpdateAmmoCounter()	if self.UserInterface then	self.UserInterface.GetElement("WeaponHud").UpdateAmmo(self)	end end function Class:Restock(Amount)	self.TotalAmmo = math.min(self.TotalAmmo + Amount, self.MaxAmmo)	if IsServer then	Replicate:FireAllClients(self.ID, "AmmoPickup", Amount)	else	self:UpdateAmmoCounter()	end end function Class:Reload()	if self.TotalAmmo == "inf" or self.TotalAmmo == 0 then return end	print("Reloading")	self.IsReloading = true	local AmountToReload = math.min(self.MagSize, self.TotalAmmo)	while self.TotalAmmo >= 1 and self.Ammo < AmountToReload and self.IsReloading do	self:PlaySound("ReloadSound")	local Track = self:PlayAnimation("Reload")	Track.Stopped:Wait()	if not self.IsReloading then	return	end	if self.IsShotgun then	self.Ammo = self.Ammo+1	print(Reverb.RunMode, "TotalAmmo:", self.TotalAmmo)	else	self.Ammo = AmountToReload	end	self:UpdateAmmoCounter()	end	self.IsReloading = false	print("Reloading finished") end function Class:HandleRecoil()	if self.camShake then	local Recoil = self.Recoil	self.camShake:ShakeOnce(	Recoil.Magnitude,	Recoil.Roughness,	Recoil.FadeIn,	Recoil.FadeOut	)	end end function Class:EjectShell(HumanoidRootPart)	if RunService:IsServer() or self.ToolModel == nil or self.ShellCasingType == nil then return end	local Handle = self.Handle --	ParticleManager.CreateShellCasing( --	self.ShellCasingType, -- ShellCasingType --	Handle.ShellEmitter.WorldCFrame, -- CF --	Handle.CFrame.RightVector * 7, -- Velocity --	Vector3.new(math.random() * 90, 0, 0) -- RotVelocity --	)	local Ids = {	953031970,	961361674,	961369740,	}	local Sound = Instance.new("Sound")	Sound.SoundId = "rbxassetid://"..Ids[math.random(1, #Ids)]	Sound.Volume = 0.8	Sound.PlaybackSpeed = 0.8	Sound.PlayOnRemove = true	Sound.EmitterSize = 30	Sound.Parent = HumanoidRootPart	Sound:Destroy() end function Class:ShotgunBlast(HumanoidRootPart, Direction)	local Shots = {}	for Pellet_Index = 1, self.NumberOfPellets do	local Spread = -(self.Spread.Max/2) + (Pellet_Index/self.NumberOfPellets) * self.Spread.Max	Direction = (CFrame.new(HumanoidRootPart.Position, HumanoidRootPart.Position + Direction) * CFrame.Angles(0, math.rad(Spread), 0)).LookVector	Shots[#Shots+1] = {HumanoidRootPart.Position, Direction}	end	return Shots end function Class:SingleShot(HumanoidRootPart, Direction)	local Shots = {}	local N = math.random(-1, 1)	if N == 0 then N = 1 end	self.Spread.Current = math.min(self.Spread.Current + self.Spread.Step, 1)	local Spread = math.random() * (self.Spread.Max * self.Spread.Current)	Direction = (CFrame.new(HumanoidRootPart.Position, HumanoidRootPart.Position + Direction) * CFrame.Angles(0, math.rad(Spread * N), 0)).LookVector	Shots[#Shots+1] = {HumanoidRootPart.Position, Direction}	return Shots end function Class:Attack()	if self.IsAttacking then return end	if self and self.Owner.Model.Parent then	if self.Ammo < 1 then	return	end	if not self.IsShotgun and self.IsReloading then	return	else	self.IsReloading = false	end	local HumanoidRootPart = self.Owner.Model:FindFirstChild("HumanoidRootPart")	local ToolModel = self.ToolModel	local Handle = self.Handle	self.IsAttacking = true	self.LastAttack = tick()	if self.IsBurst then	local BurstAmount = self.NumberOfRoundsInBurst	if self.Ammo < BurstAmount then	BurstAmount = self.Ammo	end	for _ = 1, BurstAmount do	self.LastAttack = tick()	self:PlayAnimation("Shoot")	local Shots = {}	if self.IsShotgun then	Shots = self:ShotgunBlast(HumanoidRootPart, self.InputDirection)	else	Shots = self:SingleShot(HumanoidRootPart, self.InputDirection)	end	ReplicateShot(self, Shots)	self:PlaySound("FireSound", true, true)	self.Ammo = math.max(self.Ammo-1, 0)	self.TotalAmmo = math.max(self.TotalAmmo-1, 0)	self:EjectShell(HumanoidRootPart)	self:UpdateAmmoCounter()	self:HandleRecoil()	wait(1/self.FireRate)	end	if self.UsePumpAnimation then	local Track = self:PlayAnimation("Pump")	if self.Sounds["PumpSound"] then	self:PlaySound("PumpSound")	end	--Track.Stopped:wait()	end	if self.BurstDelay > 0 then	wait(self.BurstDelay)	end	elseif self.IsShotgun then	self:PlayAnimation("Shoot")	local Shots = self:ShotgunBlast(HumanoidRootPart, self.InputDirection)	ReplicateShot(self, Shots)	self:PlaySound("FireSound", true, true)	self.Ammo = math.max(self.Ammo-1, 0)	self.TotalAmmo = math.max(self.TotalAmmo-1, 0)	self:EjectShell(HumanoidRootPart)	self:UpdateAmmoCounter()	self:HandleRecoil()	if self.UsePumpAnimation then	local Track = self:PlayAnimation("Pump")	if self.Sounds["PumpSound"] then	self:PlaySound("PumpSound")	end	--Track.Stopped:wait()	end	wait(1/self.FireRate)	else	local Track = self:PlayAnimation("Shoot")	local Shots = self:SingleShot(HumanoidRootPart, self.InputDirection)	ReplicateShot(self, Shots)	self:PlaySound("FireSound", true, true)	self.Ammo = math.max(self.Ammo-1, 0)	self.TotalAmmo = math.max(self.TotalAmmo-1, 0)	self:EjectShell(HumanoidRootPart)	self:UpdateAmmoCounter()	self:HandleRecoil()	wait(1/self.FireRate)	end	self.IsAttacking = false	if self.IsAutomatic and self.Active then	self:Attack()	end	end end function Class:Update(TimeElapsed)	if self.Owner == nil then return end --	local Character = self.Owner.Model --	local HumanoidRootPart = Character:FindFirstChild("HumanoidRootPart") --	if HumanoidRootPart == nil then return end --	if self.ToolModel then --	if self.Laser then --	local _, HitPosition = Utilities.RaycastWithIgnoreList(HumanoidRootPart.Position, HumanoidRootPart.CFrame.LookVector * 999, self.LaserIgnoreList) --	--self.Laser.Attachment0.CFrame = self.ToolModel.PrimaryPart.CFrame --	self.Laser.Attachment1.WorldPosition = (HitPosition) --	end --	end	if tick() - self.LastAttack >= 0.1 then	self.Spread.Current = math.max(0, self.Spread.Current - (self.Spread.Recovery * TimeElapsed))	end end -------------------------------------------- -- -------------------------------------------- local function OnReplicatedDo(ID, EventName, Extra)	local Tool = Cache[ID]	if Tool then --	warn(EventName)	if EventName == "ShotFired" then	if Tool.TotalAmmo == "inf" then	return	end	Tool.TotalAmmo = Tool.TotalAmmo - Extra --	warn(Tool.Name .. "[" .. tostring(Tool.ID) .. "].TotalAmmo left: " .. tostring(Tool.TotalAmmo))	elseif EventName == "AmmoPickup" then	Tool:Restock(Extra)	end	end end if IsServer then	Replicate.OnServerEvent:Connect(function(Player, ID, EventName, Extra)	OnReplicatedDo(ID, EventName, Extra)	end) else	Replicate.OnClientEvent:Connect(function(ID, EventName, Extra)	OnReplicatedDo(ID, EventName, Extra)	end) end return Class 
4 Likes

Oh and that code both Server and Client can use, this is intentional so that both Player and AI can (and will) have the same base stats per weapon, This also helps ensure that all important events like Equip, Activated and reloading are properly replicated to all clients/the server.

Its a lot of work to setup, takes some planning and big brain juice moments, but in the end you get a nicely working system with some experience to take outside of Roblox with you for later in the future.

And as a side note, don’t do bullet replication inside of the Tool or RangedWeaponClass modules, do it inside of a manager that the tools interface with. That’ll make coding a lot more managable. Oh and local events are just BindableEvents, you’ll need them.

1 Like

These segments of code really helped me understand OOP better, thank you so much. I had no clue that you could do Class.Function(self), I’ve been trying to do cache[ID]:Function() or trying to find the original table and calling the function there lol.

1 Like