# Tower Defense Game | NPC Bugged Units Spin 360 as they follow the Path

1. What do you want to achieve?:

I’d like to figure out why my enemy NPC units which follow the path in my Tower Defense game spin 360 degrees and or look at the node behind them / act buggy.

1. What is the issue?:

It’s spinning weirdly as it moves up the path, perhaps it looks back at previous nodes or the wrong nodes? Keeping the wrong track of nodes travelled?

https://i.imgur.com/qIO6xow.gif

1. What solutions have you tried so far?:

I tried adding the correct node / next node the enemy is supposed to travel to, distance checks, direction checks, but I can’t seem to figure it out.

After that, you should include more details if you have any. Try to make your topic as descriptive as possible, so that it’s easier for people to help you!

Here’s the 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

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

function SpawnNPC()
local EnemyInfo = {
["EnemyId"] = EnemyId,
["Node"] = 1,
["Speed"] = 11, -- Constant speed
["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

-- Function to handle NPC movement
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]
local nodeAfterNext = path[Enemy.Node + 2]

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

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

-- Smooth transition and rotation
if distanceToNextNode > 0.01 then -- Check for small movements to avoid unstable rotations
local targetCFrame = CFrame.lookAt(newPosition, nextNode.Position)
Enemy.CFrame = Enemy.CFrame:Lerp(targetCFrame, 1) -- Use alpha = 1 for smooth transitions
end

-- Calculate how much distance remains after reaching the next node
local remainingDistance = moveDistance - distanceToNextNode

-- Check if the NPC has moved past the next node
if remainingDistance > 0 and nodeAfterNext then
local distanceToNodeAfterNext = (nodeAfterNext.Position - nextNode.Position).Magnitude

if remainingDistance >= distanceToNodeAfterNext then
-- Move to node after next
remainingDistance = remainingDistance - distanceToNodeAfterNext
Enemy.Node = Enemy.Node + 2
if path[Enemy.Node] and path[Enemy.Node + 1] then
currentNode = path[Enemy.Node]
nextNode = path[Enemy.Node + 1]
Enemy.CFrame = CFrame.new(currentNode.Position)
print("Server: Moved to node after next: " .. Enemy.Node)
else
Enemy.Done = true
end
else
-- Move to next node
Enemy.Node = Enemy.Node + 1
if path[Enemy.Node] and path[Enemy.Node + 1] then
currentNode = path[Enemy.Node]
nextNode = path[Enemy.Node + 1]
Enemy.CFrame = CFrame.new(currentNode.Position + (nextNode.Position - currentNode.Position).Unit * remainingDistance)
print("Server: Moved to next node: " .. Enemy.Node)
else
Enemy.Done = true
end
end
elseif remainingDistance > 0 then
-- Move to next node
Enemy.Node = Enemy.Node + 1
if path[Enemy.Node] and path[Enemy.Node + 1] then
currentNode = path[Enemy.Node]
nextNode = path[Enemy.Node + 1]
Enemy.CFrame = CFrame.new(currentNode.Position + (nextNode.Position - currentNode.Position).Unit * remainingDistance)
print("Server: Moved to next node: " .. Enemy.Node)
else
Enemy.Done = true
end
end

-- Debug information
print("Server: Enemy ID: " .. Enemy.EnemyId .. " Position: " .. tostring(Enemy.CFrame.Position) .. " Node: " .. Enemy.Node)
else
print("Server: Error: currentNode or nextNode is nil for Enemy ID:", Enemy.EnemyId)
end
else
end
end
end
end

-- Main Routine

local enemyCount = 1

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

while true do
HandleMovement(dt)
end

``````

Here’s the 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 = {}

local 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]
local nodeAfterNext = path[EnemyData.Node + 2]

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

-- Move NPC by 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

-- Calculate how much distance remains after reaching the next node
local remainingDistance = moveDistance - distanceToNextNode

-- Check if the NPC has moved past the next node
if remainingDistance > 0 and nodeAfterNext then
local distanceToNodeAfterNext = (nodeAfterNext.Position - nextNode.Position).Magnitude

if remainingDistance >= distanceToNodeAfterNext then
-- Move to node after next
remainingDistance = remainingDistance - distanceToNodeAfterNext
EnemyData.Node = EnemyData.Node + 2
if path[EnemyData.Node] and path[EnemyData.Node + 1] then
currentNode = path[EnemyData.Node]
nextNode = path[EnemyData.Node + 1]
EnemyPart.CFrame = CFrame.new(currentNode.Position)
print("Client: Moved to node after next: " .. EnemyData.Node)
else
EnemyData.Done = true
end
else
-- Move to next node
EnemyData.Node = EnemyData.Node + 1
if path[EnemyData.Node] and path[EnemyData.Node + 1] then
currentNode = path[EnemyData.Node]
nextNode = path[EnemyData.Node + 1]
EnemyPart.CFrame = CFrame.new(currentNode.Position + (nextNode.Position - currentNode.Position).Unit * remainingDistance)
print("Client: Moved to next node: " .. EnemyData.Node)
else
EnemyData.Done = true
end
end
elseif remainingDistance > 0 then
-- Move to next node
EnemyData.Node = EnemyData.Node + 1
if path[EnemyData.Node] and path[EnemyData.Node + 1] then
currentNode = path[EnemyData.Node]
nextNode = path[EnemyData.Node + 1]
EnemyPart.CFrame = CFrame.new(currentNode.Position + (nextNode.Position - currentNode.Position).Unit * remainingDistance)
print("Client: Moved to next node: " .. EnemyData.Node)
else
EnemyData.Done = true
end
end

