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?
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?
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.
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?
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.
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?
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]
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.
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?
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:
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.
Are you sure that the different finish times are even a problem? Print the enemy ID and the DistributedGameTime along with each done message because it’s impossible to tell what’s happening in the output.
When you move the NPCs to a corner, you do not consider that the distance from the corner to the NPC may be shorter than the distance that they were supposed to move. To fix this, you have to move the NPC an extra amount after turning the corner, equal to the total distance minus the distance to the corner.
Alright so, for the different finish times, here’s what we have:
It’s also pretty noticeable that when the server prints that it’s done you still see a client walking the path.
Also, about the distance to corner, can you show me an example in code regarding what you mean exactly? I mean, currently the issue I’m facing is that all the NPCs take different times to finish a lap although all of them are walking the same path just with different pre-generated nodes of the spline function along the path with different tension.
I think it’ll be fine as long as the server finishes first right?
The desync looks like it’s more then 500 ms, I was thinking also switching to tween service instead of lerp, could that make the movemnet smoother?
I figured I can’t run Tween for a table value so is there a way to make my NPC movement smooth like a tween but by only using Lerp?
Perhaps it has something to do with my HandleMovement() function?
Client Code:
function HandleMovement(dt)
for i, Enemy in pairs(tableOfEnemies) do
local clientTarget = game.Workspace.Mobs[Enemy["EnemyId"]]
if Enemy.Done then
print("Client is Done ", game.Workspace.DistributedGameTime)
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 * dt -- 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, 1)
print("Magnitude: ",((newPosition - nextNode.Position).Magnitude))
print("moveDistance: ",moveDistance)
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
I tried looking at the magnitude checks and usually almost this statement is true:
if (newPosition - nextNode.Position).Magnitude <= moveDistance then
Which means that this happens:
clientTarget.CFrame = CFrame.new(currentNode.Position, nextNode.Position)
Which makes the NPC look as though it teleported, which isn’t good. Any ideas regarding what I could do to eliminate this problem? Maybe this is what’s causing this weird looking NPC movement
Here’s also a snippet of the distance checks:
Here’s the most recent code:
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(75, 100) / 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"] = 11,
["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(dt)
for i, Enemy in pairs(ServerTableOfEnemies) do
if Enemy.Done then
print("Server is Done ", game.Workspace.DistributedGameTime)
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 * dt -- 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, 1)
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 = 1
task.spawn(function()
for _ = 1, enemyCount do
task.wait(0.1) -- Spawn delay between each unit
SpawnNPC()
end
end)
while true do
local dt = task.wait()
HandleMovement(dt)
end
Client Side Code:
local Precision = 10^2
local function Lerp(a, b, alpha)
return math.clamp(a + ((b - a) * alpha), -500, 500)
end
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(dt)
for i, Enemy in pairs(tableOfEnemies) do
local clientTarget = game.Workspace.Mobs[Enemy["EnemyId"]]
if Enemy.Done then
print("Client is Done ", game.Workspace.DistributedGameTime)
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 * dt -- 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, 1)
print("Magnitude: ",((newPosition - nextNode.Position).Magnitude))
print("moveDistance: ",moveDistance)
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
local dt = task.wait()
HandleMovement(dt)
end