Is there a better way to account for lag in an fps using raycasting?

I’m working on an fps, the model will work as so:

  • Client presses the right mouse button
  • Client receives instant feedback of shot, recoil is shown, sound is made and local shot is fired
  • A simple check is done on the Client to check if the shot hits to show the correct hit markers
  • The Client tells the Server about the shot by including the origin of the shot and the time fired
  • The Server checks if the shot is valid by grabbing where the players were at the time of the shot, and using the old positions to calculate if the shot hits the player or not (to account for lag)
  • The Server damages the player it hits, and other Clients are also informed of the shot being fired

This is the current script I’m using (In the server):

local inducedPing = 0.5 --500ms (for testing purposes only)
local positions = {
	["StarterCharacter"] = {
		
	}
}

local sChr = workspace.StarterCharacter
local pos1 = CFrame.new(-30, 5.8, -30)
local pos2 = CFrame.new(30, 5.8, -30)
local lr = 0
local t = 200
local state = true

local fp = 0

game:GetService("RunService").Stepped:Connect(function()
	if state == true then
		lr = lr + 1
		if lr == t then 
			state = false 
		end
	else
		lr = lr - 1
		if lr == 0 then 
			state = true 
		end
	end
	sChr:SetPrimaryPartCFrame(pos1:lerp(pos2, lr/t))
	fp = fp + 1
	if fp%3 == 0 then
		table.insert(positions.StarterCharacter, #positions.StarterCharacter+1, {})
		for i,v in pairs(sChr:GetChildren()) do
			if v:IsA("BasePart") then
				positions.StarterCharacter[#positions.StarterCharacter][v.Name] = v.CFrame
			end
		end
		local len = #positions.StarterCharacter
		if len == 20 then
			table.remove(positions.StarterCharacter, 1)
		end
	end
end)

local fakechr = game.ReplicatedStorage.FakeCharacter

game.ReplicatedStorage.shoot.OnServerEvent:Connect(function(player, hitplayer, tic, origin, tar) --tar is just temp to calculate ray
	local ping = inducedPing + (tick() - tic)
	wait(ping)
	local pr = #positions.StarterCharacter - math.min(math.ceil(ping*20 - 0.5), 19)
	local chr = fakechr:Clone()
	for i,v in pairs(chr:GetChildren()) do
		v.CFrame = positions[hitplayer.Name][math.floor(pr)][v.Name]:lerp(positions[hitplayer.Name][math.ceil(pr)][v.Name], pr%1)
	end
	chr.Parent = workspace
	local ray = Ray.new(origin.p, (tar - origin.p).unit * 500)
	local hit, position = workspace:FindPartOnRayWithWhitelist(ray, {chr})
	if hit then
		print("hit")
		wait(0.5)
	end
	chr:Destroy()
end)

What it does is it stores the CFrames of the Characters every 1/20th of a second, then using ping it calculates the mid point between the CFrames. For example if the ping results in a value of 11.66
it will grab a value that is 66.6% between 11 and 12 CFrame as an estimate will most likely be accurate instead of storing it every frame.
Unfortunately, if this was a server of 30 players it could be storing up to 6,600 CFrames at any given time (because players * framesstored * numberofbodyparts, 30 * 20 * 11 = 6,600.

This could be quite intensive however, and thats what I am worried about, I will have to recreate all of the CFrames of all the players when a shot is fired just to check where it hits using fake models.

Another problem I’m worried about is backtracking where they could pretend they have a higher ping then they actually do and hit where the player was and not where they were.

I’m not really sure a good method to getting rid of the lag the player may experience, however, this does seem to be accurate atleast 99% of hits being detected even at really high pings.

3 Likes

To address your performance issue (assuming you keep this approach), you are really going to have to judge whether you want to have high fidelity or high performance. One way of increasing performance while reducing fidelity is your sampling rate. Do you really need 20 data-points per second? Instead, how about just 4 per second? You are already interpolating the players position between data-points to make time-contentious, so 20hz might be overkill. Another thing you could do is to avoid capturing the CFrame for each body part. Instead, just capture the players root CFrame. With just the root CFrame, you can then recreate a rough approximation of the users hitbox server-side. While this will reduce fidelity, it will decrease resource usage substantially. Just by doing this, you can reduce your datapoints from 6600 per second to (30 * 4 * 1) = 120.

However, there are other approaches that you can take which are much less memory-intensive.

2 Likes

dFirst off, :SetPrimaryPartCFrame is not something you want to call every frame. Consider using welds. SetPrimaryPartCFrame is prone to floating point imprecision as well as delays.

Also there’s no good reason to do what you’re trying to do. This just looks like bad practice.

I think I could dramatically reduce the sampling rate while remaining high amounts of detail by reducing to about 8Hz, while this is still quite high I think it will be most accurate. I think It will be quite hard to estimate the CFrames of other parts as it depends on the weapons equipped and there may be walk cycles as well. I don’t want lots of false positives appearing on client when server doesn’t detect a hit.

You say there are other aproaches, can you suggest any?

1 Like

I’m only using :SetPrimaryPartCFrame() as a test for moving the character. The character has motor6ds so there shouldn’t be any floating point errors.

And, I’m not sure what you mean as theres “no good reason” to do what I’m trying to do. If the client detects a hit but then 250ms the server only finds out, the player could have already moved quite a reasonable distance, thus it probably won’t detect a hit occurring. This would lead to an unnecessary amounts of false positives on the client. The only way is to backtrack where the other players were at the time of the shot being fired. Unless you can suggest a solution that works and accounts for lag, you can’t exactly call this a “bad practice”.

No it’s not. Detect if it’s feasible for the player have traveled the distance they did. Tracking their position is not necessary at all.

1 Like

If it’s possible for them to have been in that position a few seconds ago, count it as a hit.

2 Likes