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)
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!!
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
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.
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?