Best way to handle a gun's server-sided hit detection, and to fix horrid latency?

Greetings Robloxians. Recently I have been working on a third-person shooter, but latency between the server and client has been, horrid, to say the least. I am working on an automatic rifle that is supposed to fire every 0.15 seconds, but instead it jankily shoots every now and then, with sometimes latency making you have to wait a whole second to 2 seconds for the next fire, horrible UX.

I somewhat circumvented this by having the shooting player have bullets perfectly in-sync on their side, but in reality the gun can randomly stop working and not a single bullet would legitimately be fired, until you stop shooting and start shooting again.

Why this happens boggles my mind. My first guess is that 0.15 seconds just might be too much data too fast for Roblox’s server-client boundary to handle, but that is straight up pathetic if so, and not to mention that I’ve seen more games than I can count on Roblox that have faster guns than what I have.

The shooting and bullets themselves is created immediately when the gun shoots on the client, so there is never any lag with those, but the gun gui that displays the ammo and rounds does not update until the server fully comprehends that the bullet was shot, so you can gauge what other people see via the ammo amount in the bottom of the screen.

Here is a video of the lag in action:

As you can see, there is a large delay when at 20 bullets and a small lag spike happens again at 9. This is a much better version of the lag, when I have 4 people in the server shooting (the game is designed for 8) the lag gets much more unbearable.

This can be a huge issue because the player could be shooting straight onto an enemy for upwards of 5 seconds, but none of them will technically register, and their ammo will never run out.

Now the question is how in the world would I fix this latency?

To worsen this, I have to switch hit detection from the client side to the server side to prevent easy exploits from occuring, which I would have reason to believe the lag will only worsen. Thus I have two questions:

1: How do I optimize the code of my gun to end this horrid latency? (client and server sided code below)

2: How would I create the ray of the bullet on the server side, now that I can’t use mouse.hit.p?

CLIENT CODE:


local function shoot()
	local hold = true
	repeat
		if userInputService:IsMouseButtonPressed(Enum.UserInputType.MouseButton1) or userInputService:IsGamepadButtonDown(Enum.UserInputType.Gamepad1, Enum.KeyCode.ButtonR2) then
			hold = true
		else
			hold = false
		end
		if CanShoot == true and tool.Ammo.Bullets.Value > 0 and hold == true then
			CanShoot = false
			local ray = Ray.new(tool.WeaponModel.Hole.CFrame.p, (mouse.Hit.p - tool.WeaponModel.Hole.CFrame.p).unit * weapon.Stats.MaxRange.Value)
			local ignore = {player.Character, table.unpack(tool:GetChildren())}
			for i, v in pairs(game.Players:GetChildren()) do
				if v.Character then
					if v.Character:FindFirstChild("Hat") then
						if v.Character.Hat:FindFirstChild("Handle") then
							table.insert(ignore, v.Character.Hat.Handle)
						end
					end
				end
			end
			for i, v in pairs(game.Workspace.Map.Tools:GetDescendants()) do
				table.insert(ignore, v)
			end
			for i, v in pairs(game.Workspace.Map.Buffs:GetDescendants()) do
				table.insert(ignore, v)
			end
			for i, v in pairs(game.Workspace.Map.Spawns:GetDescendants()) do
				table.insert(ignore, v)
			end
			local hitPart, position = workspace:FindPartOnRayWithIgnoreList(ray, ignore, false, true)
			if hitPart then
				local hitpoint = mouse.Hit.p
				tool.Events.Fire:FireServer(hitPart, hitpoint)
			end
			game.ReplicatedStorage.Events.PlayerEvents.GunScreen:Fire()
			recoilTrack:Play()
			local weaponName = character.Tool.WeaponName.Value
			local weaponFolder = game.ReplicatedStorage.Gamemodes.Mode.Weapons[weaponName]
			local tool = character.Tool
                 -- bullet
			local bullet = weaponFolder.Bullet:Clone()
			collectionService:AddTag(bullet, "bullet")
			bullet.CFrame = CFrame.new((tool.WeaponModel.Hole.CFrame * CFrame.new(0,0,0)).p, mouse.Hit.p)
			local rotate = CFrame.Angles(0, math.rad(-90),0)
			bullet.CFrame = bullet.CFrame:ToWorldSpace(rotate)
			local bodyV = Instance.new("BodyVelocity", bullet)
			bodyV.Velocity = CFrame.new(bullet.Position, mouse.Hit.p).LookVector * 260
			bullet.Transparency = 0
			bullet.PlayerValue.Value = player.Name
			bullet.Parent = game.Workspace.Current
			player.PlayerScripts.Weapons.Bullet.Launched:Fire()
			tool.Handle.Fire:Play()
			wait(cooldown)
			CanShoot = true
		else
			hold = false
		end
	until hold == false
end

