How can I optimize this pathfinding visualization

Hello.

I have been working with pathfinding recently, and I was wondering if there is a way to constantly update a paths visual without it being laggy and expensive.

Here is a video showcasing the issue;

Here is a snippet of my code that I am currently using;

HeartbeatCon = RunService.Heartbeat:Connect(function()

	if not Humanoid then return end
	if not ConnectedPlayer then return end

	task.spawn(function()
		local Path = PathFindingService:CreatePath({AgentCanJump = false, AgentCanClimb = false, AgentHeight = 8})
		Path:ComputeAsync(NPC.HumanoidRootPart.Position, ConnectedPlayer.Character.HumanoidRootPart.Position)
		local WayPoints = Path:GetWaypoints()
		VisualizePath(WayPoints)
	end)

	if LastTick and tick() - LastTick < 1 then
		return
			
	end
	LastTick = tick()

	local ThisStepMagnitude = (ConnectedPlayer.Character.HumanoidRootPart.Position - NPC.HumanoidRootPart.Position).Magnitude

	if not PlayerVisible(ConnectedPlayer) then
		UpdHP = false
		ConnectedPlayer.Character.Movement.MovementClient:SetAttribute("SpeedMultiplier", 1)

		if currentPathThread == nil and tick() - LastRoamTick > 3 then
			UpdatePath(ConnectedPlayer)
		end
	else
		warn("in chase")
		LastVisible = tick()
		UpdHP = true
		UpdatePath(ConnectedPlayer)
	end
end)

Any help/suggestions would be highly appreciated :happy1:

2 Likes

it would be amazing to see what happens there.
thank you!

1 Like

The problem is you’re calling createPath and computeAsync every single heartbeat frame, which is 60+ times per second. That’s gonna destroy performance no matter what you do.

For the visualization specifically, you don’t need it updating that fast, just decouple it from heartbeat and run it on its own interval, something like every 0.1-0.2 seconds should be more than smooth enough visually and way cheaper.

Also worth noting that you are wrapping it in a task.spawn, which means you can get multiple computeasync calls overlapping each other if one takes longer than a frame. I think deboucing it or cancelling the previous oen before startin a new compute would be better.

It’d be nice to see what the UpdatePath function does though, like af_2048 said!!

2 Likes

Sorry for not attaching it, I forgot

local currentPathThread = nil

local function UpdatePath(Player: Player)
	if currentPathThread then
		task.cancel(currentPathThread)
		currentPathThread = nil
	end

	currentPathThread = task.spawn(function()
		local Path = PathFindingService:CreatePath({AgentCanJump = false, AgentCanClimb = false, AgentHeight = 8})
		Path:ComputeAsync(NPC.HumanoidRootPart.Position, Player.Character.HumanoidRootPart.Position)
		local WayPoints = Path:GetWaypoints()
	--	VisualizePath(WayPoints)

		for i, waypoint in WayPoints do
			Humanoid:MoveTo(waypoint.Position)
			Humanoid.MoveToFinished:Wait()
		end

		currentPathThread = nil
	end)
end
1 Like

Okay so the UpdatePath itself looks reasonable, the cancel + respawn pattern is correct. But I can see a potential issue, when you cancel the thread mid-loop, the NPC could be stuck waiting on MoveToFinished that never fires, which might cause weird frozen behavior depending on timing.

Also you’ve got VisualizePath commented out in here, but it’s still being called every Heartbeat frame in the code above. If you want smooth visualization without the cost, id suggest running a separate loop just for the visual that recomputes on its own timer (as i’ve suggested), completely independent from the actual movement path. That way your NPC movement could probably stay responsive and the visual smooth without hammering computeasync 60 times a second.

Could you please share what happens there also?

Have you tried reducing the amount of times the path computes and lerping it between computations? How about making the visualization loop only on the client?