Pathfinding Gradually FAILING, NPC Stops Moving After 2 Minutes! HUGE DEAL

This is a problem ive had for YEARS. i remember trying to do this most recently about 3 years ago. didnt work then, doesnt work now. no matter how i code it, no matter how clean it is, i always test everything i think of. And its always the pathfinding service. Introducing the pathfinding service always causes a problem. this is a HuGE deal for me.

  1. What do you want to achieve? Keep it simple and clear!
    I want to create a simple npc that follows the player (in my case its hardcoded to my character, “AlexanderYar”) using roblox pathfinding.

  2. What is the issue? Include screenshots / videos if possible!
    after about 2 minutes or so, the npc starts to gradually fail to move to the next waypoint. and ive tested it, it knows the next waypoint. its :MoveTo that fails to work. But I dont think the problem is :moveto in itself because i testing the same npc with pathfinding removed and instead doing a bunch of random positions. It works fine. But introducing roblox pathfinding breaks it for some reason.

  3. What solutions have you tried so far? Did you look for solutions on the Developer Hub?
    oh ive tried it all. i tested all i can think of. Recently i had the idea it might be a memory leak. but memory seems fine. AND GUESS WHAT. today i tried again but i made a strange discovery. creating a bunch of parts seems to fix the npc and it moves freely again, without stopping, temporarily.

Here are two posts ive read.
post 1
post 2

And this recently lead me to believing it was a memory leak, especially since in post 1, someone said the problem was fixed, and that was posted in May 2023. And post 2, which is a similar issue, was posted in OCTOBER 2023, after it was said to be fixed. So i thought maybe the devs skipped something and theres still a memory leakage. But i did more testing and it doesnt seem like a memory leakage.

the game is an empty baseplate with

  • some obstacles
  • the npc with the script in it
  • the clickable part which spawns in 100 little balls above it
  • christmas tree

i verified these are the only two scripts. i ran this and it says “t(x2)”
the script in the npc and the script in the clickable part. So nothing else can be interfering that i added.

for i,v in pairs(game.Workspace:GetDescendants()) do if v:IsA("Script") then print"t" end end

this is the script in the npc.
If you are too lazy or confused, ill explain it. There are two parts to it. One part sets new waypoints every 5 seconds (made it 5 seconds whilst debugging because i wanted to make sure the problem wasnt that i was updating the waypoints too fast.), the next part constantly goes to the next waypoint.

The first part does its job by waiting 5 seconds, then computingAsync the path and sets the global variable “waypooints” to the path’s waypoints, then it updates the serial variable, then it shows a visualization of the waypoints.

the point of the serial variable is to check when the waypoints variable is updated.

the second part works by using an index which decides which waypoint in the current waypoints variable it should be walking towards. I did this because if i just made it always go to waypoints[2] (which is the first waypoint that matters), then it would get stuck at that waypoint until the path was computed again.
every time a waypoint is reached, i increment the index and it goes to the next one.
Every .Stepped, it checks the serial, if its different, then it simply changes the index back to 2 so that it starts from the beginning of the new waypoints path.

Also, it constantly prints the distance from npc to the current waypoint.

local SRVC_pathdinder = game:GetService("PathfindingService")
local SRVC_runservice = game:GetService("RunService")
local SRVC_memoryservice = game:GetService("MemoryStoreService")

local targetPlr:Player = game.Players:WaitForChild("AlexanderYar")
local targetChar = targetPlr.Character
if targetChar == nil then
	
	targetPlr.CharacterAppearanceLoaded:Wait()
	targetChar = targetPlr.Character
	
end
local targetHRP:BasePart = targetChar:WaitForChild("HumanoidRootPart")

local hum:Humanoid = script.Parent:WaitForChild("Humanoid")
local hrp:BasePart = script.Parent:WaitForChild("HumanoidRootPart")

wait()

local pathParams = {
	AgentRadius = 2,
	AgentHeight = 5,
	AgentCanJump = true,
	AgentCanClimb = true,
	WaypointSpacing = math.huge,
	Costs = {}
}
local path:Path = SRVC_pathdinder:CreatePath()

local waypoints
local serial = 1 -- used to tell if waypoints was updated.

local calculateWaypointsCooldown = 5

coroutine.wrap(function() -- calculate new waypoints
	
	while true do
		
		wait(calculateWaypointsCooldown)
		
		local success, errorMessage = pcall(function()

			path:ComputeAsync(hrp.Position, targetHRP.Position)

		end)

		if success then
			
			waypoints = path:GetWaypoints()
			serial = serial +1
			
			if serial >= 1023 then
				
				serial = 0
				
			end
			
			for i,v:PathWaypoint in pairs(path:GetWaypoints()) do -- DEBUG START

				local part = Instance.new("Part")

				part.Anchored = true
				part.Shape = Enum.PartType.Ball
				part.Size = Vector3.new(1, 1, 1)
				part.Color = Color3.fromRGB(255, 0, 0)
				part.Material = Enum.Material.Neon
				part.Position = v.Position
				part.CanCollide = false
				part.Parent = game.Workspace
				
				coroutine.wrap(function()
					
					wait(calculateWaypointsCooldown)
					part:Destroy()

				end)()

			end -- DEBUG END

		else 
			
			warn("Path not computed!", errorMessage)
			
		end
		
	end
	
end)()

local lastSerial = 0
local gotoIndex = 2 -- index in waypoints to go to. this makes sure that it keeps following the path even if waypoints hasnt been updated. it wont get stuck at waypoint[2] anymore
local distanceVector3:Vector3 = Vector3.zero -- distance between hrp and current waypoint
local distance:number = 0

SRVC_runservice.Stepped:Connect(function() -- move the npc
	
	if lastSerial ~= serial then

		gotoIndex = 2
		lastSerial = serial

	end

	if (waypoints ~= nil and waypoints[gotoIndex] ~= nil) then

		distanceVector3 = waypoints[gotoIndex].Position -hrp.Position
		distance = Vector3.new(distanceVector3.X, 0, distanceVector3.Z).magnitude
		
		print(distance) -- TODO. IM PRETTY SURE PATHFINDING SERVICE HAS A MEMORY LEAK. CHECK DEV CONSOLE AND PLAY THE GAME FOR ABOUT 2 MINS. YOULL SEE IT KEEPS GOING UP AND IT SLOWS DOWN. MY CODE IS FINE, THE PROBLEM IS THE SERVER IS DYING.

		--print(distance) -- DEBUG

		if distance <= .5 then -- checks for if hum reached waypoint, if so, move on to next one unless there isnt a next one.

			if (waypoints[gotoIndex +1] ~= nil) then -- checks if next waypoint exists

				gotoIndex = gotoIndex +1

			end

		end
		hum:MoveTo(waypoints[gotoIndex].Position)
		--print(waypoints[gotoIndex].Action == Enum.PathWaypointAction.Jump) -- DEBUG
		if (waypoints[gotoIndex].Action == Enum.PathWaypointAction.Jump and hum:GetState() ~= Enum.HumanoidStateType.Jumping and hum:GetState() ~= Enum.HumanoidStateType.Freefall) then -- if jumping is necessary, jump UNLESS the hum is already jumping

			hum.Jump = true

		end

	end
	
end)

this is the script inside the clickable part

script.Parent.ClickDetector.MouseClick:Connect(function()
	
	for i = 1, 100, 1 do
		
		local part = Instance.new("Part")

		part.Anchored = false
		part.Shape = Enum.PartType.Ball
		part.Size = Vector3.new(1, 1, 1)
		part.Color = Color3.fromRGB(0, 255, 255)
		part.Material = Enum.Material.Neon
		part.Position = script.Parent.Position +Vector3.new(0, 2, 0)
		part.CanCollide = true
		part.Parent = game.Workspace
		
	end
	
end)

heres a video demonstration (low fps sorry, but i promise it looks smooth at first). Notice

  • it works as intended (except jumping but ill deal with that later) it moves around obstacles fine
  • after about 2 mins, it starts to get stuck sooner and sooner, eventually only moving to the first waypoint every time the path is updated
  • the printed value (which is distance to current waypoint) is more then .5, meaning nothing changed, waypoints are the same and serial is the same, and it SHOULD keep moving, but it simple stops and fails to move further.
  • the memory seems to be fine
  • the npc at first moves the same speed as me, meaning it works as it should until the thing starts happening
  • the npc does the same thing in server view, so its not a network stuttering issue that im concerned about.
  • clicking the part and adding all those little balls temporarily fixes the npc, then it goes back to doing the same thing again.
    demonstration video

I dont care what the solution is… if there is one. Ive been using roblox for a long time. And if theres one thing I could never do, its this. And I dont care if its a simple solution and ive been a bozo for 6 years. I want to figure this out already. I mean just LOOK at it. its a small script (my style uses a lot of spacing so it looks big, sorry) and ignoring the checks and the visualization, whats left is simply it calculating new waypoints every 5 seconds, and constantly moving the npc to the next waypoint, making sure to restart when the waypoints update. thats IT.

id appreciate comments, solutions, suggestions and such. <3

1 Like

Hello, first thing is that there are way too much prints. Over time these prints stack and cause massive lag. Also, you have two loops wheels you only need one. Put the other loop inside the run service. One more thing a loop in a loop is always a bad idea. So, for your creating part thing separate the function and add a wait between. task.wait() is better than wait() btw.

I appreciate your input. However, I strongly disagree with much of what you are saying. I dont believe any of those things will make a difference, especially since my fps didnt drop and the memory seems fine, and also moving the npc gets it back on track and fixes it temporarily. Also I see nothing wrong with nested loops, multiple loops, and using functions without declaring them earlier. All these things are common in programming.

I still tested what you suggested though. I have just tested these things one at a time:

  • removed the print statements
  • used runservice (no more while loops) and used tick() - lasttick to implement cooldown for calculations
  • used task.wait()
  • separated the for loop into an outside function and called it

low and behold, nothing changed. same problem. Id appreciate it if youd let me know if you have any helpful information such as whats the problem and how to fix it, or if its a bug in roblox of some sorts.

1 Like

Well if that didn’t work try simple path. It’s a path finding module. And if the FPS doesn’t drop and memory is fine, what is the issue again? I believe when I saw this post it said a memory leak.

this is what im trying to figure out. but it seems that almost nobody saw my post. I wouldnt expect this to be some easy fix. Ive been programming in lua, java, C, and python for years and i like to think i know what i need to know to get roblox lua scripting working, so its a surprise to me that this issue has been so persistant for years and i still cant figure it out. im hoping someone knows something about the roblox engine that i dont know that could explain why pathfinding seems to stop working after about 2 minutes.

try :MoveToPart()

also try making the y value the waypoint position 0

if that doesn’t work then maybe you could make your own movement for the npc or try copying someone else’s code

i tried these.
i didnt try movetopart cuz i dont want to summon any parts but i tried :move() and calculated the direction every .stepped. and its very interesting. It does something different but its similar to the original issue. after about 2 minuted, it starts circing around a waypoint. and i used physical axes to show where directions were. the direction was calculated correctly as it was pointing straight towards the waypoints, but the npc was moving sideways instead of in the correct direction, which is what made it go in a circle.

I made the scripts much simpler to read. its as basic as it gets. I made 3 versions:

script WITH comments:

-- par AlexanderYar

-- getting services START
local SRVC_pathdinder = game:GetService("PathfindingService")
local SRVC_runservice = game:GetService("RunService")
-- getting services END

-- getting the target char START
local targetPlr:Player = game.Players:WaitForChild("AlexanderYar")
local targetChar = targetPlr.Character
if targetChar == nil then -- if character isnt in yet, wait for character to appear then try again
	targetPlr.CharacterAppearanceLoaded:Wait()
	targetChar = targetPlr.Character
end
local targetHRP:BasePart = targetChar:WaitForChild("HumanoidRootPart")
-- getting the target char END

-- getting npc humanoidRootPart START
local npcHUM:Humanoid = script.Parent:WaitForChild("Humanoid")
local npcHRP:BasePart = script.Parent:WaitForChild("HumanoidRootPart")
-- getting npc humanoidRootPart END

-- setting up variables for pathfinding START
local path:Path = SRVC_pathdinder:CreatePath()
local waypoints
local serial = 1 -- used to tell if waypoints was updated. AKA. update checker.
local lastSerial = 0
local gotoIndex = 2 -- index in waypoints to go to. this makes sure that it keeps following the path even if waypoints hasnt been updated. it wont get stuck at waypoint[2] anymore
-- setting up variables for pathfinding END

-- variables for cooldown between waypoints calculation START
local cooldown_calculateWaypoints = 5
local lastTick_calculateWaypoints = tick()
-- variables for cooldown between waypoints calculation END

-- run every frame START
SRVC_runservice.Stepped:Connect(function()
	local timer = tick() -- DEBUG
	
	-- CALCULATING NEW WAYPOINTS START
	if (tick() -lastTick_calculateWaypoints >= cooldown_calculateWaypoints) then -- if the cooldown for calculating waypoints has passed, then calculate waypoints and wait for the cooldown again.
		lastTick_calculateWaypoints = tick()
		
		-- pathfinding service waypoints calculations and preparation START
		path:ComputeAsync(npcHRP.Position, targetHRP.Position) -- path from npc to player
		waypoints = path:GetWaypoints()
		serial = serial +1
		if serial >= 1023 then -- this just makes it so serial cycles back to 0 if it reaches 1023. i dont want a big number because it would probably crash.
			serial = 0
		end
		-- pathfinding service waypoints calculations and preparation END
		
		-- path VISUALIZATION START
		for i,v:PathWaypoint in pairs(path:GetWaypoints()) do
			local part = Instance.new("Part") -- new red ball to represent a waypoint.
			part.Anchored = true
			part.Shape = Enum.PartType.Ball
			part.Size = Vector3.new(1, 1, 1)
			part.Color = Color3.fromRGB(255, 0, 0)
			part.Material = Enum.Material.Neon
			part.Position = v.Position
			part.CanCollide = false
			part.Parent = game.Workspace
			coroutine.wrap(function() -- this just deletes them in time for new waypoints to be calculated and a NEW vizualization to appear.
				task.wait(cooldown_calculateWaypoints) -- delete visualization when new waypoints are calculated
				part:Destroy()
			end)()
		end
		-- path VISUALIZATION END
		
	end
	-- CALCULATING NEW WAYPOINTS END
	
	-- reset to first waypoint when new waypoints are calculated START
	if lastSerial ~= serial then -- if the serial changed, that just means new waypoints were calculated, so lets reset
		gotoIndex = 2 -- resets, start at the first waypoint of this new path
		lastSerial = serial
	end
	-- reset to first waypoint when new waypoints are calculated END
	
	-- move the NPC START
	if (waypoints ~= nil and waypoints[gotoIndex] ~= nil) then -- check if the current waypoint exists. if so, move to it. if not, do nothing.
		
		-- get distance from npc to current waypoint START
		local distanceVector3 = waypoints[gotoIndex].Position -npcHRP.Position
		local distance = Vector3.new(distanceVector3.X, 0, distanceVector3.Z).magnitude
		-- get distance from npc to current waypoint END
		
		-- if current waypoint is reached, go to the next one. START
		if distance <= .5 then -- checks for if hum reached waypoint, if so, move on to next one unless there isnt a next one.
			if (waypoints[gotoIndex +1] ~= nil) then -- checks if next waypoint exists
				gotoIndex = gotoIndex +1
			end
		end
		-- if current waypoint is reached, go to the next one. END
		
		npcHUM:MoveTo(waypoints[gotoIndex].Position) -- move the npc. after about 2 mins, this just stops. As if its still trying to get to the right destination but it just stops moving.
	end
	-- move the NPC END
	
	print(tick() -timer) -- DEBUG. this prints how much time it took (in seconds) to do all this stuff in this runservice.stepped function.
end)
-- run every frame END

script withOUT comments:

-- par AlexanderYar

local SRVC_pathdinder = game:GetService("PathfindingService")
local SRVC_runservice = game:GetService("RunService")
local targetPlr:Player = game.Players:WaitForChild("AlexanderYar")
local targetChar = targetPlr.Character
if targetChar == nil then
	targetPlr.CharacterAppearanceLoaded:Wait()
	targetChar = targetPlr.Character
end
local targetHRP:BasePart = targetChar:WaitForChild("HumanoidRootPart")
local npcHUM:Humanoid = script.Parent:WaitForChild("Humanoid")
local npcHRP:BasePart = script.Parent:WaitForChild("HumanoidRootPart")
local path:Path = SRVC_pathdinder:CreatePath()
local waypoints
local serial = 1
local lastSerial = 0
local gotoIndex = 2
local cooldown_calculateWaypoints = 5
local lastTick_calculateWaypoints = tick()
SRVC_runservice.Stepped:Connect(function()
	local timer = tick()
	if (tick() -lastTick_calculateWaypoints >= cooldown_calculateWaypoints) then
		lastTick_calculateWaypoints = tick()
		path:ComputeAsync(npcHRP.Position, targetHRP.Position)
		waypoints = path:GetWaypoints()
		serial = serial +1
		if serial >= 1023 then
			serial = 0
		end
		for i,v:PathWaypoint in pairs(path:GetWaypoints()) do
			local part = Instance.new("Part")
			part.Anchored = true
			part.Shape = Enum.PartType.Ball
			part.Size = Vector3.new(1, 1, 1)
			part.Color = Color3.fromRGB(255, 0, 0)
			part.Material = Enum.Material.Neon
			part.Position = v.Position
			part.CanCollide = false
			part.Parent = game.Workspace
			coroutine.wrap(function()
				task.wait(cooldown_calculateWaypoints)
				part:Destroy()
			end)()
		end
	end
	if lastSerial ~= serial then
		gotoIndex = 2
		lastSerial = serial
	end
	if (waypoints ~= nil and waypoints[gotoIndex] ~= nil) then
		local distanceVector3 = waypoints[gotoIndex].Position -npcHRP.Position
		local distance = Vector3.new(distanceVector3.X, 0, distanceVector3.Z).magnitude
		if distance <= .5 then
			if (waypoints[gotoIndex +1] ~= nil) then
				gotoIndex = gotoIndex +1
			end
		end
		npcHUM:MoveTo(waypoints[gotoIndex].Position)
	end
	print(tick() -timer)
end)

and script for following a part (run, go to server view, draw the part wherever you want. same issue occurs):

-- par AlexanderYar

local SRVC_pathdinder = game:GetService("PathfindingService")
local SRVC_runservice = game:GetService("RunService")
local targetHRP:BasePart = game.Workspace:WaitForChild("target")
local npcHUM:Humanoid = script.Parent:WaitForChild("Humanoid")
local npcHRP:BasePart = script.Parent:WaitForChild("HumanoidRootPart")
local path:Path = SRVC_pathdinder:CreatePath()
local waypoints
local serial = 1
local lastSerial = 0
local gotoIndex = 2
local cooldown_calculateWaypoints = 5
local lastTick_calculateWaypoints = tick()
SRVC_runservice.Stepped:Connect(function()
	local timer = tick()
	if (tick() -lastTick_calculateWaypoints >= cooldown_calculateWaypoints) then
		lastTick_calculateWaypoints = tick()
		path:ComputeAsync(npcHRP.Position, targetHRP.Position)
		waypoints = path:GetWaypoints()
		serial = serial +1
		if serial >= 1023 then
			serial = 0
		end
		for i,v:PathWaypoint in pairs(path:GetWaypoints()) do
			local part = Instance.new("Part")
			part.Anchored = true
			part.Shape = Enum.PartType.Ball
			part.Size = Vector3.new(1, 1, 1)
			part.Color = Color3.fromRGB(255, 0, 0)
			part.Material = Enum.Material.Neon
			part.Position = v.Position
			part.CanCollide = false
			part.Parent = game.Workspace
			coroutine.wrap(function()
				task.wait(cooldown_calculateWaypoints)
				part:Destroy()
			end)()
		end
	end
	if lastSerial ~= serial then
		gotoIndex = 2
		lastSerial = serial
	end
	if (waypoints ~= nil and waypoints[gotoIndex] ~= nil) then
		local distanceVector3 = waypoints[gotoIndex].Position -npcHRP.Position
		local distance = Vector3.new(distanceVector3.X, 0, distanceVector3.Z).magnitude
		if distance <= .5 then
			if (waypoints[gotoIndex +1] ~= nil) then
				gotoIndex = gotoIndex +1
			end
		end
		npcHUM:MoveTo(waypoints[gotoIndex].Position)
	end
	print(tick() -timer)
end)

