Tower Defense Game - Troubleshooting Wonky / Glitchy NPC Movement

Have you tried using deltaTime instead of incrementing by a fixed movement? Add a dt parameter to both your HandleMovement functions and have moveDistance multiply to dt rather than the fixed rate of .01.

You would then update dt within the loop.

local lastTime = tick()
while true do
	local currentTime = tick()
	local deltaTime = currentTime - lastTime
	lastTime = currentTime

	HandleMovement(deltaTime)
	task.wait(0.01)
end

I usually use dt to ensure consistency for projectiles, but im sure it applies here as well.

2 Likes

Instead of deltatime I believe I have the [“SyncTime”] = workspace.DistributedGameTime - EnemyData[6] both on the sever and client to sync and handle the movement between them.

1 Like

I see that you didnt use SyncTime. If theyre similar in functionality, I dont see why you cant do the same thing. Init a SyncTime for enemyInfo. Set up a currentTime variable right after declaring HandleMovement and then an elapasedTime variable after checking currentNode. We then update SyncTime when moving nodes.

ServerScript:

function SpawnNPC()
	local EnemyInfo = {
		["EnemyId"] = EnemyId,
		["Node"] = 1,
		["Speed"] = 25,
		["Path"] = math.random(1, 100),
		["CFrame"] = CFrame.lookAt(game.Workspace.Points["1"].Position, game.Workspace.Points["2"].Position),
		["Done"] = false,
		["SyncTime"] = workspace.DistributedGameTime -- Initialized to current server time
	}

	ServerTableOfEnemies[EnemyInfo.EnemyId] = EnemyInfo
	EnemyId = EnemyId + 1

	local ClientEnemyInfo = {
		[1] = EnemyInfo.EnemyId,
		[2] = Vector3int16.new(EnemyInfo.CFrame.X * Precision, EnemyInfo.CFrame.Y * Precision, EnemyInfo.CFrame.Z * Precision),
		[3] = EnemyInfo.Node,
		[4] = EnemyInfo.Speed,
		[5] = EnemyInfo.Path,
		[6] = workspace.DistributedGameTime,
	}

	SpawnedEnemy:FireAllClients(ClientEnemyInfo)
end

-- Actual Movement Handling

function HandleMovement()
	local currentTime = workspace.DistributedGameTime
	
	for i, Enemy in pairs(ServerTableOfEnemies) do
		if Enemy.Done then
			print("Server is Done")
			ServerTableOfEnemies[i] = nil
		else
			local path = tableOfPaths[Enemy.Path]
			if path then
				local currentNode = path[Enemy.Node]
				local nextNode = path[Enemy.Node + 1]

				if currentNode and nextNode then
					local direction = (nextNode.Position - Enemy.CFrame.Position).Unit
					local distanceToNextNode = (nextNode.Position - Enemy.CFrame.Position).Magnitude

					-- Move NPC by a constant speed
					local elapsedTime = currentTime - Enemy.SyncTime -- Calculate time elapsed
					local moveDistance = Enemy.Speed * 0.01
					local newPosition = Enemy.CFrame.Position + (direction * moveDistance)

					-- Smooth transition and rotation
					local targetCFrame = CFrame.lookAt(newPosition, nextNode.Position)
					Enemy.CFrame = Enemy.CFrame:Lerp(targetCFrame, 0.5)

					if (newPosition - nextNode.Position).Magnitude <= moveDistance then
						Enemy.Node = Enemy.Node + 1
						Enemy.SyncTime = currentTime -- Update SyncTime
						
						if Enemy.Node > #path - 1 then
							Enemy.Done = true
						else
							currentNode = path[Enemy.Node]
							nextNode = path[Enemy.Node + 1]
							if currentNode and nextNode then
								Enemy.CFrame = CFrame.new(currentNode.Position, nextNode.Position)
							else
								print("Error: currentNode or nextNode is nil after updating Enemy.Node for Enemy ID:", Enemy.EnemyId)
							end
						end
					end
				else
					print("Error: currentNode or nextNode is nil for Enemy ID:", Enemy.EnemyId)
				end
			else
				print("Error: Path not found for Enemy ID:", Enemy.EnemyId)
			end
		end
	end
end

The only thing that changes in your client is HandleMovement, the EnemyData stays the same

2 Likes

I see, you’re right I didn’t notice I removed the SyncTime parameter from the movement calculation. I’m experiencing another issue now where the client finishes earlier then the server does, do you have an idea why?

Here’s the current code I have:

Server side:

local TweenService = game:GetService("TweenService")

-- Remote Events and Remote Functions
local SpawnedEnemy = game.ReplicatedStorage:WaitForChild("SpawnedEnemy")
local RequestPaths = game.ReplicatedStorage:WaitForChild("RequestPaths")

-- Spline Calculations
local tableOfPaths = {}

function DrawPath()
	local NodesTable = {}
	local tensionMain = math.random(35, 85) / 100 
	local counter = 0	
	local Points = {}

	for i = 1, #game.Workspace.Points:GetChildren() do
		Points[i] = game.Workspace.Points[i].Position
	end

	local NumPoints = #Points
	for i = 1, NumPoints - 1 do
		local p0 = Points[i - 1] or Points[1]
		local p1 = Points[i]
		local p2 = Points[i + 1]
		local p3 = Points[i + 2] or Points[NumPoints]
		for j = 0, 1, 0.05 do
			local tension = tensionMain
			local t = j
			local t2 = t * t
			local t3 = t2 * t

			local p = (
				(-tension * t + 2 * tension * t2 - tension * t3) * p0 +
					(2 + (tension - 6) * t2 + (4 - tension) * t3) * p1 +
					(tension * t - 2 * (tension - 3) * t2 + (tension - 4) * t3) * p2 +
					(-tension * t2 + tension * t3) * p3
			) * 0.5

			counter = counter + 1
			NodesTable[counter] = { Position = p }
		end
	end

	return NodesTable
end

for i = 1, 100 do
	tableOfPaths[i] = DrawPath()
end

function RequestPath()
	return tableOfPaths
end

RequestPaths.OnServerInvoke = RequestPath

task.wait(5)

local Precision = 10^2
local ServerTableOfEnemies = {}
local EnemyId = 1

function SpawnNPC()
	local EnemyInfo = {
		["EnemyId"] = EnemyId,
		["Node"] = 1,
		["Speed"] = 25,
		["Path"] = math.random(1, 100),
		["CFrame"] = CFrame.lookAt(game.Workspace.Points["1"].Position, game.Workspace.Points["2"].Position),
		["Done"] = false,
		["SyncTime"] = workspace.DistributedGameTime -- Initialized to current server time
	}

	ServerTableOfEnemies[EnemyInfo.EnemyId] = EnemyInfo
	EnemyId = EnemyId + 1

	local ClientEnemyInfo = {
		[1] = EnemyInfo.EnemyId,
		[2] = Vector3int16.new(EnemyInfo.CFrame.X * Precision, EnemyInfo.CFrame.Y * Precision, EnemyInfo.CFrame.Z * Precision),
		[3] = EnemyInfo.Node,
		[4] = EnemyInfo.Speed,
		[5] = EnemyInfo.Path,
		[6] = workspace.DistributedGameTime,
	}

	SpawnedEnemy:FireAllClients(ClientEnemyInfo)
end

-- Actual Movement Handling

function HandleMovement()
	local currentTime = workspace.DistributedGameTime

	for i, Enemy in pairs(ServerTableOfEnemies) do
		if Enemy.Done then
			print("Server is Done")
			ServerTableOfEnemies[i] = nil
		else
			local path = tableOfPaths[Enemy.Path]
			if path then
				local currentNode = path[Enemy.Node]
				local nextNode = path[Enemy.Node + 1]

				if currentNode and nextNode then
					local direction = (nextNode.Position - Enemy.CFrame.Position).Unit
					local distanceToNextNode = (nextNode.Position - Enemy.CFrame.Position).Magnitude

					-- Move NPC by a constant speed
					local elapsedTime = currentTime - Enemy.SyncTime -- Calculate time elapsed
					local moveDistance = Enemy.Speed * 0.01
					local newPosition = Enemy.CFrame.Position + (direction * moveDistance)

					-- Smooth transition and rotation
					local targetCFrame = CFrame.lookAt(newPosition, nextNode.Position)
					Enemy.CFrame = Enemy.CFrame:Lerp(targetCFrame, 0.5)

					if (newPosition - nextNode.Position).Magnitude <= moveDistance then
						Enemy.Node = Enemy.Node + 1
						Enemy.SyncTime = currentTime -- Update SyncTime

						if Enemy.Node > #path - 1 then
							Enemy.Done = true
						else
							currentNode = path[Enemy.Node]
							nextNode = path[Enemy.Node + 1]
							if currentNode and nextNode then
								Enemy.CFrame = CFrame.new(currentNode.Position, nextNode.Position)
							else
								print("Error: currentNode or nextNode is nil after updating Enemy.Node for Enemy ID:", Enemy.EnemyId)
							end
						end
					end
				else
					print("Error: currentNode or nextNode is nil for Enemy ID:", Enemy.EnemyId)
				end
			else
				print("Error: Path not found for Enemy ID:", Enemy.EnemyId)
			end
		end
	end
