NPC :MoveTo has a stuttering effect whilst using PathfindingService

In this issue, we are using PathfindingService alongside Humanoid:MoveTo.

When we call Humanoid:MoveTo() from waypoint to waypoint, and use the MoveToFinished event to detect when the character reaches each waypoint it does work, however unlike the demo place and code portrayed in the documentation on Character Pathfinding, many games such as ours will not operate with smoothness due to the event having a delay on being communicated, leaving a stuttering NPC between nodes and overall a poor visual effect.

This is detrimental on considering performance in public games, unlike test baseplates with minimal events and memory.

The solution’s attempted include setting Network Ownership to nil on all BaseParts and MeshParts.

My approach to using this was as followed:

Path being passed through followpath method

local path  = PathfindingService:CreatePath({
		WaypointSpacing = 5,
		Costs = {
			RailwayTrack = math.huge,
			Trees = math.huge,
			Grass = 10, 
			LeafyGrass = 10,
			GrassPart = 10,
			Road = 0,
			Crossing = 0,
			DiamondPlate = 0,
			Railing = math.huge
		},
		AgentCanJump = true,
		AgentCanClimb = true,
		AgentHeight = 3,
		AgentRadius = 2
	})
local function BeginPath(Rig, path, primaryDestination)
	local success, errorMessage = pcall(function()
		path:ComputeAsync(Rig.HumanoidRootPart.Position, primaryDestination)
	end)

	if success and path.Status == Enum.PathStatus.Success then
		waypoints = path:GetWaypoints()

		-- Detect if path becomes blocked
		blockedConnection = Janitor:Add(path.Blocked:Connect(function(blockedWaypointIndex)
			if blockedWaypointIndex >= nextWaypointIndex and tries <= 3 then
				for _, player in ipairs(game.Players:GetPlayers()) do
					if player.Character and player.Character:FindFirstChild("HumanoidRootPart") then
						local distance = (Rig.HumanoidRootPart.Position - player.Character.HumanoidRootPart.Position).magnitude
						if distance < closestPlayerDistance then
							closestPlayerDistance = distance
							closestPlayer = player
						end
					end
				end

				if closestPlayer then
					local furthestPoint = findFurthestPointFromPlayer(Rig, closestPlayer.Character.HumanoidRootPart.Position)
					if furthestPoint then
						BeginPath(Rig, path, furthestPoint)
						Rig.Humanoid.WalkSpeed = 29
					else
						tries += 1
						BeginPath(Rig, path, primaryDestination)
					end 
				end 
			elseif tries > 7 then
				_remotes.Blocked:Fire()
				print(`[NPCService > Path]: Path is blocked, terminating this attempt.`)
				if Janitor then Janitor:Cleanup()
				end
			end
		end))

		-- Detect when movement to next waypoint is complete
		if not reachedConnection then
			reachedConnection = Janitor:Add(Rig.Humanoid.MoveToFinished:Connect(function(reached)
				for i,v in pairs(script.Parent:GetDescendants()) do
					if v:IsA("Part") or v:IsA("MeshPart") then
						v:SetNetworkOwner(nil)
					end
				end
				if reached and nextWaypointIndex < #waypoints then
					nextWaypointIndex += 1
					for _, player in ipairs(game.Players:GetPlayers()) do
						if player.Character and player.Character:FindFirstChild("HumanoidRootPart") then
							local distance = (Rig.HumanoidRootPart.Position - player.Character.HumanoidRootPart.Position).magnitude
							if distance < closestPlayerDistance then
								closestPlayerDistance = distance
								closestPlayer = player
							end
						end
					end
					if closestPlayer and Fleeing == false and EligableToFlee == true then
						local furthestPoint = findFurthestPointFromPlayer(Rig, closestPlayer.Character.HumanoidRootPart.Position)
						Fleeing = true
						nextWaypointIndex = 2
						BeginPath(Rig, path, furthestPoint)
					else
						Rig.Humanoid:MoveTo(waypoints[nextWaypointIndex].Position)
					end 
				else
					if Fleeing then 
						for _, player in ipairs(game.Players:GetPlayers()) do
							if player.Character and player.Character:FindFirstChild("HumanoidRootPart") then
								local distance = (Rig.HumanoidRootPart.Position - player.Character.HumanoidRootPart.Position).magnitude
								if distance < closestPlayerDistance then
									closestPlayerDistance = distance
									closestPlayer = player
								end
							end
						end

						if closestPlayer and EligableToFlee == true then
							local furthestPoint = findFurthestPointFromPlayer(Rig, closestPlayer.Character.HumanoidRootPart.Position)
							Fleeing = true
							nextWaypointIndex = 2
							BeginPath(Rig, path, furthestPoint)
							_remotes.Reached:Fire(Fleeing)
						end 	
					else
						_remotes.Reached:Fire(Fleeing)
						if Janitor then Janitor:Cleanup()
						end
					end
				end
			end))
		end
		nextWaypointIndex = 2
		Rig.Humanoid:MoveTo(waypoints[nextWaypointIndex].Position)
	else
		warn("Path not computed!", errorMessage)
		return -1
	end
end

Expected behavior

If everything was running as expected, there should be a smooth transition between each path node.

Instead, the NPC stutters after reaching a node before moving off.

Evidence

See @Dlimerick’s post here: NPC :MoveTo has a stuttering effect whilst using PathfindingService - #11 by Dlimerick

2 Likes

From my past interactions, I think this falls under @portenio’s area.

