NPC pathfinding malfunctions

You can write your topic however you want, but you need to answer these questions:

  1. What do you want to achieve? Keep it simple and clear!

I have a module script with two functions. I want my NPC to travel randomly but when a remote is fired itll directly stop the random path traveling IMMEDIATELY and head to me. When the walkTo function is called it sets a variable TargetSpotted to true. And the random path traveling is a while true do loop which first checks if TargetSpotted is true to break the loop immediately, and this leads to my issue.

  1. What is the issue? Include screenshots / videos if possible!

So basically there is a remote that when called, it is supposed to call the npc to me and stop the random path traveling IMMEDIATELY. Except, for some reason, the NPC finishes traveling a waypoint, goes back, or does whatever the heck in the video shown, instead of going DIRECTLY to me like supposed to. Why is this? Can someone break down why and know how to fix?

Video:
robloxapp-20241222-2058443.wmv (583.1 KB)

Output of what happened in video:

this is my MODULE script im using (Ignore block comments)

local module = {}

local PathfindingService = game:GetService("PathfindingService")

local Path = PathfindingService:CreatePath()

local TargetSpotted = false

module.walkTo = function(character : Model, endpos: Part)
	TargetSpotted = true

	
	local humanoid: Humanoid = character.Humanoid
	local humanoidRootPart = character.HumanoidRootPart

	local maxRetries = 5
	local currentRetry = 1
	local Success, Result


	local reachedEvent
	local pathBlockedEvent

	repeat 

		Success, Result = pcall(Path.ComputeAsync, Path, humanoidRootPart.Position, endpos)

	until Success or currentRetry == maxRetries


	if not Success then
		print(Result)
		return
	end

	if Path.Status ~= Enum.PathStatus.Success then
		print("Path could not be generated.")
		return
	end


	local Waypoints = Path:GetWaypoints()
	local CurrentWaypointIndex = 1 -- dw ik 

	for i, v in Waypoints do
		local Sphere = Instance.new("Part")
		Sphere.Shape = Enum.PartType.Ball
		Sphere.Position = v.Position
		Sphere.Anchored = true
		Sphere.CanCollide = false
		Sphere.BrickColor = BrickColor.new("Baby blue")
		Sphere.Material = Enum.Material.SmoothPlastic
		Sphere.Parent = workspace
	end

	-- R u teasing me rn lol
	-- lololol gg ez
	local function MoveToNextWaypoint()
		CurrentWaypointIndex += 1

		humanoid:MoveTo(Waypoints[CurrentWaypointIndex].Position)
		if Waypoints[CurrentWaypointIndex].Action == Enum.PathWaypointAction.Jump then
			humanoid.Jump = true
		end
	end

	local function onReached(reached)
		if reached and CurrentWaypointIndex < #Waypoints then
			MoveToNextWaypoint()
			print("Next waypoint")	
		else
			reachedEvent:Disconnect()
			reachedEvent = nil 
			pathBlockedEvent:Disconnect()
			pathBlockedEvent = nil
		end
	end

	if reachedEvent == nil then
		reachedEvent = humanoid.MoveToFinished:Connect(onReached)
	end

	pathBlockedEvent = Path.Blocked:Connect(function(waypointBlocked)

		if waypointBlocked > CurrentWaypointIndex then
			pathBlockedEvent:Disconnect()

			-- We r gonna assign it a new connection anyway cuz this function is gonna be called again
			-- when u recurse the function pathBlockedEvent is gona be assigned a new value

			print("Generating new path...")
		end




	end)

	MoveToNextWaypoint()
end