end


-- Main Routine 

local enemyCount = 200

task.spawn(function()
	for _ = 1, enemyCount do
		task.wait(0.1) -- Spawn delay between each unit
		SpawnNPC()
	end
end)

while true do
	task.wait(0.01)
	HandleMovement()
end

Client Side:

local Precision = 10^2

local tableOfPaths = game.ReplicatedStorage.RequestPaths:InvokeServer()
local tableOfEnemies = {}

function Decompile(EnemyData)
	return {
		["EnemyId"] = EnemyData[1],
		["EnemyPosition"] = EnemyData[2],
		["Node"] = EnemyData[3],
		["Speed"] = EnemyData[4],
		["Path"] = tableOfPaths[EnemyData[5]],
		["SyncTime"] = workspace.DistributedGameTime - EnemyData[6],
		["Done"] = false,
	}
end

game.ReplicatedStorage.SpawnedEnemy.OnClientEvent:Connect(function(EnemyInfo)
	local ClientTableEnemyInfo = Decompile(EnemyInfo)
	local key = ClientTableEnemyInfo["EnemyId"]

	tableOfEnemies[key] = ClientTableEnemyInfo

	local CFrameNoOrientation = CFrame.new(ClientTableEnemyInfo["EnemyPosition"].X / Precision, ClientTableEnemyInfo["EnemyPosition"].Y / Precision, ClientTableEnemyInfo["EnemyPosition"].Z / Precision)

	local cloneClientSide = game.ReplicatedStorage.ClientSide:Clone()
	cloneClientSide.Name = ClientTableEnemyInfo["EnemyId"]
	cloneClientSide.Parent = game.Workspace.Mobs
	cloneClientSide.Position = game.Workspace.Points["1"].Position
	cloneClientSide:PivotTo(CFrame.new(cloneClientSide.Position, CFrameNoOrientation.Position))
end)

function HandleMovement()
	for i, Enemy in pairs(tableOfEnemies) do
		local clientTarget = game.Workspace.Mobs[Enemy["EnemyId"]]

		if Enemy.Done then
			print("Client is Done")
			tableOfEnemies[i] = nil

			game.Debris:AddItem(clientTarget, 0)
		else
			local path = Enemy.Path
			if path then
				local currentNode = path[Enemy.Node]
				local nextNode = path[Enemy.Node + 1]

				if currentNode and nextNode then
					local direction = (nextNode.Position - clientTarget.CFrame.Position).Unit
					local distanceToNextNode = (nextNode.Position - clientTarget.CFrame.Position).Magnitude

					-- Move NPC by a constant speed
					local moveDistance = Enemy.Speed * Enemy.SyncTime -- Multiplying by SyncTime
					local newPosition = clientTarget.CFrame.Position + (direction * moveDistance)
					local newOrientation = CFrame.lookAt(newPosition, nextNode.Position)

					clientTarget.CFrame = newOrientation

					if (newPosition - nextNode.Position).Magnitude <= moveDistance then
						Enemy.Node = Enemy.Node + 1
						if Enemy.Node > #path - 1 then
							Enemy.Done = true
						else
							currentNode = path[Enemy.Node]
							nextNode = path[Enemy.Node + 1]
							if currentNode and nextNode then
								clientTarget.CFrame = CFrame.new(currentNode.Position, nextNode.Position)
							else
								print("Error: currentNode or nextNode is nil after updating Enemy.Node for Enemy ID:", Enemy["EnemyId"])
							end
						end
					end
				else
					print("Error: currentNode or nextNode is nil for Enemy ID:", Enemy["EnemyId"])
				end
			else
				print("Error: Path not found for Enemy ID:", Enemy["EnemyId"])
			end
		end
	end
end

