Hitboxes Buggy/Lagging?

I’m making an Asym (yea yea original i know), but I’m struggling with hitboxes, not the concept or anything, but having them spawn ATLEAST the general vicinity of the player.
I have 0 clue how but whenever the player creates hitboxes its 20 miles away on their screen, but 10 miles ahead from the server??? (expertly crafted mock-up here)


For context, I’m detecting an input from the player, telling that to the server via a remote, and having the server call upon a module script to handle the creation, hit detection, and damaging of the player. Idk if this is what’s causing the delay but any help would be GREATLY appreaceated.

4 Likes

Could you provide a video or some example code, please?

It’s because of ping. The server’s position of the client is delayed based on ping.
To fix, the client should pass in the position of the hitbox. Assure it is valid with distance checks.

1 Like


The grey thing is the character and the red box is the hitbox
and heres a bit from the module

function hitbox.Create(plr, range, height, size)
	
	local hitbox = Instance.new("Part")
	
	hitbox.Name = "hitbox ".. plr.Name
	hitbox.Size = Vector3.new(size, height, range)
	hitbox.CanCollide = false
	hitbox.Transparency = 0.5
	hitbox.Parent = workspace.Hitboxes
	hitbox.Anchored = true
	hitbox.Color = Color3.new(1, 0, 0)
	hitbox.Material = Enum.Material.ForceField

	hitbox.Position = Vector3.new(plr.Character.HumanoidRootPart.Position.X, plr.Character.HumanoidRootPart.Position.Y, plr.Character.HumanoidRootPart.Position.Z + range)

	local hitboxDepth = hitbox.Size.Z 
	local offset = CFrame.new(0, 0, -((range+1)/ 2)) 
	hitbox.CFrame = plr.Character.HumanoidRootPart.CFrame * offset
	
	local thingtoreturn = workspace:GetPartsInPart(hitbox) 
	local cotine = coroutine.create(function()
		task.wait(0.5)
		hitbox:Destroy()
	end)
	coroutine.resume(cotine)
	return thingtoreturn
end
	
function hitbox.Damage(plr, damage, debounce, sound, hitt, tablehit)
	
	local Hit = hitt
	local character = Hit.Parent
	local hum = Hit.Parent:FindFirstChildOfClass("Humanoid")
	local hit = tablehit
	print("running")
	if table.find(hit, hum) or hum.Health <= 0 or
		hum.Parent.Name == plr.Name 
		--game.Players:GetPlayerFromCharacter(hum.Parent).Name == plr.Name 
	then print("no") return end
			
	if character:HasTag("Shielded") then
		character.HumanoidRootPart.Shield:Destroy()
		character.Highlight.Enabled = false
		damage = 0
		task.wait(0.1)
		character:RemoveTag("Shielded")
	end
	
	local guimod = require(game.ReplicatedStorage.Scripts.GeneralScripts.GUIMod)
	guimod.HitEffects()
	sound:Play()
	table.insert(hit, hum)
	if damage > 0 then
		hum:TakeDamage(damage)
	end
	
	task.wait(debounce)
	print(table.find(hit, hum))
	table.remove(hit, table.find(hit, hum))
	
end	

It cant be, the highest ping I’ve gotten is ~50

So basically, with remote events in Roblox, sending data no matter how much or little, between the server and client always generates latency (lag time). That’s one downside of remote events that I don’t like. If your character was at x position on the client, and you sent a remote event to the server and got the position there, that x position would be at a different position because of a few millisecond delay remotes cause. There are some tricks people have used like custom replication for better positioning on the server like Chrono which basically just updates the current cframe better which is more aligned to the server position. And then another thing which is my personal favorite which may help depending on your game is Velocity Prediction. Basically just take a cframe/position and an assemblyLinearVelocity and/or assemblyAngularVelocity with a multiply tweak and dt mixed with a latency read, and calculate an accurate position based off of the inputted arguments. Most people use 6 as a starting point for predictionTime.

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local PingRemote = Instance.new("RemoteFunction")
PingRemote.Name = "Ping"
PingRemote.Parent = ReplicatedStorage

local function getPlayerLatency(player: Player): number
    local sendTime = os.clock()
    local _ = PingRemote:InvokeClient(player, sendTime)
    local returnTime = os.clock()
    return (returnTime - sendTime) * 0.5
