How do I make accurate hit detection for area of effect damage

Due to the latency between server and client, the player sees their character is outside of the hitbox but the server still has the character inside of the range. Hitboxes are cylinder parts with the attributes “Life” for how long they last before being destroyed and then inflicting “Damage” amount of damage to players in the radius.

Here is a video of my problem:

To fix this I attempted to code a solution that follows this logic:
-Warnpart is added
-Client checks debris time on the warnpart
-After lifetime expires client reports position to server
-Server compares position to where the client theoretically could have gone in the span of the damage part spawning (this can be delegated to an anti-teleport/speedhack script running a simulation of all characters)
-Client will report if they got hit as well (can’t be trusted fully but there has to be some level of trust in this system)

So far to implement this logic i have this code, but i’m stuck on what to do next:

Client:

local Remotes = game.ReplicatedStorage.Remotes
local RequestDamagePart = Remotes.RequestDamagePart
RequestDamagePart.OnClientInvoke = function()
	return Character.PrimaryPart.Position, workspace:GetServerTimeNow()
end

workspace.DamageParts.ChildAdded:Connect(function(child)
	child.Destroying:Connect(function()
		for _, v in pairs(workspace:GetPartsInPart(child)) do
			if v.Parent == Character then
				--Tell server i got hit
			end
		end
	end)
end)

Server:

local Remotes = game.ReplicatedStorage.Remotes
local RequestDamagePart = Remotes.RequestDamagePart
workspace.DamageParts.ChildAdded:Connect(function(child)
	local hitTable = {}
	if child:IsA("BasePart") then
		task.wait(child:GetAttribute("Life"))		
		for _, player in pairs(game.Players:GetPlayers()) do
			if not player.Character or not player.Character:FindFirstChild("Humanoid") then
				continue
			end
			local cachedPosition = player.Character.PrimaryPart.Position
			if (child.Position - cachedPosition).Magnitude <= child.Size.Z then
				player.Character.Humanoid:TakeDamage(child:GetAttribute("Amount"))
			end
			if (child.Position - cachedPosition).Magnitude <= child.Size.Z * 3 then
				task.spawn(function()
					local pingTime = workspace:GetServerTimeNow()
					local timeoutcount = 0
					local timeout = 5
					local result = nil
					task.spawn(function()
						local position, timestamp 
						local succ, err = pcall(function()
							position, timestamp = RequestDamagePart:InvokeClient(player)
						end)
						if not succ then
							warn(err)
						end
						if position and timestamp and typeof(position) == "Vector3" and typeof(timestamp) == "number" then
							result = "\n\n"..tostring(position)..", "..timestamp
						else
							--must be exploiting
							if (child.Position - cachedPosition).Magnitude <= child.Size.Z then
								player.Character.Humanoid:TakeDamage(child:GetAttribute("Amount"))
							end
						end
					end)
					repeat
						task.wait(1)
						timeoutcount = timeoutcount + 1
					until timeoutcount >= timeout or result ~= nil
					if result then
						print(result)
					else
						warn("Player "..player.Name.." took too long to respond to remote call to ReplicatedStorage.Remotes.RequestDamagePart")
						--must be exploiting or laggy	
					end
				end)
			end
		end
		child:Destroy()
	end
end)

When you add the warnpart, spawn it with an attribute that has the time when the hitbox or whatever will detonate
The given time should be a synced value that is dictated by the server and easily retrieved by any client
Personally I use an attribute in workspace that has the server’s tick() value that is updated every x seconds

By the time the client detects the warnpart, it can already compute the network latency or whatever delays might happen

Three side notes/suggestions:

  1. RemoteFunctions are extremely slow and easy to break; two-way RemoteEvents are faster and don’t break if an error is thrown
  2. Make the warnpart (im assuming its the red cylinder) expand towards a goal size before damage is done to give the client more time to react to seeing the warning
  3. If you’re making your collisions even slightly client-sided you might as well make them fully client-sided, because exploiters are effectively “god” and will find ways to avoid the hitboxes regardless of what securities are perceived to work against them

For 1 and 2, thanks I will make those changes.

But for 3, does this mean I should not even bother with server sided hit detection or I need to change my approach to even make it work? Like running a simulation on the server for positions (frankly I have no idea how to do this) like most competitive games do?

In my games I use precise/expensive hit detections on the client (hitboxes, high-volume raycasts, etc) and the server usually just does a distance/raycast check if applicable. Of course there are instances where the server would cast hitboxes/expensive stuff but it’s really up to you as to how complex/straightforward you want your collisions detections to be.

I usually have it this way so that gameplay can feel more responsive for clients with poor connections, at the cost of slight inaccuracies/inconsistencies with distance checks

I drew a pretty graph that best explains what im trying to say

1 Like

LOL understood

yeah i’m not making a very high budget game i’ll just go with the easiest client sided method for now and figure out how to deal with exploiters when i actually get players

1 Like