while true do
	task.wait(0.01)
	HandleMovement()
end
1 Like

You didnt update the clients HandleMovement, they have to be consistent. Its the same as the server HandleMovement. More specifically, you didnt initialize currentTime and didnt update Enemy’s SyncTime. I actually didnt see you updated it, let me see

function HandleMovement()
	local currentTime = workspace.DistributedGameTime

	for i, Enemy in pairs(ServerTableOfEnemies) do
		if Enemy.Done then
			print("Server is Done")
			ServerTableOfEnemies[i] = nil
		else
			local path = tableOfPaths[Enemy.Path]
			if path then
				local currentNode = path[Enemy.Node]
				local nextNode = path[Enemy.Node + 1]

				if currentNode and nextNode then
					local direction = (nextNode.Position - Enemy.CFrame.Position).Unit
					local distanceToNextNode = (nextNode.Position - Enemy.CFrame.Position).Magnitude

					-- Move NPC by a constant speed
					local elapsedTime = currentTime - Enemy.SyncTime -- Calculate time elapsed
					local moveDistance = Enemy.Speed * 0.01
					local newPosition = Enemy.CFrame.Position + (direction * moveDistance)

					-- Smooth transition and rotation
					local targetCFrame = CFrame.lookAt(newPosition, nextNode.Position)
					Enemy.CFrame = Enemy.CFrame:Lerp(targetCFrame, 0.5)

					if (newPosition - nextNode.Position).Magnitude <= moveDistance then
						Enemy.Node = Enemy.Node + 1
						Enemy.SyncTime = currentTime -- Update SyncTime

						if Enemy.Node > #path - 1 then
							Enemy.Done = true
						else
							currentNode = path[Enemy.Node]
							nextNode = path[Enemy.Node + 1]
							if currentNode and nextNode then
								Enemy.CFrame = CFrame.new(currentNode.Position, nextNode.Position)
							else
								print("Error: currentNode or nextNode is nil after updating Enemy.Node for Enemy ID:", Enemy.EnemyId)
							end
						end
					end
				else
					print("Error: currentNode or nextNode is nil for Enemy ID:", Enemy.EnemyId)
				end
			else
				print("Error: Path not found for Enemy ID:", Enemy.EnemyId)
			end
		end
	end
end
3 Likes

The minimum time task.wait can yield for is the time of 1 frame (~0.015 seconds), so task.wait(0.01) does not yield for exactly or close to 0.01 seconds on the server and on some clients with a capped FPS.

task.wait actually yields for shorter time depending on the client’s framerate, so as a result, for clients running at >60 FPS, the client is calling HandleMovement more often or at a different rate than the server.

You can fix this by passing the return value of task.wait (the elapsed time) into HandleMovement to use as a time step.

while true do
	local dt = task.wait()
	HandleMovement(dt)
end

In HandleMovement, change local moveDistance = Enemy.Speed * 0.01 to local moveDistance = Enemy.Speed * dt

3 Likes

I know this is a different topic but its sorta related, hum:MoveTo() is the best you can do. You should focus on changing it only when you see issues or get bug reports in your game.

Tons of games use hum:MoveTo(), popular example is roblox piggy. it uses hum:MoveTo() and for the most part the NPC movement is smooth, if you really care about smoothness you can play around with client/server or networkownership. But yeah.

1 Like

You’re right, my bad. Also, I noticed that we aren’t actually doing anything with Enemy.SyncTime still? We just store it in the elapsedTime variable and don’t touch it? Do you want me to replace the 0.01 withh the elapsed time variable so we multiply by it?

1 Like

Yeah I see now, thank you. That’s what I thought. But can’t we instead just use SyncTime instead of dt? Wouldn’t it be the same thing? Since we’re still syncing the client with the server elapsed time?

1 Like

SyncTime should be used a single time when the client creates an NPC to move their copy forward along the path by the delta time (in your elapsedTime variable) so that the position is synchronized with the server.

Then, from that position, the client NPC can be simulated entirely locally using the HandleMovement loop. You don’t need SyncTime beyond the NPC creation.

SyncTime is used to calculate the delay between the server and the client (i.e. server-to-client ping), while dt is the time of 1 frame on the machine (client and server). dt is for local simulation and SyncTime for a one-time synchronization on the client.

