Lag_Compensation_Test.rbxl (76.5 KB)
I made a server script for a PvE beat/shoot em up that makes a log of the CFrame of every enemy hurtbox for the last 0.5 second and then sets that hurtbox for the enemy to the CFrame that the player would see it at from their perspective. That way, when the player fires a bullet or throws a punch, the server can use the folder containing the hurtboxes specifically lag compensated for the player in order to see if the player actually hit the enemy or not.
The main reason why I did this is because I really didn’t want to have the client determine whether they hit an enemy or not, because of the risk of exploits.
This is the main server script
local Workspace = game:GetService("Workspace")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerStorage = game:GetService("ServerStorage")
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local EnemiesFolder = Workspace.Environment.Enemies
local LagCompensatedHurtBoxFolder = Workspace.Environment.LagCompensatedHurtBoxes
local PlayerDelayUpdate = ReplicatedStorage.Remotes.PlayerDelayUpdate
local GetEnemyInstanceFromEnemyID = ServerStorage.Bindables.GetEnemyInstanceFromEnemyID
local EnemyHurtBoxTracker = {}
-- SETTINGS --
local MAX_DELAY = 0.5
-----
local uniqueID = 0
local function GenerateID()
uniqueID += 1
return uniqueID
end
local function GetLagCompensatedHurtBoxCFrameFromEnemyID(enemyID: string, clientTime: number)
if not EnemyHurtBoxTracker[enemyID] then warn("EnemyID does not appear on tracker") return end
local result = {}
for hurtBoxName, hurtBoxInfo in pairs(EnemyHurtBoxTracker[enemyID]) do
-- print(hurtBoxInfo)
-- IMPORTANT: the following for loop has a questionable edge case, where the table referring the the enemy is changed in the middle of the for loop,
-- but since the times go from right to left, I think that doing a for loop from right to left is fine.
for i = #hurtBoxInfo , 1, -1 do
if #hurtBoxInfo[i] < 2 then
-- we check if the player delay is so small that the "end" part of the relevant log entry isn't there yet
if hurtBoxInfo[i][1].TimeStamp <= clientTime then
local hurtBoxCurrentCFrame = GetEnemyInstanceFromEnemyID:Invoke(enemyID).HurtBoxes:FindFirstChild(hurtBoxName).CFrame
result[hurtBoxName] = hurtBoxInfo[i][1].CFrame:Lerp(hurtBoxCurrentCFrame, (clientTime - hurtBoxInfo[i][1].TimeStamp)/(os.clock() - hurtBoxInfo[i][1].TimeStamp))
break
end
end
if hurtBoxInfo[i][1].TimeStamp <= clientTime and clientTime <= hurtBoxInfo[i][2].TimeStamp then
-- print("MAX_DELAY not exceeded".. os.clock()-clientTime)
result[hurtBoxName] = hurtBoxInfo[i][1].CFrame:Lerp(hurtBoxInfo[i][2].CFrame, (clientTime - hurtBoxInfo[i][1].TimeStamp)/(hurtBoxInfo[i][2].TimeStamp - hurtBoxInfo[i][1].TimeStamp))
break
elseif i == 1 then
-- print("MAX_DELAY exceeded".. os.clock()-clientTime)
-- print(clientTime)
result[hurtBoxName] = hurtBoxInfo[i][1].CFrame
end
end
end
return result
end
Players.PlayerAdded:Connect(function(plr)
-- unless a player can disconnect and reconnect insanely fast, I don't think that this will be triggered unless something actually goes wrong
if LagCompensatedHurtBoxFolder:FindFirstChild(tostring(plr.UserId)) then warn("FATAL ERROR: folder for the player compensated hitbox already exists") return end
local PlayerCompensatedHurtBoxFolder = Instance.new("Folder")
PlayerCompensatedHurtBoxFolder.Name = tostring(plr.UserId)
PlayerCompensatedHurtBoxFolder:SetAttribute("Delay", 0)
PlayerCompensatedHurtBoxFolder.Parent = LagCompensatedHurtBoxFolder
local playerDelay = PlayerCompensatedHurtBoxFolder:GetAttribute("Delay")
PlayerCompensatedHurtBoxFolder.AttributeChanged:Connect(function(attribute)
if attribute ~= "Delay" then return end
playerDelay = PlayerCompensatedHurtBoxFolder:GetAttribute("Delay")
end)
while LagCompensatedHurtBoxFolder:FindFirstChild(tostring(plr.UserId)) do
for enemyID, enemyInfo in pairs(EnemyHurtBoxTracker) do
-- the following if statements adds the hitboxes in if it wasn't there before
if not PlayerCompensatedHurtBoxFolder:FindFirstChild(enemyID) then
local PlayerCompensatedEnemyFolder = Instance.new("Folder")
PlayerCompensatedEnemyFolder.Name = enemyID
for name, hurtBoxInfo in enemyInfo do
local HurtBoxPart = Instance.new("Part")
-- assign the required properties to the part
HurtBoxPart.Color = Color3.new(0, 1, 0)
HurtBoxPart.Transparency = 0.75
HurtBoxPart.Anchored = true
HurtBoxPart.CanCollide = false
HurtBoxPart.CanTouch = false
HurtBoxPart.Name = name
HurtBoxPart.Size = hurtBoxInfo.Size
HurtBoxPart.Parent = PlayerCompensatedEnemyFolder
end
PlayerCompensatedEnemyFolder.Parent = PlayerCompensatedHurtBoxFolder
end
local hurtBoxCFrames = GetLagCompensatedHurtBoxCFrameFromEnemyID(enemyID, os.clock() - playerDelay)
for hurtBoxName, hurtBoxCFrame in pairs(hurtBoxCFrames) do
PlayerCompensatedHurtBoxFolder:FindFirstChild(enemyID):FindFirstChild(hurtBoxName).CFrame = hurtBoxCFrame
end
end
task.wait()
end
end)
Players.PlayerRemoving:Connect(function(plr)
if LagCompensatedHurtBoxFolder:FindFirstChild(tostring(plr.UserId)) then
LagCompensatedHurtBoxFolder:FindFirstChild(tostring(plr.UserId)):Destroy()
end
end)
PlayerDelayUpdate.OnServerEvent:Connect(function(plr, timeStamp)
if not LagCompensatedHurtBoxFolder:FindFirstChild(tostring(plr.UserId)) then return end
local delayValue = os.clock() - timeStamp
-- print(plr.Name.." has delay of "..delayValue)
LagCompensatedHurtBoxFolder:FindFirstChild(tostring(plr.UserId)):SetAttribute("Delay", delayValue)
end)
-- I'm gonna do this just for enemies for now, I don't see a good reason to do the same for players in a PvE game, unless I make something like a TF2 Medic Crusader Crossbow support weapon
local function AddEnemyToTracker(enemy:Model)
if enemy:IsA("Model") and enemy:FindFirstChildOfClass("Humanoid") then
local RootPart: BasePart= enemy.HumanoidRootPart
if not enemy:GetAttribute("EnemyID") then
enemy:SetAttribute("EnemyID", GenerateID())
end
EnemyHurtBoxTracker[tostring(enemy:GetAttribute("EnemyID"))] = {}
print("added enemy to enemy tracker")
for _, hurtBox: BasePart in pairs(enemy.HurtBoxes:GetChildren()) do
EnemyHurtBoxTracker[tostring(enemy:GetAttribute("EnemyID"))][hurtBox.Name] = {Size = hurtBox.Size}
spawn(function()
local lastCheckedTime = os.clock()
local PartTrackingInfo = EnemyHurtBoxTracker[tostring(enemy:GetAttribute("EnemyID"))][hurtBox.Name]
local TrackedPart = hurtBox
while PartTrackingInfo do
table.insert(PartTrackingInfo, {{TimeStamp = lastCheckedTime, CFrame = TrackedPart.CFrame}})
task.wait(0.05)
lastCheckedTime = os.clock()
PartTrackingInfo[#PartTrackingInfo][2] = {TimeStamp = lastCheckedTime, CFrame = TrackedPart.CFrame}
-- remove old entries whose time stamps end at `MAX_DELAY` seconds before current time
while PartTrackingInfo[1][2].TimeStamp < lastCheckedTime - MAX_DELAY do
table.remove(PartTrackingInfo, 1)
end
end
end)
end
end
end
local function RemoveEnemyFromTracker(enemy)
if enemy:IsA("Model") and enemy:FindFirstChildOfClass("Humanoid") then
EnemyHurtBoxTracker[tostring(enemy:GetAttribute("EnemyID"))] = nil -- I'm actually kinda surprised that this works considering that the child is destroyed and all
for _, playerFolder:Folder in pairs(LagCompensatedHurtBoxFolder:GetChildren()) do
if playerFolder:FindFirstChild(tostring(enemy:GetAttribute("EnemyID"))) then
playerFolder:FindFirstChild(tostring(enemy:GetAttribute("EnemyID"))):Destroy()
end
end
end
end
EnemiesFolder.ChildAdded:Connect(AddEnemyToTracker)
EnemiesFolder.ChildRemoved:Connect(RemoveEnemyFromTracker)
for _,enemy:Instance in pairs(EnemiesFolder:GetChildren()) do
print(enemy.ClassName)
AddEnemyToTracker(enemy)
end
--[[ This is just for debugging
while task.wait(0.1) do
print(os.clock())
print(EnemyHurtBoxTracker)
for _, player in pairs(Players:GetPlayers()) do
-- print(player.Name.." ping is: ".. player:GetNetworkPing())
end
end
]]--
So these are my following questions:
Is there a more optimized way of doing this? When i was running this test on my laptop it really struggled as soon as more than 5 enemies were in the game world.
I don’t actually need the client to keep track of were these hurtboxes are especially for other players, since only the server calculates the collisions and the hurtboxes on the client will be inherently accurate. Therefore can I just delete the “LagCompensatedHurtBoxes” folder in the client in order to save performance on the client?
Does this actually work, aka, would the hurtboxes made in the server accurately represent what the player is seeing from their end? I’m still rather new to scripting, so I don’t know if roblox has lag compensation built in to the networking.
Edit: I also made a local script for in the client so that the client can send its current time and then the server uses that to determine the delay, the reason for that is because I don’t have a clue how exactly :GetNetworkPing works.
Edit 2: It turns out, that what was causing my bad performance was because I was printing out a ton of stuff every tenth of a second, after removing that while loop at the end of the LagCompensationScript, I could run 32 enemies with no issue.