end

local function predictPosition(
    baseCFrame: CFrame,
    velocity: Vector3,
    dt: number,
    latency: number,
    predictionTime: number
): CFrame
    local totalTime = latency + (dt * predictionTime)
    local displacement = velocity * totalTime
    return baseCFrame + displacement
end

return {
    GetPlayerLatency = getPlayerLatency,
    PredictPosition = predictPosition,
}


alright so im 100% sure im doing this wrong `function hitbox.Create(plr, range, height, size)

local hitbox = Instance.new("Part")
local hitPos = require(script.PingDetection)
hitbox.Name = "hitbox ".. plr.Name
hitbox.Size = Vector3.new(size, height, range)
hitbox.CanCollide = false
hitbox.Transparency = 0.5
hitbox.Parent = workspace.Hitboxes
hitbox.Anchored = true
hitbox.Color = Color3.new(1, 0, 0)
hitbox.Material = Enum.Material.ForceField
local hrp = plr.Character.HumanoidRootPart
local increment = 0.03
local offset = 0.5
local baseCFrame = hrp.CFrame
print(baseCFrame)

local pingFactor = hitPos.GetPlayerLatency(plr)
local velocityOffset = hitPos.PredictPosition(hrp, baseCFrame.LookVector, pingFactor, increment, 6)

hitbox.CFrame = baseCFrame * velocityOffset

local thingtoreturn = workspace:GetPartsInPart(hitbox) 
local cotine = coroutine.create(function()
	task.wait(5)
	hitbox:Destroy()
end)
coroutine.resume(cotine)
return thingtoreturn

end`

whenever it runs just stops when getting the ping factor, im sorry if im coming off as needy but im new to this scripting stuff :sob:

no so basically, you want to get the ping factor whenever you want to create a hitbox. Typically, you’d have a hitbox module:

--This would be another MODULE script and put PingDetection in it as a child

local hitPos = require(script.PingDetection)
local RunService = game:GetService("RunService")
local Debris = game:GetService("Debris")

local hitboxModule = {}

function hitboxModule.Create(plr: Player, size: Vector3)
    local hitbox = Instance.new("Part")
    hitbox.Name = "hitbox ".. plr.Name
    hitbox.Size = size
    hitbox.CanCollide = false
    hitbox.Transparency = 0.5
    hitbox.Parent = workspace.Hitboxes
    hitbox.Anchored = true
    hitbox.Color = Color3.new(1, 0, 0)
    hitbox.Material = Enum.Material.ForceField

    local hrp = plr.Character.HumanoidRootPart
    local baseCFrame = hrp.CFrame
    print(baseCFrame)

    local pingFactor = hitPos.GetPlayerLatency(plr)
    local correctedCFrame = hitPos.PredictPosition(baseCFrame, hrp.AssemblyLinearVelocity, RunService.Heartbeat:Wait(), pingFactor, 6)

    hitbox.CFrame = correctedCFrame
    local hitParts = workspace:GetPartsInPart(hitbox)
    
    Debris:AddItem(hitbox, 0.5)
    
    return hitParts
end

Then, In your other script:

local hitboxModule = require(path.to.hitboxModule)

local hitParts = hitboxModule.Create()--Put the player and size of the hitbox

If you’d like a deeper explanation I can help you out or I’ll leave it to you to analyze it :smiley: :+1:

Are you spawning the hitbox on the server?
if yes, let the client handle hitbox positioning(so it stays in sync with the player and feels responsive), then sending that info to the server.
The server can do the actual hit detection / damage + validation to avoid exploits (distance checks, cooldowns, etc.).
This usually fixes the “hitbox appears behind the player” desync issue.

I have no clue whats going on but i copied and pasted it exactly, it’s making the hitbox, but the second it gets to local pingFactor it just stops. No errors, no warnings, nothing. I put a print statement before and after pingFactor is involved and it doesnt run after pingFactor.

Could I see the code perchance?

the code in the module

local hitPos = require(script.PingDetection)
local RunService = game:GetService("RunService")

local hitbox = {}