Wait is what you’re saying different from what @CrossBeast said to do in his example with the ElapsedTime? So I don’t need the ElapsedTime at all in the HandleMovement other then in the SpawnedEnemy Event?

1 Like

If I’m understanding what you’re trying to do, yes, you don’t need ElapsedTime in HandleMovement. Only use it in your SpawnedEnemy event to change the initial position of the NPC so that it matches up with the server.

1 Like

Wouldn’t I have to do some sort of calculation that I am doing in the HandleMovement function in the SpawnedEnemy event to be able to do something with the ElapsedTimeVariable?

For instance

-- Move NPC by a constant speed
					local elapsedTime = currentTime - Enemy.SyncTime -- Calculate time elapsed
					local moveDistance = Enemy.Speed * elapsedTime -- Multiplying by SyncTime
					local newPosition = clientTarget.CFrame.Position + (direction * moveDistance)
					local newOrientation = CFrame.lookAt(newPosition, nextNode.Position)

					clientTarget.CFrame = newOrientation

^ This but in the SpawnedEnemy event?

1 Like

Yeah, you would need to put that stuff in the enemy spawn. Put the calculations into another function (MoveEnemy(enemy, dt)) so that you don’t have to do a big copy and paste and pass in ElapsedTime for dt.

2 Likes

What about the server side code then? Does that mean we don’t need the ElapsedTime Variable there either for any calculations in the HandleMovement() function? Will we just have to pass it once and that’s all?

1 Like

I see why it might not sync, for the client, try removing the subtraction from the SyncTime and make sure that it references the server. So EnemyInfo[6 or7]

2 Likes

Yeah, the server doesn’t need it at all. You don’t have to perform any initial calculations with it either (the server is already synchronized with itself, but the clients are not because of network lag). So the server only simulates with HandleMovement in the while loop.

2 Likes

Alright so I hope I understood all of you, I got pretty confused but this is what I got in the end:

Server Side Code:

local TweenService = game:GetService("TweenService")

-- Remote Events and Remote Functions
local SpawnedEnemy = game.ReplicatedStorage:WaitForChild("SpawnedEnemy")
local RequestPaths = game.ReplicatedStorage:WaitForChild("RequestPaths")

-- Spline Calculations
local tableOfPaths = {}

function DrawPath()
	local NodesTable = {}
	local tensionMain = math.random(35, 85) / 100 
	local counter = 0	
	local Points = {}

	for i = 1, #game.Workspace.Points:GetChildren() do
		Points[i] = game.Workspace.Points[i].Position
	end

	local NumPoints = #Points
	for i = 1, NumPoints - 1 do
		local p0 = Points[i - 1] or Points[1]
		local p1 = Points[i]
		local p2 = Points[i + 1]
		local p3 = Points[i + 2] or Points[NumPoints]
		for j = 0, 1, 0.05 do
			local tension = tensionMain
			local t = j
			local t2 = t * t
			local t3 = t2 * t

			local p = (
				(-tension * t + 2 * tension * t2 - tension * t3) * p0 +
					(2 + (tension - 6) * t2 + (4 - tension) * t3) * p1 +
					(tension * t - 2 * (tension - 3) * t2 + (tension - 4) * t3) * p2 +
					(-tension * t2 + tension * t3) * p3
			) * 0.5

			counter = counter + 1
			NodesTable[counter] = { Position = p }
		end
	end

	return NodesTable
end

for i = 1, 100 do
	tableOfPaths[i] = DrawPath()
end

function RequestPath()
	return tableOfPaths
end

RequestPaths.OnServerInvoke = RequestPath

task.wait(5)

local Precision = 10^2
local ServerTableOfEnemies = {}
local EnemyId = 1

function SpawnNPC()
	local EnemyInfo = {
		["EnemyId"] = EnemyId,
		["Node"] = 1,
		["Speed"] = 25,
		["Path"] = math.random(1, 100),
		["CFrame"] = CFrame.lookAt(game.Workspace.Points["1"].Position, game.Workspace.Points["2"].Position),
		["Done"] = false,
		["SyncTime"] = workspace.DistributedGameTime -- Initialized to current server time
	}

	ServerTableOfEnemies[EnemyInfo.EnemyId] = EnemyInfo
	EnemyId = EnemyId + 1

	local ClientEnemyInfo = {
		[1] = EnemyInfo.EnemyId,
		[2] = Vector3int16.new(EnemyInfo.CFrame.X * Precision, EnemyInfo.CFrame.Y * Precision, EnemyInfo.CFrame.Z * Precision),
		[3] = EnemyInfo.Node,
		[4] = EnemyInfo.Speed,
		[5] = EnemyInfo.Path,
		[6] = workspace.DistributedGameTime,
	}

	SpawnedEnemy:FireAllClients(ClientEnemyInfo)