module.RandomTraveling = function(character: Model, WaypointsFolder : Folder)
	local humanoid = character.Humanoid
	local humanoidRootPart = character.HumanoidRootPart
	local WaypointsFolder = workspace.Waypoints

	-- i defined it here but we dont need it cuz we're only using it here

	while true do
		if TargetSpotted then
			print("Target Spotted, breaking the random traveling")
			break
		end
		
		local children = WaypointsFolder:GetChildren() -- array of waypoints
		for index = 1, #children do -- length of the array of waypoints
			local randomWaypoint = children[math.random(1, #children)] -- random index

			humanoid:MoveTo(randomWaypoint.Position)
			humanoid.MoveToFinished:Wait()
		end
	end
end


return module



The script calling the module if needed:

local PathModule = require(game.ReplicatedStorage.PathModule)
local CallAgentRemote = game.ReplicatedStorage.CallAgent

CallAgentRemote.OnServerEvent:Connect(function(player : Player)
	PathModule.walkTo(workspace.Agent, player.Character.HumanoidRootPart.Position)

end)

PathModule.RandomTraveling(workspace.Agent, workspace.Waypoints)

I believe your problem lies with your usage of MoveToFinished:Wait().

The script will wait until the humanoid has reached a waypoint (its current waypoint trajectory) before going to you.

Setting TargetSpotted—which should be camelCase to align with your naming convention—does stop your while loop, but not its inner numerical for loop. Since no code exists beyond this loop, move your check to within it to stay more up-to-date with the state of TargetSpotted

while true do
	local waypoints = WaypointsFolder:GetChildren()

	for _ = 1, #waypoints do
        if targetSpotted then
			print("Target Spotted, breaking the random travelling.")

			break
		end

        -- Ensure the NPC travels to a different waypoint.
		local randomWaypoint = table.remove(waypoints, math.random(#waypoints))

		humanoid:MoveTo(randomWaypoint.Position)
		humanoid.MoveToFinished:Wait()
	end
end

With more complex logic, to avoid having to place duplicate checks at yielding points, you can encapsulate your logic in a to-be-cancelled thread:

function module.randomTraveling(character: Model, WaypointsFolder: Folder) -- You misspelt "travelling".
    if module.randomTravellingThread then
        return
    end

    module.randomTravellingThread = task.defer(function()
        while true do
			local waypoints = WaypointsFolder:GetChildren()
		
			for _ = 1, #waypoints do
				local randomWaypoint = table.remove(waypoints, math.random(#waypoints))
		
				humanoid:MoveTo(randomWaypoint.Position)
				humanoid.MoveToFinished:Wait()
			end
		end
    end)
end
function module.walkTo(character: Model, endpos: Part)
    if module.randomTravellingThread then
        task.cancel(module.randomTravellingThread)

        module.randomTravellingThread = nil
    end

    -- ...
end
1 Like

It works! However, do you think you can give me some more explaining on what this does? (Your explanation is great, I just want to make sure I understand this right.

Like, what does this block do for example

   if module.randomTravellingThread then
        return
    end

this is what made my code work but i also dont grasp what the fix was. Isn’t this also checking if theres random traveling occuring? Like instead of in the while true do loop checking for if a variable is true, in the walkTo function when its called you can just cancel the random traveling function?

function module.randomTraveling(character: Model, WaypointsFolder: Folder) -- You misspelt "travelling".
    if module.randomTravellingThread then
        return
    end

    module.randomTravellingThread = task.defer(function()
        while true do
			local waypoints = WaypointsFolder:GetChildren()
		
			for _ = 1, #waypoints do
				local randomWaypoint = table.remove(waypoints, math.random(#waypoints))
		
				humanoid:MoveTo(randomWaypoint.Position)
				humanoid.MoveToFinished:Wait()
			end
		end
    end)
end
function module.walkTo(character: Model, endpos: Part)
    if module.randomTravellingThread then
        task.cancel(module.randomTravellingThread)
    end

    -- ...
end

The solution utilizes threads. Roblox is, by default, cooperatively multitasked. This means only one thread of execution is running at any given time. It is only when that thread yields (pauses) that another may begin running. This is why you need to yield in long-winded loops as not doing so will cause other vital threads to stop running, ultimately hanging the engine on your loop. Your loop yields when the NPC is walking (Humanoid.MoveToFinishes:Wait()), which allows other code to run. At some point, your module.walkTo function will be called, and its function body executed. As the function body is executing, we know the random travelling thread is paused. Threads can only be manipulated in this state. We can use task.cancel to prevent the random travelling thread from resuming, ultimately killing the random travelling process

This was already happening before due to the previously mentioned cooperative multitasking rule. We just took control of the thread with task.defer; the only way for module.walkTo to raise the flag for your module.randomTraveling loop to stop was for that loop to yield. This is why I say controlling the thread is best as it allows you to stop externally the thread any “any point” without having to manually lay checkpoints for your flag

1 Like

Hi, sorry for the late reply.

Just one more thing im wondering; it’s about this block of code again.

  if module.randomTravellingThread then
        return
    end

If I have multiple npcs, let’s say I call the randomTraveling function multiple times, it will also create a new randomTravellingThread multiple times, correct? With that information, here is where my question lies:

On the second time it is called, it will return because randomTravellingThread has already been called. This is done from that code block above. However, why does it do this? When it calls the randomTraveling function again how does it know from the past that randomTraveling thread was set to a truthy value (meaning it was called previously)?

module.randomTraveling does 2 things:

  1. Check if a random travelling process had already been initiated. If so, do not initiate another one
  2. Initiate a random travelling process. This process is stored in module.randomTravellingThread so it can be cancelled later on.

If a thread is stored in module.randomTravellingThread, we know a random travelling process had been started. Your question has brought up a bug in my code though. When I cancel the thread, I do not erase its existence, which leads to module.randomTraveling to think that a process is still active. I edited my code to fix that

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.