tool.Equipped:Connect(function()
	gunGui.Ammo.Ammo.Text = tool.Ammo.Bullets.Value
	gunGui.Rounds.Rounds.Text = tool.Ammo.Rounds.Value
	local tweenInfo = TweenInfo.new(0.3, Enum.EasingStyle.Exponential, Enum.EasingDirection.Out, 0, false, 0)
	local goal = {}
	goal.FieldOfView = 70
	local tween = tweenService:Create(workspace.Camera,tweenInfo, goal)
	tween:Play()
	aimBool.Value = false
	local tweenInfo = TweenInfo.new(0.75, Enum.EasingStyle.Bounce, Enum.EasingDirection.Out, 0, false, 0)
	local goal = {}
	goal.Position = UDim2.fromScale(0.412, 0.85)
	local tween = tweenService:Create(gunGui,tweenInfo, goal)
	tween:Play()
	equipTrack:Play()
	equipTrack.Stopped:Wait()
	CanShoot = true
	mouse.Icon = "rbxassetid://6945978207"
	holdTrack:Play()
	actionService:BindAction("MouseFire", shoot, false, Enum.UserInputType.MouseButton1)
	actionService:BindAction("ControllerFire", shoot, true, Enum.KeyCode.ButtonR2)
	actionService:BindAction("MouseReload", reload, false, Enum.KeyCode.R)
	actionService:BindAction("ControllerReload", reload, true, Enum.KeyCode.ButtonX)
	actionService:BindAction("MouseAim", aim, false, Enum.UserInputType.MouseButton2)
	actionService:BindAction("ControllerAim", aim, true, Enum.KeyCode.ButtonL2)
end)

tool.Events.Fire.OnClientEvent:Connect(function(ammo)
	gunGui.Ammo.Ammo.Text = ammo
-- this gets fired back by the server, so the gunGui text will not change until the server registers the bullet, thus you can detect lag with the gunGui on the bottom of the screen.
end)

SERVER SIDED CODE:


events.Fire.OnServerEvent:Connect(function(player, hitPart, hitpoint)
	if tool.Ammo.Bullets.Value > 0 and CanRun == true then
		local localPlayer = players:GetPlayerFromCharacter(tool.Parent)
		CanRun = false
		game.ReplicatedStorage.Events.GameEvents.WeaponEvents.Bullet:FireAllClients(player, hitpoint, tool)
		tool.Ammo.Bullets.Value = tool.Ammo.Bullets.Value - 1
		events.Fire:FireClient(player, tool.Ammo.Bullets.Value)
		if hitPart ~= nil then
			local part = hitPart
			local humanoid = part.Parent:FindFirstChild("Humanoid")
			if humanoid then
				local char = part.Parent
				local plr = players:GetPlayerFromCharacter(char)
				if plr ~= localPlayer then
					local damage = weapon.Stats.Damage.Value
					if hitPart.Name == "Head" then
						damage = weapon.Stats.Damage.Value * weapon.Stats.HeadshotMult.Value
					end
					local changeType = "Damage"
					game.ReplicatedStorage.Events.PlayerEvents.HealthChange:Fire(player, plr, damage, changeType, game.ReplicatedStorage.Gamemodes.Mode.Weapons[tool.WeaponName.Value].DisplayName.Value)
				end
			end
			print("finished!")
		end
		CanRun = true
	end
end)

Let the client handle the shooting, bullets, etc, and replicate a remote-event to the server for only the hit event (and anything else important if you have to.)

That is what I’m already doing. The client immediately creates a dummy bullet, recoil effect, and sound. The client as of right now also handles the hit detection, and the server handles detecting whether or not that hitPart is a player, and if so to do damage to them.

What I’m trying to accomplish is to stop latency from the client to the server so lag like this isn’t as bad, and also I want to move hit detection to the server to prevent exploits. I don’t know how I would cast a ray though without mouse.hit.p, my initial guess is to cast a ray from the direction the hole of the gun is pointing, but how would I accomplish that is beyond me.

I’m gonna offer a suggestion just to see if it helps at all. No guarantee it will, but could you add a spawn function before your repeat loop in your shoot function. I think the loop that you’re activating every time is the big issue and a spawn function could make that loop run on its own each time.

You can send the Mouse.Hit.p to the server using a remote.
Thru a local script, you can set it to a parameter: event:FireServer(mouse.Hit.p)

That is what hit detection on the client side is, am I wrong?

The exploits come in to play when the client sets mouse.hit.P to a random parameter of where a player is, so I believe.

Perhaps I am wrong and I am already using server-sided hit detection.

I have never utilized spawn functions and my understanding of them is pretty thin, would you mind showing me how I would use a spawn function in this situation?

local function shoot()
	local hold = true