function hitbox.Create(plr: Player, size: Vector3)
	local hitbox = Instance.new("Part")
	hitbox.Name = "hitbox ".. plr.Name
	hitbox.Size = size
	hitbox.CanCollide = false
	hitbox.Transparency = 0.5
	hitbox.Parent = workspace.Hitboxes
	hitbox.Anchored = true
	hitbox.Color = Color3.new(1, 0, 0)
	hitbox.Material = Enum.Material.ForceField

	local hrp = plr.Character.HumanoidRootPart
	local baseCFrame = hrp.CFrame
	print(baseCFrame)

	local pingFactor = hitPos.GetPlayerLatency(plr)
	local correctedCFrame = hitPos.PredictPosition(baseCFrame, hrp.AssemblyLinearVelocity, RunService.Heartbeat:Wait(), pingFactor, 6)

	hitbox.CFrame = correctedCFrame
	local hitParts = workspace:GetPartsInPart(hitbox)
	local croutine = coroutine.create(function()
		task.wait(0.5)
		hitbox:Destroy()
	end)
	coroutine.resume(croutine)
	return hitParts
end

the code that’s a child of said module

local Pingmodule = {}
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local PingRemote = Instance.new("RemoteFunction")
PingRemote.Name = "Ping"
PingRemote.Parent = ReplicatedStorage

local function getPlayerLatency(player: Player): number
	local sendTime = os.clock()
	local _ = PingRemote:InvokeClient(player, sendTime)
	local returnTime = os.clock()
	return (returnTime - sendTime) * 0.5
end

local function predictPosition(
	baseCFrame: CFrame,
	velocity: Vector3,
	dt: number,
	latency: number,
	predictionTime: number
): CFrame
	local totalTime = latency + (dt * predictionTime)
	local displacement = velocity * totalTime
	return baseCFrame + displacement
end

return {
	GetPlayerLatency = getPlayerLatency,
	PredictPosition = predictPosition,
}

1st module is successfully called, but the second just stops

I was having difficulty making my own hitbox system for a while, and I unfortunately had to completely scrap a design using workspace:GetPartsInPart() (even accounting for ping and delay).

The function workspace:GetPartsInPart() is completely server based, so no matter your ping you will not be able to have it at the exact position of a player. Even if you move the hitbox forward/backward, slowing down quickly will cause the hitbox to move forward according to your previous speed.

I would recommend using .Touched:Connect() as it provides instant client-to-server response time. Also, make sure to add some sort of distance check so exploiters cannot abuse it. Here is an example:

local hitbox = hitboxModule.Create(player, hitboxSize)
local debounceTable = {} -- used to keep track of hit players

hitbox.Touched:Connect(function(hit)
    if (hit.Position - hitbox.Position).Magnitude < hitbox.Size.X * 2 then -- ensure client distance is fair
        if hit.Name == "HumanoidRootPart" and hit.Parent:FindFirstChildOfClass("Humanoid") then
            if not debounceTable[hit] then
                debounceTable[hit] = true -- add target to table

                print("Detected player")

                task.wait(1)
                debounceTable[hit] = nil -- allow target to be hit again
            end
        end
    end
end)

You can combine this with your hitbox module and make it its own function. You can also adjust the hitbox size and hit cooldown to your liking.

Edit: I also recommend using a WeldConstraint for position the hitbox to the player. Each time you create a hitbox, also create a weld constraint that attaches the hitbox the humanoid root part. Also don’t destroy the hitbox right away, give it around 0.4 seconds before removing it or disabling detection.

I would suggest welding as well but .Touched is not reliable. It often slips and fails at detecting objects moving at high speeds.

It isn’t the best detection method, but increasing the lifetime helps limit potential errors. For example, you can wait 0.4 seconds before destroying the hitbox, giving the client time to react to .Touched()

May I see your code for your velocity compensation? Generally, if you’re okay with semi-decent hitboxes, velocity compensation with ping factored in is enough.

My server-sided hitboxes aren’t perfect, but since I wanted to play around with a custom hitbox solution for a game I’m doing work on, I tried this:

  1. Use Suphi Kaner’s Packet module to send a player’s character position 30 times per second to the server (just a fancy remoteEvent that’s slightly more performant, i think?)
  2. A server module, which we’ll call ReplicationModule tracks the positions of every player for the last second, prune any data from beyond a second ago on the server. It stores this data sent to it as a table of {timeItWasSent, CFrameSent}
  3. My hitbox module just calls this other module and gives it the current time in os.clock().
  4. ReplicationModule checks the last 2 entries in its table for that player (i’m using table.insert to put values in the table for each player, so the last 2 entries are the most recent), and it then does a bit of math to figure out the position to interpolate the CFrames in the table to to predict where the player actually is on the server.
  5. My hitboxModule takes this CFrame and then applies the velocity and ping offset to it.