end

-- Actual Movement Handling

function HandleMovement()
	for i, Enemy in pairs(ServerTableOfEnemies) do
		if Enemy.Done then
			print("Server is Done")
			ServerTableOfEnemies[i] = nil
		else
			local path = tableOfPaths[Enemy.Path]
			
			if path then
				local currentNode = path[Enemy.Node]
				local nextNode = path[Enemy.Node + 1]

				if currentNode and nextNode then
					local direction = (nextNode.Position - Enemy.CFrame.Position).Unit
					local distanceToNextNode = (nextNode.Position - Enemy.CFrame.Position).Magnitude

					-- Move NPC by a constant speed
					local moveDistance = Enemy.Speed * 0.01 -- Multiplying by SyncTime
					local newPosition = Enemy.CFrame.Position + (direction * moveDistance)

					-- Smooth transition and rotation
					local targetCFrame = CFrame.lookAt(newPosition, nextNode.Position)
					Enemy.CFrame = Enemy.CFrame:Lerp(targetCFrame, 0.5)

					if (newPosition - nextNode.Position).Magnitude <= moveDistance then
						Enemy.Node = Enemy.Node + 1

						if Enemy.Node > #path - 1 then
							Enemy.Done = true
						else
							currentNode = path[Enemy.Node]
							nextNode = path[Enemy.Node + 1]
							if currentNode and nextNode then
								Enemy.CFrame = CFrame.new(currentNode.Position, nextNode.Position)
							else
								print("Error: currentNode or nextNode is nil after updating Enemy.Node for Enemy ID:", Enemy.EnemyId)
							end
						end
					end
				else
					print("Error: currentNode or nextNode is nil for Enemy ID:", Enemy.EnemyId)
				end
			else
				print("Error: Path not found for Enemy ID:", Enemy.EnemyId)
			end
		end
	end
end

-- Main Routine 

local enemyCount = 200

task.spawn(function()
	for _ = 1, enemyCount do
		task.wait(0.1) -- Spawn delay between each unit
		SpawnNPC()
	end
end)

while true do
	task.wait(0.015)
	HandleMovement()
end

Client Side Code:

local Precision = 10^2

local tableOfPaths = game.ReplicatedStorage:WaitForChild("RequestPaths"):InvokeServer()
local tableOfEnemies = {}

function SyncMoveEnemy(EnemyPart, EnemyData)
	local currentTime = workspace.DistributedGameTime
	
	local path = EnemyData.Path

	if path then
		local currentNode = path[EnemyData.Node]
		local nextNode = path[EnemyData.Node + 1]

		if currentNode and nextNode then
			local direction = (nextNode.Position - EnemyPart.CFrame.Position).Unit
			local distanceToNextNode = (nextNode.Position - EnemyPart.CFrame.Position).Magnitude

			-- Move NPC by a constant speed
			local elapsedTime = currentTime - EnemyData.SyncTime -- Calculate time elapsed
			local moveDistance = EnemyData.Speed * elapsedTime -- Multiplying by SyncTime
			local newPosition = EnemyPart.CFrame.Position + (direction * moveDistance)
			local newOrientation = CFrame.lookAt(newPosition, nextNode.Position)

			EnemyPart.CFrame = newOrientation

			if (newPosition - nextNode.Position).Magnitude <= moveDistance then
				EnemyData.Node = EnemyData.Node + 1
				if EnemyData.Node > #path - 1 then
					EnemyData.Done = true
				else
					currentNode = path[EnemyData.Node]
					nextNode = path[EnemyData.Node + 1]
					if currentNode and nextNode then
						EnemyPart.CFrame = CFrame.new(currentNode.Position, nextNode.Position)
					else
						print("Error: currentNode or nextNode is nil after updating Enemy.Node for Enemy ID:", EnemyData["EnemyId"])
					end
				end
			end
		else
			print("Error: currentNode or nextNode is nil for Enemy ID:", EnemyData["EnemyId"])
		end
	else
		print("Error: Path not found for Enemy ID:", EnemyData["EnemyId"])
	end
