Hello there!
I’ve been trying to make a server sided movement validation system for this game i’m making (and I’m fairly a beginner when it comes to anti cheats), if you’re a familiar with Battle Cats then you will understand what i’m trying to do.
Due to the chaotic nature of this game, I wanted to handle hitboxes on the client for performance.
Since the troops move in a straight line it makes the synchronization fairly simple, this is what i achieved so far
- Both the client and server run the same movement logic starting from a shared
startTime. - The client sends its position to the server every 0.2 seconds, which the server compares against its internal simulation.
- On average, this gives extremely tight sync, with only ~0.001 studs of difference.
Everything works fine until a troop stops moving to attack (e.g. detects a target in range) but to keep in mind:
- The server tracks all troops and checks for interactions using
Magnitudeand range values every frame. - When a troop stops, both client and server start an attack cycle, respecting foreswing, backswing, and total cooldown timing using separate threads.
- After each attack (exactly after the backswing), the troop resumes movement.
Here an example of the issue
Despite both sides following the same logic, each time the troop stops and resumes, the client becomes slightly more desynced from the server.
The more times it stops and resumes, the worse the desync becomes.
Here’s the part of the code that handles the server side simulation, there are some band-aid solutions for a few minimal things and an overall mess I have to improve so don’t mind that too much.
function StartTroopValidation(player: Player, id: number, guid: string, startingPos: Vector3, troopType: string, startTime: number)
local data = troopData[troopType][id]
local position = startingPos
local direction = (troopType == "Enemy") and Vector3.new(1, 0, 0) or Vector3.new(-1, 0, 0)
-- initialize troop for simulation
local troopInfo = {
TotalElapsedTime = os.clock() - startTime,
StartingPos = startingPos,
Position = startingPos,
Health = data.Stats.Health,
Data = data,
Type = troopType,
Direction = direction,
OnCooldown = false,
OnBackswing = false
}
reportedTroops[player] = reportedTroops[player] or {}
reportedTroops[player][guid] = troopInfo
local conn
conn = RunService.Heartbeat:Connect(function(deltaTime: number)
local troopInfo = reportedTroops[player] and reportedTroops[player][guid]
if not troopInfo then
conn:Disconnect()
return
end
if troopInfo.OnBackswing then return end
local detected = false
-- checks for troops ahead
for otherId, other in pairs(reportedTroops[player]) do
if otherId ~= guid and other.Type ~= troopInfo.Type then
local distance = math.abs(troopInfo.Position.X - other.Position.X)
if distance <= troopInfo.Data.Stats.Range then
detected = true
break
end
end
end
if detected then
if not troopInfo.OnCooldown then
local attackCycle = troopInfo.Data.AttackCycle
--thread to handle the attack cycle if it detects another troop
task.spawn(function()
task.wait(attackCycle.Foreswing)
if not reportedTroops[player] or not reportedTroops[player][guid] then return end
reportedTroops[player][guid].OnBackswing = true
task.wait(attackCycle.Backswing)
if reportedTroops[player] and reportedTroops[player][guid] then
reportedTroops[player][guid].OnCooldown = true
reportedTroops[player][guid].OnBackswing = false
local TBA = math.max(attackCycle.TBA, attackCycle.Foreswing + attackCycle.Backswing)
local cooldownDuration = TBA - (attackCycle.Foreswing + attackCycle.Backswing)
task.delay(cooldownDuration, function()
if reportedTroops[player] and reportedTroops[player][guid] then
reportedTroops[player][guid].OnCooldown = false
end
end)
end
end)
end
else
troopInfo.TotalElapsedTime += deltaTime
troopInfo.Position = troopInfo.StartingPos + troopInfo.Direction * troopInfo.Data.Stats.Speed * troopInfo.TotalElapsedTime
end
reportedTroops[player][guid] = troopInfo
end)
end
So questions
- Is there something I might be missing or doing wrong when pausing/resuming movement?
- Has anyone attempted a similar server-authoritative validation system for movement, especially in unit-based games like this?
- Is there a more reliable approach for syncing these state transitions (stop/resume/attack) accurately?
Any advice or shared experience would be extremely helpful!
Thanks in advance ![]()