I currently have client sends request → server validates
The currenct code tries to put the hitbox where the player WAS when they sent the request,
local ServerPosition = hrp.CFrame
local ClientPosition = data.position
local serverPos = ServerPosition.Position
local clientPos = ClientPosition.Position
local MaxDist = ClientAccuracy*100
local offset = clientPos-serverPos
local distance = offset.Magnitude
if distance > MaxDist then
clientPos= serverPos + offset.Unit * MaxDist
end
local clamped = CFrame.new(clientPos) * ClientPosition.Rotation
I have NO IDEA if this is good or bad, to me it feels good (with 150 ms simulated ping) but at the same time there i have NO idea if this is a good way to do hitboxes.
PLEASE if someone knows how it should be done, give me the basic idea. Ive been fighting this loop of thought and i need someone else to break it.
Your current approach is okay as a basic sanity check, but it is not true lag compensation.
Clamping the client position only limits how far the client can move the hitbox from the server’s current position. It does not prove that the player was actually there when the attack happened, and the client can still send a fake position or rotation.
A more reliable setup is:
The client sends the attack timestamp and attack data.
The server stores a short history of each character’s positions.
The server rewinds to the attacker’s position at that timestamp.
The server creates or checks the hitbox from the rewound position.
The server validates cooldowns, attack range, state, and timestamp limits.
The server should remain authoritative. The client can tell the server when an attack was started, but it should not be allowed to decide the final hitbox position.
Your clamp can still be used as an additional validation layer, but I would also:
reject timestamps that are too old
validate the attack rotation
validate the player’s current state and cooldown
limit how much rewind is allowed
avoid trusting a full client-provided CFrame
For a simpler system, you can let the server use the player’s current position and slightly enlarge the hitbox based on ping. For a more accurate system, use server-side position history and rewind.
Every frame im now calculating what position the player was:
local runConn = RunService.Heartbeat:Connect(function(dt)
local char = plr.Character
if not char then return end
local hrp = char:FindFirstChild("HumanoidRootPart")
if not hrp then return end
local currentTime = workspace:GetServerTimeNow()
local history = PlayerPositionRollback[plr.UserId]
if history then
table.insert(history, {Time = currentTime, CFrame = hrp.CFrame})
while #history > 0 and (currentTime - history[1].Time) > MaxHistoryAge do
table.remove(history, 1)
end
end
end)
Then when the client makes a request, we check the rollback.
function RewindUser(userId, ping)
local history = PlayerPositionRollback[userId]
if not history or #history == 0 then return nil end
-- I did abit of research earlier and this made it feel better somehow
local Buffer = 0.05
local targetTime = workspace:GetServerTimeNow() - (ping/2) + Buffer
-- refuse the client from going to far back
if targetTime <= history[1].Time then
return history[1].CFrame
end
if targetTime >= history[#history].Time then
return history[#history].CFrame
end
-- merge two frames together to make it feel better i guess
for i = 1, #history - 1 do
-- ..merge
end
-- fallback to latest frame
return history[#history].CFrame
end
Im not sure if this is the correct idea, but i think it is good
Your clamp is fine as an additional sanity check, but it is not real lag compensation. You are comparing the client position against the server’s current position, not the position the player had when the request was sent. You are also fully trusting the client-provided rotation.
Ideally, the server should store a short position history, validate the client timestamp, rewind to the appropriate server-recorded position, and create the hitbox itself. Keep the server authoritative and use the client position only as a hint, not as the final source of truth.