-- Debug information
print("Client: Enemy ID: " .. EnemyData.EnemyId .. " Position: " .. tostring(EnemyPart.Position) .. " Node: " .. EnemyData.Node)
else
print("Client: Error: currentNode or nextNode is nil for Enemy ID:", EnemyData["EnemyId"])
end
else
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

else
local path = Enemy.Path

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

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

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

-- Smooth transition and rotation
if distanceToNextNode > 0.01 then -- Check for small movements to avoid unstable rotations
local targetCFrame = CFrame.lookAt(newPosition, nextNode.Position)
clientTarget.CFrame = clientTarget.CFrame:Lerp(targetCFrame, 1) -- Use alpha = 1 for smooth transitions
end

-- Calculate how much distance remains after reaching the next node
local remainingDistance = moveDistance - distanceToNextNode

-- Check if the NPC has moved past the next node
if remainingDistance > 0 and nodeAfterNext then
local distanceToNodeAfterNext = (nodeAfterNext.Position - nextNode.Position).Magnitude

if remainingDistance >= distanceToNodeAfterNext then
-- Move to node after next
remainingDistance = remainingDistance - distanceToNodeAfterNext
Enemy.Node = Enemy.Node + 2
if path[Enemy.Node] and path[Enemy.Node + 1] then
currentNode = path[Enemy.Node]
nextNode = path[Enemy.Node + 1]
clientTarget.CFrame = CFrame.new(currentNode.Position)
print("Client: Moved to node after next: " .. Enemy.Node)
else
Enemy.Done = true
end
else
-- Move to next node
Enemy.Node = Enemy.Node + 1
if path[Enemy.Node] and path[Enemy.Node + 1] then
currentNode = path[Enemy.Node]
nextNode = path[Enemy.Node + 1]
clientTarget.CFrame = CFrame.new(currentNode.Position + (nextNode.Position - currentNode.Position).Unit * remainingDistance)
print("Client: Moved to next node: " .. Enemy.Node)
else
Enemy.Done = true
end
end
elseif remainingDistance > 0 then
-- Move to next node
Enemy.Node = Enemy.Node + 1
if path[Enemy.Node] and path[Enemy.Node + 1] then
currentNode = path[Enemy.Node]
nextNode = path[Enemy.Node + 1]
clientTarget.CFrame = CFrame.new(currentNode.Position + (nextNode.Position - currentNode.Position).Unit * remainingDistance)
print("Client: Moved to next node: " .. Enemy.Node)
else
Enemy.Done = true
end
end

-- Debug information
print("Client: Enemy ID: " .. Enemy.EnemyId .. " Position: " .. tostring(clientTarget.Position) .. " Node: " .. Enemy.Node)
else
print("Client: Error: currentNode or nextNode is nil for Enemy ID:", Enemy.EnemyId)
end
else
end
end
end
end

while true do
HandleMovement(dt)
end

``````

I’ll really appreciate it if someone could help me figure out what’s causing this issue, I’ve attached a copy of the place as well just in case you want to test and or see the logs of the units being spawned. Thank you.

1 Like

Have you tried only making it only the server is handling the lerping? Because you have the CFrame lepred on the server while the CFrame is getting lerped too on the client.

Both scripts might be trying to achieve the same thing but it causes to over-ride each other.

The server is doing it’s own calculations to keep track of the position of the NPC on the sever. While the client also does it’s own calculation which is separate from the server, but which actually affects the in game displayed / rendered model.

The server’s lerping doesn’t affect the client rendered model. They use the same function and same calculations to stay synced.

managed to fix the orientation using something like:

``````EnemyPart.CFrame = CFrame.new(currentNode.Position + (nextNode.Position - currentNode.Position).Unit * remainingDistance, nextNode.Position)
``````

But there’s still one part missing, there’s a slight difference in distance between all the units, although all of them travel the same path, just with different nodes, and a constant speed, some of catch up to other units which causes the units to look weird.

Also what I noticed is, when I set the NPCs speed to a high number, as they move they slow down at some nodes and then speed up once again, usually when passing corners.

I tried checking maybe it had something to do with the node generation but it comes out to be about the same length maybe a difference of 1 stud + / -, but I don’t think that’s the issue for the distance difference in units.

Any help is appreciated

I was thinking maybe calculate segments and then make sure the distance traveled for each segment is the same.