Here’s the math part!
It’s not the cleanest, haha :sweat_smile:

ReplicationService.GetHistoricalCFrame = function(player: Player, snapshot: number)
	local timestampRegistry = cframeRegistry[player]
	--print(timestampRegistry)
	if not timestampRegistry or #timestampRegistry < 2 then
		return
	end
	local latestEntry = timestampRegistry[#timestampRegistry]
	local secondLatestEntry = timestampRegistry[#timestampRegistry - 1]
	
	if snapshot > latestEntry[1] then
		local delta = latestEntry[1] - secondLatestEntry[1]
		
		if delta <= 0 then return latestEntry[2] end
		local extrapolationAlpha = (snapshot - latestEntry[1])/delta
		
		return secondLatestEntry[2]:Lerp(latestEntry[2], 1 + extrapolationAlpha)
	end
	
	return (nil :: any) :: CFrame
end

It worked well enough at 200 walkspeed. It wasn’t perfect, but I hope this gives you an idea on how to handle this issue if you want something a bit more accurate than plain velocity prediction!

For peak asymmetrical horror game speeds for killers, like maybe 30-50, it worked well in my opinion.

Hope you figure this out swiftly!

EDIT: OOPS, I just saw your post where you show the code!
The kicker is probably you invoking the client!

I’m no expert, but I think that yields the script for a while, and is generally bad practice since if the client suddenly disappears, I think the game doesn’t know what to do?

Your hitboxes are likely more delayed than usual due to the ping compensation. Remove the ping compensation and you’ll notice a MASSIVE improvement, and implement the solution above too if you want some slightly smoother hitboxes.

Hello! Sorry but how do i correctly work this into my script? Theres some undefined variables and i have no idea when to use it :sob: . Sorry if this seems pushy or needy idk what im doing.

No problem!

My implementation basically works like this:

  • You create a local script on the client that, every frame, uses Suphi’s Packet module to fire a remote event to the server with the client humanoid root part position. The server just stores it in a table.
RunService.Heartbeat:Connect(function()
	for player, timestampRegistry in cframeRegistry do
		for index = #timestampRegistry, 2, -1 do
			if os.clock() - timestampRegistry[index][1] > 0.5 then
				table.remove(timestampRegistry, index)
				continue
			end
		end
	end
end)

replicationEvent.OnServerEvent:Connect(function(player, updatedCFrame, clientTime)
	-- client time is for later if we want to do math stuff
	cframeRegistry[player] = cframeRegistry[player] or {}
	table.insert(cframeRegistry[player], {os.clock(), updatedCFrame})
end)

You can see the server-side of this here. I receive the cframes and just put them in a table with the current timestamp and cframe.

Every frame on the server I remove entries in the table that are old to save on performance.

Here’s the client side of things, too.

function ReplicationService:Replicate(character)
	if not character then return end
	local HRP = character:FindFirstChild("HumanoidRootPart") :: BasePart
	if not HRP then return end
	ReplicateEvent:Fire(HRP.CFrame)
end

RunService.Heartbeat:Connect(function()
	if os.clock() - lastReplication < replicationInterval then return end
	lastReplication = os.clock()
	ReplicationService:Replicate(character)
end)

Hope this gives you an idea of what I did. I just got the player’s CFrame from this module and then in the main hitbox script just pretended this was the player’s server CFrame when doing all the same calculations you’re already doing.

Also, have you removed the InvokeClient part of your code yet? I know it’s not as great, but using GetNetworkPing is probably a better alternative to risky client invokes.

Here’s the result if you want to see results of this method.

I kind of need to implement security checks to prevent exploiters from sending blatant lies :sweat_smile:

Is this how custom replication implementations like Chrono work? They constantly send client positions to the server? Also, this method would go really good with object pooling using the hitboxes you showed in the video example!

1 Like