NPC Pathfinding Stutter

I am trying to make a custom NPC Pathfinding system for a game. The system works great… for the first 60 seconds or so. After, the NPC’s start to stutter and stop walking between points causing a stop-go-stop-go effect.

I’m unsure what would cause this as it works fine and after a short while it stutters as you can see below:
1

script.Name = script.Parent.Name.." AI"
_G.Debug = true

--//Config Values
local attackDamage = 20
local attackCooldown = 2
local searchDistance = 100
local wanderDistance = 20
local returntoWanderWhenAttackDB = false -- (i have a mob in my game that will attack once and run away)

--//Services
local pathFinding = game:GetService("PathfindingService")
local Players = game:GetService("Players")

local Path = pathFinding:CreatePath({AgentRadius = 4;WaypointSpacing = 5})

local Waypoints = {}
local CurrentPoint = 0
local BlockedPoint = 999

--//Vars
local Body = script.Parent
local Humanoid = Body:WaitForChild("Humanoid")
local Head = Body:WaitForChild("Head")
local bodyRoot = Body:WaitForChild("HumanoidRootPart")

local attackDB = false
local debugTable = {}

local function getPath(pointA,pointB)
	CurrentPoint = 0
	BlockedPoint = 999
	Path:ComputeAsync(pointA,pointB)
	Waypoints = Path:GetWaypoints()	

	if _G.Debug then
		for _,v in pairs(debugTable) do v:Destroy() end;debugTable = {}	
		for _, waypoint in pairs(Waypoints) do
			local part = Instance.new("Part")
			part.Shape = "Ball"
			part.Material = "Neon"
			part.Size = Vector3.new(0.6, 0.6, 0.6)
			part.Position = waypoint.Position
			part.Anchored = true
			part.CanCollide = false
			part.Parent = game.Workspace
			table.insert(debugTable,part)
		end
	end
end

local function getClosestHRP()
	local closestHRP,closestDistance = nil,searchDistance

	if attackDB and returntoWanderWhenAttackDB then return nil end --If return to wander after attack is true then dont find humans

	--//Find closest player within the range defined in searchDistance
	for _,Player in pairs(Players:GetPlayers()) do
		local pChar = Player.Character
		if pChar and Players:GetPlayerFromCharacter(pChar) then
			local pHRP = pChar:FindFirstChild("HumanoidRootPart")
			local pHumanoid = pChar:FindFirstChild("Humanoid")
			if pHRP and pHumanoid then
				local pDistance = (pHRP.Position - bodyRoot.Position).Magnitude

				if pDistance < closestDistance and pHumanoid.Health > 0 then
					closestHRP = pHRP
					closestDistance = pDistance
				end
			end
		end
	end

	return closestHRP
end

Humanoid.Touched:Connect(function(hit,limb)
	if not attackDB then
		local pHumanoid = hit.Parent:FindFirstChild("Humanoid")
		local pChar = hit.Parent
		if pHumanoid and Players:GetPlayerFromCharacter(pChar) then
			if pHumanoid.Health > 0 then
				attackDB = true
				Head:FindFirstChild("Attack"):Play()
				pHumanoid:TakeDamage(attackDamage)
				task.wait(attackCooldown)
				attackDB = false
			end
		end
	end
end)

Path.Blocked:Connect(function(p)
	if p > CurrentPoint then
		if _G.Debug then
			print("Path Blocked at",p)
		end
		BlockedPoint = p
	end
end)


while Humanoid.Health > 0 do
	local closestHRP = getClosestHRP()
	local goingDirect = false
	
	if attackDB and returntoWanderWhenAttackDB then closestHRP = nil end --On attack cooldown

	if closestHRP then
		local ray = Ray.new(bodyRoot.Position, (closestHRP.Position - bodyRoot.Position).Unit * 100)
		local hit,position = workspace:FindPartOnRayWithIgnoreList(ray,{script.Parent})

		if hit then --If can see player and they are at a similar y pos then go direct
			if hit:IsDescendantOf(closestHRP.Parent) and math.abs(bodyRoot.Position.Y - closestHRP.Position.Y) < 4 then
				Humanoid:MoveTo(position)
				goingDirect = true
				task.wait()
			end
		end
		if not goingDirect then
			getPath(bodyRoot.Position or Humanoid.WalkToPoint,closestHRP.Position)
		end
	else
		local wanderPos = bodyRoot.Position + Vector3.new(math.random(-wanderDistance,wanderDistance),0,math.random(-wanderDistance,wanderDistance))
		getPath(bodyRoot.Position,wanderPos)
	end
	
	if not goingDirect then
		if closestHRP or wanderDistance > 0 then		
			for i = 1,closestHRP and 5 or #Waypoints-1 do -- Go to first 5 nodes if chasing players then re-route. If not chasing player then go to node before last
				CurrentPoint = i
				local currentGoal = Waypoints[CurrentPoint]
				
				if CurrentPoint + 1 >= BlockedPoint then break end --Re-calculate
				
				if currentGoal then
					if not closestHRP then --If there wasnt a human to chase and now there is
						if not returntoWanderWhenAttackDB and getClosestHRP() then
							break
						end
					end
					
					Humanoid:MoveTo(currentGoal.Position)
					if currentGoal.Action == Enum.PathWaypointAction.Jump then
						Humanoid.Jump = true
					end
					
					local start = tick() --Wait until close to goal or timeout (This seemed to work better than MoveToFinished)
					repeat task.wait() until (bodyRoot.Position - currentGoal.Position).Magnitude <= 3 or tick()-start >= 2
					
					if #debugTable > 0 then --Remove point from debug vis
						debugTable[1]:Destroy()
						table.remove(debugTable,1)
					end
					
					if Waypoints[CurrentPoint + 1] then --Start moving to next point while route re-calculates (This seemed to fix initial stutter as it moves to next point while calculating)
						Humanoid:MoveTo(Waypoints[CurrentPoint + 1].Position)
					end
				end
			end
		end
	end
end

I am attaching the place file if anyone would be so generous to take a look, provide feedback, and maybe solve the issue?
ai.rbxl (48.2 KB)

Add this:

for _, desc in ipairs(Body:GetDescendants()) do
if (desc:IsA(“BasePart”)) then
desc:SetNetworkOwner()
end
end

Didn’t think to try network ownership, let me try that and I will report back…

Edit: That fixed the issue! Thank you, I am glad it was a quick fix!

1 Like

Scratch that, it fixed the issue in Studio but is still a problem in live-game :frowning: But at this point I think I can solve the issue. I moved a bunch of stuff around trying to fix the problem caused by network ownership.