How do games handle their gun systems with only one server script? I’m planning to rewrite my current gun system, but I can’t wrap my head around how to check the fire-rate and how to check fire-rate for burst guns etc etc, also my old gun system I had to fire a “visualizer” ray on the client for a faster visualize on click but I’m wondering is there a different way to do this?
my old client-side scirpt
local Players = game:GetService("Players")
local Workspace = game:GetService("Workspace")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local localPlayer = Players.LocalPlayer
local mouse = localPlayer:GetMouse()
local character = localPlayer.Character or localPlayer.CharacterAdded:Wait()
local tool = script.Parent
local handle = tool:WaitForChild("Handle")
local muzzle = handle:WaitForChild("Muzzle")
local remotes = tool:WaitForChild("Remotes")
local values = tool:WaitForChild("Values")
local defaults = tool:WaitForChild("Defaults")
local reloading = values:WaitForChild("Reloading")
local fireMode = defaults:WaitForChild("FireMode")
local ammo = defaults:WaitForChild("Ammunition")
local attackSpeed = defaults:WaitForChild("AttackSpeed")
local range = defaults:WaitForChild("MaximumRange")
local spread = defaults:WaitForChild("BulletSpread")
local burstShots = defaults:WaitForChild("BurstSize")
local burstDelay = defaults:WaitForChild("BurstDelay")
local numBullets = defaults:WaitForChild("PelletCount")
local raycastParams = RaycastParams.new()
raycastParams.CollisionGroup = "Characters"
raycastParams.FilterDescendantsInstances = {handle}
raycastParams.FilterType = Enum.RaycastFilterType.Exclude
raycastParams.IgnoreWater = true
local activated = false
local coolingDown = false
local timeOfLastShot = tick()
local function castRay()
local serverTimeNow = Workspace:GetServerTimeNow() * 1000000
local RNG = Random.new(serverTimeNow)
local mousePosition = mouse.Hit.Position
local origin = muzzle.WorldPosition
local direction = (mousePosition - origin).Unit * range.Value + Vector3.new(RNG:NextNumber(-spread.Value, spread.Value), RNG:NextNumber(-spread.Value, spread.Value), RNG:NextNumber(-spread.Value, spread.Value))
local raycastResult = Workspace:Raycast(origin, direction, raycastParams)
local intersection = raycastResult and raycastResult.Position or origin + direction
local normal = raycastResult and raycastResult.Normal
local distance = (origin - intersection).Magnitude
local hitHumanoid = false
remotes.Fire:FireServer(mousePosition, serverTimeNow)
if raycastResult then
local character = raycastResult.Instance.Parent
local humanoid = character:FindFirstChild("Humanoid")
if humanoid and character:HasTag("Zombie") then
hitHumanoid = true
end
end
ReplicatedStorage.Bindables.ReplicateShot:Fire(tool, intersection, distance, normal, hitHumanoid)
end
local function canFire(): boolean
return not reloading.Value and ammo.Value > 0
end
local function processFire()
for index = 1, numBullets.Value do
if not canFire() then
print(ammo.Value)
return
end
castRay()
end
end
local function processReload()
remotes.Reload:FireServer()
end
local function startFiring()
if ammo.Value <= 0 then
warn("HAS TO RELOAD")
processReload() -- If the player has no ammo, start reloading automatically.
end
if not canFire() then
warn("CANFIRE")
return
end
if coolingDown then
warn("COOLING DOWN")
return
end
if fireMode.Value == "Single" then
processFire()
coolingDown = true
task.delay(attackSpeed.Value, function()
coolingDown = false
end)
elseif fireMode.Value == "Burst" then
coolingDown = true
for index = 1, burstShots.Value do
if not canFire() then
coolingDown = false
return
end
processFire()
task.wait(burstDelay.Value)
end
task.delay(attackSpeed.Value, function()
coolingDown = false
end)
elseif fireMode.Value == "Auto" then
task.spawn(function()
coolingDown = true
while activated and canFire() do
local currentTime = tick()
if (currentTime - timeOfLastShot) >= attackSpeed.Value then
processFire()
timeOfLastShot = currentTime
else
task.wait()
end
end
coolingDown = false
if ammo.Value <= 0 then
processReload()
end
end)
end
end
local function onActivated()
activated = true
startFiring()
end
local function onDeactivated()
activated = false
end
tool.Activated:Connect(onActivated)
tool.Deactivated:Connect(onDeactivated)
my old server-side script
local Players = game:GetService("Players")
local Workspace = game:GetService("Workspace")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local tool = script.Parent
local handle = tool.Handle
local muzzle = handle.Muzzle
local remotes = tool.Remotes
local values = tool.Values
local defaults = tool.Defaults
local reloading = values.Reloading
local fireMode = defaults.FireMode
local ammo = defaults.Ammunition
local clip = defaults.MagazineCapacity
local reserve = defaults.ReserveAmmunition
local reloadSpeed = defaults.ReloadSpeed
local attackSpeed = defaults.AttackSpeed
local range = defaults.MaximumRange
local spread = defaults.BulletSpread
local burstShots = defaults.BurstSize
local burstDelay = defaults.BurstDelay
local numBullets = defaults.PelletCount
local fireSound = handle.Fire
local reloadSound = handle.Reload
local raycastParams = RaycastParams.new()
raycastParams.CollisionGroup = "Characters"
raycastParams.FilterDescendantsInstances = {handle}
raycastParams.FilterType = Enum.RaycastFilterType.Exclude
raycastParams.IgnoreWater = true
local equipped = false
local reloadThread
local timeOfLastShot = os.clock()
local timeOfLastBurstShot = os.clock()
local timeOfLastFullBurst = os.clock()
local burstFired = 0
local function canFire(mousePosition: Vector3, clientServerTime: number): boolean
return equipped and typeof(mousePosition) == "Vector3" and typeof(clientServerTime) == "number" and not (clientServerTime ~= clientServerTime) and not reloading.Value and ammo.Value > 0
end
local function canReload(): boolean
return equipped and not reloading.Value and ammo.Value ~= clip.Value and reserve.Value > 0
end
local function castRay(playerThatFired: Player, mousePosition: Vector3, serverTime: number): (Vector3, number, Vector3, boolean)
local RNG = Random.new(serverTime)
local origin = muzzle.WorldPosition
local direction = (mousePosition - origin).Unit * range.Value + Vector3.new(RNG:NextNumber(-spread.Value, spread.Value), RNG:NextNumber(-spread.Value, spread.Value), RNG:NextNumber(-spread.Value, spread.Value))
local raycastResult = Workspace:Raycast(origin, direction, raycastParams)
local intersection = raycastResult and raycastResult.Position or origin + direction
local normal = raycastResult and raycastResult.Normal
local distance = (origin - intersection).Magnitude
local hitHumanoid = false
if raycastResult then
local character = raycastResult.Instance.Parent
local humanoid = character:FindFirstChild("Humanoid")
if humanoid and character:HasTag("Zombie") then
hitHumanoid = true
end
end
return intersection, distance, normal, hitHumanoid
end
local function onFireEvent(playerThatFired: Player, mousePosition: Vector3, clientServerTime: number)
if not canFire(mousePosition, clientServerTime) then
return
end
local currentTime = os.clock()
if fireMode.Value == "Burst" then
if burstFired == 0 and (currentTime - timeOfLastBurstShot) < burstDelay.Value then
return
end
if burstFired >= burstShots.Value then
if (tick() - timeOfLastFullBurst) < attackSpeed.Value then
return
end
burstFired = 0
timeOfLastFullBurst = tick()
timeOfLastBurstShot = tick()
end
else
if (currentTime - timeOfLastShot) < attackSpeed.Value then
return
end
end
for index = 1, numBullets.Value do
local intersection, distance, normal, hitHumanoid = castRay(playerThatFired, mousePosition, clientServerTime)
for _, player in Players:GetPlayers() do
if player ~= playerThatFired then
ReplicatedStorage.Remotes.ReplicateShot:FireClient(player, tool, intersection, distance, normal, hitHumanoid)
end
end
end
timeOfLastShot = currentTime
ammo.Value -= 1
end
local function onReloadEvent(player: Player)
if not canReload() then
warn("AHHH")
return
end
reloading.Value = true
reloadSound:Play()
reloadThread = task.delay(reloadSpeed.Value, function()
local reallocate = math.min(clip.Value - ammo.Value, reserve.Value)
ammo.Value += reallocate
reserve.Value -= reallocate
print(ammo.Value)
reloading.Value = false
end)
end
local function onEquipped(mouse: Mouse)
equipped = true
end
local function onUnequipped()
equipped = false
if reloadThread then
task.cancel(reloadThread)
reloading.Value = false
reloadSound:Stop()
fireSound:Stop()
end
end
tool.Equipped:Connect(onEquipped)
tool.Unequipped:Connect(onUnequipped)
remotes.Fire.OnServerEvent:Connect(onFireEvent)
remotes.Reload.OnServerEvent:Connect(onReloadEvent)
yes I know there’s flaws in these