I have a simple system that handles lag compensation in my game, but its not quite precise ( ~1 stud of difference ). I was wondering if there was any way to make it more precise,
Here’s a recording of the issue im facing ( green part is the position of another player’s limb on the client, red part is the lag compensated hitbox on the server ):
This is the module that im using ( treat queue variable like a usual table ):
-- services
local Debris = game:GetService("Debris")
local RunService = game:GetService("RunService")
-- define
local queue = require(script.Queue).new(100) -- change the number here to the number of max snapshots
-- consts
local showHitboxes: boolean = true -- do or dont show hitboxes
local characterScaleAffectsHitboxSize: boolean = true -- does character scale affect hitbox sizes
local maxLatency = 0.25 -- this is the maximum value for player's latency
local timeValidityThreshold: number = 0.05 -- maximum difference between assumed player latency and server time while validating time passed by the client
local limbHitboxSizes: {any} = {
["Head"] = Vector3.new(1.2, 1.2, 1.2);
["Torso"] = Vector3.new(2.0, 2.0, 1.0);
["Right Arm"] = Vector3.new(1.0, 2.0, 1.0);
["Left Arm"] = Vector3.new(1.0, 2.0, 1.0);
["Right Leg"] = Vector3.new(1.0, 2.0, 1.0);
["Left Leg"] = Vector3.new(1.0, 2.0, 1.0);
}
local hitboxPartPrefab: Part = Instance.new("Part")
hitboxPartPrefab.CanCollide = false
hitboxPartPrefab.CanTouch = false
hitboxPartPrefab.Anchored = true
hitboxPartPrefab.CastShadow = false
hitboxPartPrefab.Material = Enum.Material.Neon
hitboxPartPrefab.Color = Color3.fromRGB(255,0,0)
local compensator = {}
local CreateHitboxPart = function(
cframe: CFrame,
limbName: string,
lifeTime: number
)
local newPart = hitboxPartPrefab:Clone()
newPart.Size = limbHitboxSizes[limbName] or Vector3.new(1, 1, 1)
newPart.CFrame = cframe or CFrame.identity
newPart.Parent = workspace
newPart.Transparency = if showHitboxes then 0.75 else 1
Debris:AddItem(newPart, lifeTime or 1)
end
-- checks if the time provided by the client is valid depending on player's latency
local CheckTimeValidity = function(
player: Player,
clientTime: number
) : boolean
local playerLatency: number = math.min(player:GetNetworkPing(), maxLatency)
local currentServerTime: number = workspace:GetServerTimeNow()
local assumedClientTime: number = currentServerTime - playerLatency
local difference = math.abs(assumedClientTime - clientTime)
return difference < timeValidityThreshold
end
-- interpolates character cframes between two frames
local Interpolate = function(
snapshot1: {any},
snapshot2: {any},
timeSample: number
) : {CFrame}
assert(snapshot1.SnapshotTime ~= snapshot2.SnapshotTime, "Frames have the same time.")
local first, last
if snapshot1.SnapshotTime < snapshot2.SnapshotTime then
first, last = snapshot1, snapshot2
else
first, last = snapshot2, snapshot1
end
local alpha = (timeSample - first.SnapshotTime) / (last.SnapshotTime - first.SnapshotTime)
local cframeTable = {}
for character: Model, characterCFrameTable: {CFrame} in first.Characters do
for limb: BasePart, limbCFrame: CFrame in characterCFrameTable do
cframeTable[limb] = limbCFrame:Lerp(last.Characters[character][limb], alpha)
end
end
return cframeTable
end
local CalculateCFrameAtTime = function(
timeToRewindTo: number
)
local queueSize: number = queue:GetCurrentSize()
if queueSize == 0 then return end
if queueSize == 1 then return queue:Index(0) end
local left: number, right: number, middle: number = 0, queue:GetCurrentSize() - 1, 0
assert(right - left > 0, "Attempt to interpolate on an empty queue.")
while right - left > 1 do
middle = math.floor((right - left) / 2) + left
local middleFrame = queue:Index(middle)
if middleFrame.SnapshotTime > timeToRewindTo then
right = middle
elseif middleFrame.SnapshotTime < timeToRewindTo then
left = middle
else
left, right = middle, middle
end
end
if left == right then
return queue:Index(left)
else
return Interpolate(
queue:Index(left),
queue:Index(right),
timeToRewindTo
)
end
end
-- creates hitboxes relative to time passed by the client
compensator.CreateHitboxes = function(
player: Player,
sentTime: number
)
-- local isTimeValid: boolean = CheckTimeValidity(player, sentTime)
-- if not isTimeValid then return end
local serverTime = game.Workspace:GetServerTimeNow()
local difference = (serverTime - sentTime)
local cframes: {any} = CalculateCFrameAtTime(serverTime - difference)
for limb: BasePart, limbCFrame: CFrame in cframes do
task.spawn(CreateHitboxPart, limbCFrame, limb.Name, 1)
end
end
-- gets fired each frame after physics calculations
local OnStepped = function(
delta: number
)
local snapshot = {
SnapshotTime = workspace:GetServerTimeNow();
Characters = {};
}
for _, character: Model in workspace.Characters:GetChildren() do
if not character:IsA("Model") then continue end
local characterHumanoid = character:FindFirstChildOfClass("Humanoid")
if not characterHumanoid or characterHumanoid.Health <= 0 then continue end
local limbCFrames: {BasePart} = {}
for limbName: string, _ in limbHitboxSizes do
local findLimb = character:FindFirstChild(limbName)
if not findLimb or not findLimb:IsA("BasePart") then continue end
limbCFrames[findLimb] = findLimb.CFrame
end
snapshot.Characters[character] = limbCFrames
end
queue:Append(snapshot)
end
-- initializer
local Init = function()
RunService.Stepped:Connect(OnStepped)
end
-- inititalize
Init()
return compensator