Alright, I looked into this, and the likely issue is that, for one, MoveToFinished is fairly unreliable because it can work in the future and use predictions. So, as a replacement (as seen here), many developers just use Heartbeat logic to perform waypoint actions. (You can also see people struggling with MoveTo logic here).
Iâd recommend using RenderStepped/Heartbeat logic for your problem, and Iâd implement it similar to this (you should just be able to replace your loop with this).
local RunService = game:GetService("RunService")
local Debris = game:GetService("Debris")
local agentParams = {
AgentRadius = 4;
AgentHeight = 10;
AgentCanJump = false;
}
local path = pathfindingservice:CreatePath(agentParams)
local function followToDestination(entity, part)
if not entity or not entity.PrimaryPart or not part then return false end
while true do
local success, error = pcall(function()
path:ComputeAsync(entity.PrimaryPart.Position, part.Position)
end)
if not success or path.Status ~= Enum.PathStatus.Success then
warn("Path compute failed:", error, path.Status)
return false
end
local waypoints = path:GetWaypoints()
if #waypoints == 0 then
warn("No waypoints returned")
return false
end
local finalPos = waypoints[#waypoints].Position
if (entity.PrimaryPart.Position - finalPos).Magnitude <= 1.0 then
return true
end
local blocked = false
local blockedConn
blockedConn = path.Blocked:Connect(function(blockedIndex)
blocked = true
if blockedConn and blockedConn.Connected then blockedConn:Disconnect() end
end)
local humanoid = entity:FindFirstChildOfClass("Humanoid")
local root = entity.PrimaryPart
if not humanoid or not root then
if blockedConn and blockedConn.Connected then blockedConn:Disconnect() end
warn("Missing humanoid or PrimaryPart")
return false
end
local reachedAll = true
for i = 1, #waypoints do
local waypoint = waypoints[i]
local debugPart = Instance.new("Part")
debugPart.Size = Vector3.new(1,1,1)
debugPart.Position = waypoint.Position
debugPart.Parent = game.Workspace
debugPart.Anchored = true
debugPart.BrickColor = BrickColor.new("Really red")
debugPart.CanCollide = false
debugPart.Material = Enum.Material.Neon
Debris:AddItem(debugPart, 5)
humanoid:MoveTo(waypoint.Position)
local v = math.max(humanoid.WalkSpeed or 16, 0.1)
local a = 20
local d_stop = (v * v) / (2 * a)
local safety_margin = 1.5
local threshold = math.max(0.8, d_stop + safety_margin)
local D = (root.Position - waypoint.Position).Magnitude
local t_ideal = D / v
local beta = 1.4
local t_min = 0.35
local timeout = math.max(t_min, beta * t_ideal + 0.25)
timeout = math.min(timeout, 6)
local reached = false
local startTime = tick()
while tick() - startTime < timeout do
local curDist = (root.Position - waypoint.Position).Magnitude
if curDist <= threshold then
reached = true
break
end
if curDist <= d_stop + safety_margin * 1.2 then
local extraWaitStart = tick()
while tick() - extraWaitStart < 0.25 do
if (root.Position - waypoint.Position).Magnitude <= threshold then
reached = true
break
end
RunService.Heartbeat:Wait()
end
if reached then break end
end
if blocked then break end
RunService.Heartbeat:Wait()
end
if not reached or blocked then
reachedAll = false
break
end
end
if blockedConn and blockedConn.Connected then blockedConn:Disconnect() end
if reachedAll then
local finalWaypointPos = waypoints[#waypoints].Position
if (entity.PrimaryPart.Position - finalWaypointPos).Magnitude <= math.max(1.0, (humanoid.WalkSpeed or 16) * 0.2) then
return true
end
end
wait(0.1)
end
end
If you need a further explanation, this uses some kinematics such as đ_stop = đŁ^2/2đ, which computes the stopping distance, đĄ_ideal = đˇ/đŁ which multiplies the timeout by đ˝ and a small margin. Furthermore, I used a few numbers to get the best results.
a = 20 (studs/s^2), which stops the script from overestimating the distance
safety_margin = 1.5 which covers the jitter, and prevents the NPC from going astray
beta = 1.4 which gives small but responsive and tolerant updates
I tried to account for everything, such as measuring the distance between the waypoints, preventing the code from oscillating when met with obstacles instead of oscillating, and making sure nothing overrides each other. Let me know if this works.