2 Likes

I think this is a case of the calculation of waypoints in pathfinding, sometimes the NPC moves weirdly due to all the calculations that are happening internally. I’ve experienced it too.

1 Like

By the way something you could try is increasing the waypoint spacing, see if that works.

Already tried, no luck due to the game being designed in a way which paths are the key to it. Performance is impacted nonetheless.

Rolblox should really improve their services, this is why people don’t use luau that much.

Datastoreservice for example should have built-in session locking, but nope. Developers have to do roblox’s work for them.

Got the same effect here, though it’s only noticable if there’s 40+ up where NPCs start to drastically drop FPS on low end PC

1 Like

to be fair, port dealt with my last pathfinding issue extremely well. got a feeling he’s a key figure in the npc area.

Yeah he’s good, but this is a multi-billion dollar company. They’re always being so cheap.

The amount of feature requests I have yet no access. :sob:

Here you can see a clip of the NPC stuttering

robloxapp-20240713-1932432.wmv (4.7 MB)

1 Like

Just acknowledging that we have assigned this issue internally and will take a look!

1 Like

@M_etrics : I tried click to move in one of our baseplate and my character did not stutter.

Looking at the shared example code, it seems pathfinding is getting called every waypoint. This may be causing the stuttering effect. Could you please share a repo case , so we can take a look and suggest alternatve solution.

As seen in my original post, I stated that the problem is not evident in baseplates. I’d say that the game is subject to burdenous strain unlike a baseplate due to the game having to function not solely on just the pathfinding, but the whole game’s systems. Consequently, exacerbating the problem.

The repro place is here: https://www.roblox.com/games/14711534718/Police-Response-Blythtonhttps://www.roblox.com/games/14711534718/Police-Response-Blythton.

Cheers.

1 Like

You are correct. We will update our documentation for this use case and share an example code snippet. In the meantime, I would recommend looking at the implementation of ClickToMoveController. Specifcailly search for IsCurrentWaypointReached function. You would notice that ClickToMove controller does not register for MoveToFinished but instead uses its own logic to decide when to move to next waypoint. This would avoid waiting for the dealyed event.
image

1 Like

Cheers for the response, I will publish a solution later today to help out others. Would you like me to PR the documentation github with this?

local PathfindingService = game:GetService("PathfindingService")
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")

local path = PathfindingService:CreatePath()

local player = Players.LocalPlayer
local character = player.Character
local humanoidRootPart = character:FindFirstChild("HumanoidRootPart")
local humanoid = character:WaitForChild("Humanoid")

local TEST_DESTINATION = Vector3.new(100, 0, 100)
local ZERO_VECTOR3 = Vector3.new(0,0,0)

local waypoints
local nextWaypointIndex
local reachedConnection
local blockedConnection

local currentWaypointPlaneNormal = ZERO_VECTOR3
local currentWaypointPlaneDistance = 0

local function followPath(destination)
	-- Compute the path
	local success, errorMessage = pcall(function()
		path:ComputeAsync(character.PrimaryPart.Position, destination)
	end)

	if success and path.Status == Enum.PathStatus.Success then
		-- Get the path waypoints
		waypoints = path:GetWaypoints()

		-- Detect if path becomes blocked
		blockedConnection = path.Blocked:Connect(function(blockedWaypointIndex)
			-- Check if the obstacle is further down the path
			if blockedWaypointIndex >= nextWaypointIndex then
				-- Stop detecting path blockage until path is re-computed
				blockedConnection:Disconnect()
				-- Call function to re-compute new path
				followPath(destination)
			end
		end)
		-- Initially move to second waypoint (first waypoint is path start; skip it)
		nextWaypointIndex = 2
		humanoid:MoveTo(waypoints[nextWaypointIndex].Position)
		
		-- Detect when movement to next waypoint is complete every frame
		RunService.RenderStepped:Connect(function(dt: number)
			local reached = false
			local currentHumanoidPosition = humanoidRootPart.Position
			local currentHumanoidVelocity = humanoidRootPart.Velocity

			-- Check we do have a plane, if not, we consider the waypoint reached
			if currentWaypointPlaneNormal ~= ZERO_VECTOR3 then
				-- Compute distance of Humanoid from destination plane
				local dist = currentWaypointPlaneNormal:Dot(currentHumanoidPosition) - currentWaypointPlaneDistance
				-- Compute the component of the Humanoid velocity that is towards the plane
				local velocity = -currentWaypointPlaneNormal:Dot(currentHumanoidVelocity)
				-- Compute the threshold from the destination plane based on Humanoid velocity
				local threshold = math.max(1.0, 0.0625 * velocity)
				-- If we are less then threshold in front of the plane (between 0 and threshold) or if we are behing the plane (less then 0), we consider we reached it
				reached = dist < threshold
			else
				reached = true
				currentWaypointPlaneNormal	= ZERO_VECTOR3
				currentWaypointPlaneDistance = 0
			end

			if reached and nextWaypointIndex < #waypoints then
				currentWaypointPlaneNormal = ZERO_VECTOR3 
				currentWaypointPlaneDistance = 0
				-- Increase waypoint index and move to next waypoint
				nextWaypointIndex += 1
				humanoid:MoveTo(waypoints[nextWaypointIndex].Position)
			else
				blockedConnection:Disconnect()
			end
		end)
		
	else
		warn("Path not computed!", errorMessage)
	end
end

followPath(TEST_DESTINATION)
1 Like

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