Calculating Lag-Compensated Positions

Hey, and thanks for reading in advance.

One of the classes in the small CTF-style game I’m developing is capable of raising a shield to block all frontal attacks, negating their damage entirely. To be clear, I already know multiple ways of determining whether or not a position is ‘behind’ a player relative to their position/facing direction - that’s not the issue. I’m using a dot product and angular comparison to make this determination.

That said - the issue I’m having is the function returning false-positives due to latency - allowing my attacks to damage someone that is clearly blocking and clearly facing me.

For debugging, I’ve made the function create two parts - a blue box representing the server’s idea of the target’s HumanoidRootPart position, and another part placed at the position passed to the check that is either red or green depending on which result the ‘behind’ check returned - red for negative, green for positive.

This is the result of a particular test seen from my end. The blue box representing the Root position is close to perfectly accurate, and although obscured by my opponent’s model, a green part representing the position of the arrow’s raycast is behind his torso, indicating a successful ‘behind’ check even though my opponent is facing me and very clearly blocking.

This is the result of another test conducted, shown from my opponent’s perspective:

External Media

You can very clearly see here that even accounting for the built-in lag compensation, the server’s idea of where my opponent is does not match his own perspective whatsoever, and it becomes obvious why the server thinks my shot is behind him.

If anyone knows what I might do to alleviate this or spots a mistake I’ve made, I’d appreciate the info.

Code (inside a server-side module):

function Core:IsBehind(CF, Target)
	local RUN = game:GetService("RunService")
	local Root = Target:FindFirstChild("HumanoidRootPart")
	
	if Root then
		local Player = game.Players:GetPlayerFromCharacter(Target)
		local Latency = (Player and Player:GetAttribute("Latency")) or 0
		
		local GuessPosition = Root.Position - (Root.AssemblyLinearVelocity * Latency)
		local GuessCF = CFrame.new(GuessPosition, GuessPosition + Root.CFrame.LookVector)
		local PassCheck = math.acos(GuessCF.LookVector:Dot((CF.Position - GuessPosition).Unit)) >= math.pi/2
		
		--
		local Marker = Instance.new("Part")
		Marker.Size = Vector3.new(1, 1, 1)
		Marker.Anchored = true; Marker.CanCollide = false; Marker.CanQuery = false
		Marker.Color = (PassCheck and Color3.new(0, 1, 0)) or Color3.new(1, 0, 0)
		Marker.Material = Enum.Material.Neon
		Marker.Transparency = .5
		Marker.CastShadow = false
		Marker.Name = tostring(CF.Position)
		Marker.CFrame = CF
		Marker.Parent = workspace
		
		game.Debris:AddItem(Marker, 2)
		--
		
		--
		local FakeRoot = Instance.new("Part")
		FakeRoot.Size = Root.Size
		FakeRoot.Anchored = true; Marker.CanCollide = false; Marker.CanQuery = false
		FakeRoot.Color = Color3.new(0, 0, 1)
		FakeRoot.Material = Enum.Material.Neon
		FakeRoot.Transparency = .5
		FakeRoot.CastShadow = false
		FakeRoot.Name = "FakeRoot"
		FakeRoot.CFrame = GuessCF
		FakeRoot.Parent = workspace

		game.Debris:AddItem(FakeRoot, 2)
		--]]
		
		return PassCheck
	end
	
	return false
end

Still haven’t found a cohesive solution to this that doesn’t involve outright trusting one client or the other - help or advice would be much appreciated. :slight_smile: