How do games handle their gun systems with only one server script?

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

Most games just have general remote events for their guns. An example would be something like this:

SERVER:

-- Simply an example, not accurate (ignore errors, did not use studio for this)
local ReloadEvent = game:GetService("ReplicatedStorage").Reload

ReloadEvent.OnServerEvent:Connect(function(player)
     local character = player.Character
     if character then
          local tool = character:FindFirstChildOfClass("Tool")
          if tool then
               tool.Handle.ReloadSound:Play()
          end
     end
end)

The above is just a simple example of a remote to handle playing a reload sound on the server. Obviously gun systems are much more complex than just this.

Okay, thanks, do you have any thoughts on how to handle my old system better because I was just doing the same thing on the client as the server and raycasting on both ends and visualizing the bullet but I feel like its bad

Well, for starters, raycasting on the server isn’t the best. It often results in a lot of latency and incorrect results being produced. Instead, you should use the raycast data from the person making the shot, and then do your best to logically validate the data they’re sending. This can be done multiple ways, such as validating the distance, the hit target, etc.

Additionally, you should store important configuration- like damage, on the server. Don’t rely on the client to tell you how much damage to do. Instead, just get the value yourself on the server.

I don’t think it’d be viable to “migrate” your current script. Realistically, you should take aspects of your current scripts, and use them to design a new set of scripts with the singular server script architecture.

Okay, so all the ray-casting for the shot itself should be done on the client, but on the server I’ll make sure to verify that they can damage people and distance and whatever other parameters right?

Yes. That’s how most people implement things like this. It’s much more performant than raycasting on the server.

1 Like

Okay, thank you so much for your help bro :pray:

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.