end

function Decompile(EnemyData)
	return {
		["EnemyId"] = EnemyData[1],
		["EnemyPosition"] = EnemyData[2],
		["Node"] = EnemyData[3],
		["Speed"] = EnemyData[4],
		["Path"] = tableOfPaths[EnemyData[5]],
		["SyncTime"] = EnemyData[6],
		["Done"] = false,
	}
end

game.ReplicatedStorage.SpawnedEnemy.OnClientEvent:Connect(function(EnemyData)
	local ClientTableEnemyInfo = Decompile(EnemyData)
	local EnemyId = ClientTableEnemyInfo["EnemyId"]

	tableOfEnemies[EnemyId] = ClientTableEnemyInfo

	local CFrameNoOrientation = CFrame.new(ClientTableEnemyInfo["EnemyPosition"].X / Precision, ClientTableEnemyInfo["EnemyPosition"].Y / Precision, ClientTableEnemyInfo["EnemyPosition"].Z / Precision)

	local cloneClientSide = game.ReplicatedStorage.ClientSide:Clone()
	
	cloneClientSide.Name = ClientTableEnemyInfo["EnemyId"]
	cloneClientSide.Parent = game.Workspace.Mobs
	cloneClientSide.Position = game.Workspace.Points["1"].Position
	cloneClientSide:PivotTo(CFrame.new(cloneClientSide.Position, CFrameNoOrientation.Position))

	SyncMoveEnemy(cloneClientSide, ClientTableEnemyInfo)
end)

function HandleMovement()
	for i, Enemy in pairs(tableOfEnemies) do
		local clientTarget = game.Workspace.Mobs[Enemy["EnemyId"]]

		if Enemy.Done then
			print("Client is Done") 
			tableOfEnemies[i] = nil

			game.Debris:AddItem(clientTarget, 0)
		else
			local path = Enemy.Path

			if path then
				local currentNode = path[Enemy.Node]
				local nextNode = path[Enemy.Node + 1]

				if currentNode and nextNode then
					local direction = (nextNode.Position - clientTarget.CFrame.Position).Unit
					local distanceToNextNode = (nextNode.Position - clientTarget.CFrame.Position).Magnitude

					-- Move NPC by a constant speed
					local moveDistance = Enemy.Speed * 0.01 -- Multiplying by SyncTime
					local newPosition = clientTarget.CFrame.Position + (direction * moveDistance)

					-- Smooth transition and rotation
					local targetCFrame = CFrame.lookAt(newPosition, nextNode.Position)
					clientTarget.CFrame = clientTarget.CFrame:Lerp(targetCFrame, 0.5)

					if (newPosition - nextNode.Position).Magnitude <= moveDistance then
						Enemy.Node = Enemy.Node + 1

						if Enemy.Node > #path - 1 then
							Enemy.Done = true
						else
							currentNode = path[Enemy.Node]
							nextNode = path[Enemy.Node + 1]
							if currentNode and nextNode then
								clientTarget.CFrame = CFrame.new(currentNode.Position, nextNode.Position)
							else
								print("Error: currentNode or nextNode is nil after updating Enemy.Node for Enemy ID:", Enemy.EnemyId)
							end
						end
					end
				else
					print("Error: currentNode or nextNode is nil for Enemy ID:", Enemy.EnemyId)
				end
			else
				print("Error: Path not found for Enemy ID:", Enemy.EnemyId)
			end
		end
	end
end

while true do
	task.wait(0.01)
	HandleMovement()
end

Now I’m still having an issue, the client seems to finish before the server does.

It’s weird because the HandleMovement() function is pretty much identical for the Server Side and the Client Side. Any ideas?

1 Like

From your screenshot, it seems they are finishing at pretty much the same time.

If that’s still an issue, did you see my reply here:

2 Likes

So I tried doing that:

Server side and Client Side:

while true do
	local dt = task.wait()
	HandleMovement(dt)
end

and

					-- Move NPC by a constant speed
					local moveDistance = Enemy.Speed * dt -- Multiplying by SyncTime
					local newPosition = clientTarget.CFrame.Position + (direction * moveDistance)

But the results I’m getting are different depending on the speed of the units.

I can’t really tell if it’s really synced well, what do you think?

Well other then that, there’s also another issue, whenever the units pass around a corner on the path, they slow down. Then speed back up.