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

task.wait(5)

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
				print("Server: 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

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
		print("Client: 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]
				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
				print("Client: Error: Path not found for Enemy ID:", Enemy.EnemyId)
			end
		end
	end
end

while true do
	local dt = task.wait()
	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.

prototypeMain27_WorksButSpinsWeirdly_1.rbxl (103.0 KB)

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.