i made a demo rblx with these 3 scripts. i put them inside a random npc.
pathfinding_service_bug.rbxl (71.8 KB)

I think the problem was with your cooldown:

-- par AlexanderYar

local SRVC_pathdinder = game:GetService("PathfindingService")
local SRVC_runservice = game:GetService("RunService")

local targetHRP:BasePart = game.Workspace:WaitForChild("target")
local npcHUM:Humanoid = script.Parent:WaitForChild("Humanoid")
local npcHRP:BasePart = script.Parent:WaitForChild("HumanoidRootPart")
local path:Path = SRVC_pathdinder:CreatePath()

local waypoints
local serial = 1
local lastSerial = 0
local gotoIndex = 2
local cooldown_calculateWaypoints = 1
local visualize = true

while true do
		
	path:ComputeAsync(npcHRP.Position, targetHRP.Position)
	waypoints = path:GetWaypoints()
	serial = serial + 1

	if serial >= 1023 then
		serial = 0
	end
	
	if visualize then
		for i,v:PathWaypoint in pairs(path:GetWaypoints()) do
			local part = Instance.new("Part")
			part.Anchored = true
			part.Shape = Enum.PartType.Ball
			part.Size = Vector3.new(1, 1, 1)
			part.Color = Color3.fromRGB(255, 0, 0)
			part.Material = Enum.Material.Neon
			part.Position = v.Position
			part.CanCollide = false
			part.Parent = game.Workspace

			delay(cooldown_calculateWaypoints, function()
				part:Destroy()
			end)
		end
	end

	if waypoints and waypoints[gotoIndex] then
		local distanceVector3 = waypoints[gotoIndex].Position - npcHRP.Position
		local distance = Vector3.new(distanceVector3.X, 0, distanceVector3.Z).magnitude

		if distance <= .5 then
			if (waypoints[gotoIndex + 1] ~= nil) then
				gotoIndex = gotoIndex + 1
			end
		end

		npcHUM:MoveTo(waypoints[gotoIndex].Position)
	end


	task.wait()
end

Only thing is it doesn’t work like you had it initially. It now moves where the part is without finishing and the visualization is kinda broken, but I’m assuming it’s just for testing so it should be fine. I tested it and it seemed to work. However, you should still check.

Also I still highly recommend simple path.

ehh im a tad confused. I see “It now moves where the part is without finishing and the visualization is kinda broken”. i tested it many times. the visualization works perfectly for me and it recalculates the path to the part every 5 seconds as intended. Did it act differently for you?

Also what do you mean by simple path?

Are you talking about your script or my script? If it’s my script then that’s great.
Simple path is a path finding module that’s lag free.

first of all, oops i misread it. I realize now that you just posted some lua code. second, how on earth was cooldown the problem? cooldown or no cooldown the same problem exists. the cooldown is just there so i know for a fact that the issue cant be the waypoint calculations overloading the server. i only want to calculate new waypoints every 5 seconds, but i want the npc to always move to the next waypoint.

Did you at least try the code yet? It worked for me. If it works then you can try fixing the cooldown. I’m just trying to help you find the problem.

1 Like