spawn function()
	repeat
		if userInputService:IsMouseButtonPressed(Enum.UserInputType.MouseButton1) or userInputService:IsGamepadButtonDown(Enum.UserInputType.Gamepad1, Enum.KeyCode.ButtonR2) then
			hold = true
		else
			hold = false
		end
		if CanShoot == true and tool.Ammo.Bullets.Value > 0 and hold == true then
			CanShoot = false
			local ray = Ray.new(tool.WeaponModel.Hole.CFrame.p, (mouse.Hit.p - tool.WeaponModel.Hole.CFrame.p).unit * weapon.Stats.MaxRange.Value)
			local ignore = {player.Character, table.unpack(tool:GetChildren())}
			for i, v in pairs(game.Players:GetChildren()) do
				if v.Character then
					if v.Character:FindFirstChild("Hat") then
						if v.Character.Hat:FindFirstChild("Handle") then
							table.insert(ignore, v.Character.Hat.Handle)
						end
					end
				end
			end
			for i, v in pairs(game.Workspace.Map.Tools:GetDescendants()) do
				table.insert(ignore, v)
			end
			for i, v in pairs(game.Workspace.Map.Buffs:GetDescendants()) do
				table.insert(ignore, v)
			end
			for i, v in pairs(game.Workspace.Map.Spawns:GetDescendants()) do
				table.insert(ignore, v)
			end
			local hitPart, position = workspace:FindPartOnRayWithIgnoreList(ray, ignore, false, true)
			if hitPart then
				local hitpoint = mouse.Hit.p
				tool.Events.Fire:FireServer(hitPart, hitpoint)
			end
			game.ReplicatedStorage.Events.PlayerEvents.GunScreen:Fire()
			recoilTrack:Play()
			local weaponName = character.Tool.WeaponName.Value
			local weaponFolder = game.ReplicatedStorage.Gamemodes.Mode.Weapons[weaponName]
			local tool = character.Tool
                 -- bullet
			local bullet = weaponFolder.Bullet:Clone()
			collectionService:AddTag(bullet, "bullet")
			bullet.CFrame = CFrame.new((tool.WeaponModel.Hole.CFrame * CFrame.new(0,0,0)).p, mouse.Hit.p)
			local rotate = CFrame.Angles(0, math.rad(-90),0)
			bullet.CFrame = bullet.CFrame:ToWorldSpace(rotate)
			local bodyV = Instance.new("BodyVelocity", bullet)
			bodyV.Velocity = CFrame.new(bullet.Position, mouse.Hit.p).LookVector * 260
			bullet.Transparency = 0
			bullet.PlayerValue.Value = player.Name
			bullet.Parent = game.Workspace.Current
			player.PlayerScripts.Weapons.Bullet.Launched:Fire()
			tool.Handle.Fire:Play()
			wait(cooldown)
			CanShoot = true
		else
			hold = false
		end
	until hold == false
end)
end

^ just make sure you tab it out > you can highlight over everything in between the spawn function and press tab once to tab everything together.

It’s partly client and server, it just sends the Mouse.Hit.p to the remote, your remote will handle the detection.
If you’re worried about exploits, you could use sanity checks.

I’d guess the sanity check being used is a direct line of sight check. Would this require a server-sided raycast? If so then it’s still essentially my original question.

I will say for detection > you should never create instances on the client due to them bringing so many issues > my general tip is to create the skills,move them server side and all that > but send a request to client and loop a magnitude or raycast check > this keeps it consistent and I haven’t really had any big issues with it.

1 Like

You could probably use a boolean to tell the remote whether or not to do damage, the value being set to true if the sanity checks are valid. Other than that, I’m not so sure…

1 Like

Ok I see, so it appears that the best solution would be to send the server mouse.hit.p, but create the raycast on the server and find the hitpart there. I have also tried the spawn(function() suggestion and have found no latency on my own yet, but I have to try it later on with multiple people.

Damage isn’t my issue really. If there is a hitpart that is a player damage is taken on the server with a bindableEvent to a separate script that is just for hit detection. I think we’ve found a solution of simply adding a spawn() function to the shoot function, and also creating the ray on the server instead of the client.

1 Like

On a side note, something good you can use for when you’re actually creating skills on server would be to use network ownership.

Network Ownership (roblox.com)

This will only work serverside, but it’ll run just like it would on client removing any initial spawn lag. You also cant use it on parts that are anchored.

1 Like

Network ownership isn’t really an issue for my game. The bullets you physically see are purely for visuals and are too fast to see any noticeable lag. Also, the slight delay from you shooting the bullet to it reaching your target because it isn’t instant helps make server latency look like it’s just the bullet travel, because the actually hit detection is instant.

My issue isn’t the delay between shooting and your target being it, that is a fairly trivial issue on my game. My main issue is that the server must fully register the hit before you can shoot again, so your shots aren’t consistent, but we might’ve found a good conclusion that I will confirm once the code is finished.

Not sure if you still need assistance with this but I actually have a better method then just letting the server handle things , what should happen is you should handle damage on the server, as well as sounds. For part generation and ray casting what you should instead do is do it how you would on the main players client before filtering enabled, then alongside it make variables for all of the vector3s, and pass them to the server. Now that the server has this info set up a client side to the remote event and copy the sequence of what’s happening on the main players client to the other clients given the vector3s to replicate it accurately. To make it even more accurate instead of doing them alongside eachother , run a spawn function or coroutine that fires the remote event before you do you action on the main players client so they both happen simultaneously.

The code only checks if the part is a humanoid, no other checks are done.
Also, I apologize for the awful formatting, the code blocks refuse to obey my will.