Pathfinding choppy & freezing

  1. What do you want to achieve?

Smooth npc movement and interaction as seen in the first part of the video below,
To stop npc pathfinding from randomly freezing after they switch targets after 4-5 attempts

I tried messing with timings, with how moveto() works as it has a timeout from my knowledge, which appears to occur as they freeze for that timeout (7 seconds roughly) But nothing seemed to work, i dont see any timing issues or conflictions with the rest of my code so I must be missing something.

Video on issue (first 15 secs fine):

function Knob.Run(enemy)
	local hum = enemy:WaitForChild("Humanoid")
	local root = enemy:WaitForChild("HumanoidRootPart")

	local detectPlayerRange = enemy:GetAttribute("detectplayer") or 12
	local buildBox = Vector3.new(6,6,6)

	while hum.Health > 0 do
		local playerChar = getClosestPlayer(enemy, detectPlayerRange)
		if playerChar then
			AttackPlayer(enemy, playerChar)
			continue
		end

		local targetBuilding = findClosestBuilding(enemy)
		if not targetBuilding or not targetBuilding.PrimaryPart then
			task.wait(0.2)
			continue
		end

		local path = PathfindingService:CreatePath({
			AgentRadius = 2,
			AgentHeight = 6,
			AgentCanJump = true
		})
		path:ComputeAsync(root.Position, targetBuilding.PrimaryPart.Position)

		if path.Status == Enum.PathStatus.Success then
			for _, waypoint in ipairs(path:GetWaypoints()) do

				local marker = Instance.new("Part")
				marker.Shape = Enum.PartType.Ball
				marker.Material = Enum.Material.Neon
				marker.Color = Color3.fromRGB(0, 255, 0)
				marker.Size = Vector3.new(0.6, 0.6, 0.6)
				marker.Anchored = true
				marker.CanCollide = false
				marker.Position = waypoint.Position
				marker.Parent = workspace

				-- Auto cleanup after 5 seconds to prevent clutter
				game.Debris:AddItem(marker, 5)
			end
			for _, waypoint in ipairs(path:GetWaypoints()) do
				hum:MoveTo(waypoint.Position)
				hum.MoveToFinished:Wait()

				local playerChar = getClosestPlayer(enemy, detectPlayerRange)
				if playerChar then
					AttackPlayer(enemy, playerChar)
					task.wait(0.1)
					--path:ComputeAsync(root.Position, targetBuilding.PrimaryPart.Position)
					break
				end

				local buildInBox = getBuildingInBox(enemy, buildBox)
				if buildInBox then
					attackBuilding(enemy, buildInBox)
					task.wait(0.1)
					--path:ComputeAsync(root.Position, targetBuilding.PrimaryPart.Position)
					break
				end

				--task.wait(0.1)
			end
		end

		task.wait(0.01)
	end
end

return Knob

Above is the main script for calculating pathfinding, The attackplayer and attack building functions also have moveto() inside, but only waits the duration of 0.05 secs + about 0.7secs but thats based on the attack/ attack speed of the enemy. Let me know if you want to see the entire script incase those are the issues.

Im not great with pathfinding so if anyone knows threads with better bulletproof pathfinding let me know

1 Like

To clear some stuff up, Move() does not have a timeout, however MoveTo() does, and it’s 8 seconds. After 8 seconds pass and the NPC still hasn’t gotten to it’s destination, it will return false if you have MoveToFinished:Wait() set, this is useful for checking if NPC has made it or not. Though this doesn’t really help : P. MoveTo Docs

Looking at your code, the freezing appears to happen because you’re calling AttackPlayer() or attackBuilding(). It’s most likely the AttackPlayer() function causes something to break but since I don’t have either the functions, I can’t say for certain whats causing it to break.

Also looking at your code I don’t think you should be making the NPC’s find stuff to attack like that. I don’t have any details about the functions so I can’t say whether or not it’s bad but if I were you I’d

First, make a way to stop the NPC from continuing the Path its currently following. Doing this depends on how you’re currently making the NPC pathfind and move. From what you have (and the way I usually do it), I’d make a task.spawn() (task.defer() is usually better for most cases) and have the move functions and stuff happen there so that it doesn’t make the script wait until the “Follow Path” stuff happens and so I can the new task to stop it from making the NPC follow the path.

Second, make a way to find an enemy. Rather than using 2 functions, I’d combine the functions so that it makes stuff more clean and easier to use and debug. Not sure how you’re finding the closest towers so I can’t really help you on this.

Third, make an attack function that takes in a Target and does damage to it if it’s a tower or if it’s a player. It’ll also check if it’s close the Target first before attacking.

Final script would probably look something like this:

local CurNPCPath = nil -- This variable will be used to store the task we create

function ClearPath()
	if CurNPCPath then
		corutine.close(CurNPCPath)
	end
end

function FindTarget()
	local Target = nil
	-- Find target
	return Target
end

function MoveNPC(EndPos)
	task.defer(function()
		-- move NPC and stuff
	end)
end

function AttackTarget(Target)
	if NPC is near Target then
		Attack Target
	end
end

while true do
	local Target = FindTarget()
	if Target then
		ClearPath()
		MoveNPC(Target.Position)
		AttackTarget(Target)
	end
	task.wait(.25)
end

I understand the idea but Im not fully sure how to implement it like that. From what i see it recalculates every 0.25 the path, but even that may be too frequent, i only want to do that once, when needed as ill have alot of enemies

on each waypoint how mine works, it checks for a nearby player or a nearby building hence the two functions then runs the code to follow in a straight line to the target, and if it cant find a building or player nearby in view it pathfinds again.

but then after a fixed amount of it changing from following a player to pathfinding again, it just decides not to work as intended

These are just the other targeting functions that run,

local function attackBuilding(enemy, target)
	if not (enemy and target and target.PrimaryPart) then return end

	local hum = enemy:FindFirstChild("Humanoid")
	if not hum then return end

	local attackSpeed = enemy:GetAttribute("attackspeed") or 1
	local damage = enemy:GetAttribute("damage") or 10
	local attackRange = enemy:GetAttribute("enemybuildrange")
	local detectPlayerRange = enemy:GetAttribute("detectplayer") or 12
	local plrDamageDist = enemy:GetAttribute("plrdmgdistance") or 6

	while hum and hum.Health > 0 and target and target.PrimaryPart do
		local playerChar, distToPlayer = getClosestPlayer(enemy, detectPlayerRange)
		if playerChar and distToPlayer <= plrDamageDist then
			return AttackPlayer(enemy, playerChar)
		end

		local dist = (enemy.PrimaryPart.Position - target.PrimaryPart.Position).Magnitude
		if dist > attackRange * 2 or (target:GetAttribute("health") and target:GetAttribute("health") <= 0) then
			break
		end

		if dist > attackRange then
			hum:MoveTo(target.PrimaryPart.Position)
		else
			if target:GetAttribute("health") then
				target:SetAttribute("health", math.max(target:GetAttribute("health") - damage, 0))
			end
			task.wait(attackSpeed-0.05)
			local stillClose = (enemy.PrimaryPart.Position - target.PrimaryPart.Position).Magnitude <= attackRange
			if not stillClose or (target:GetAttribute("health") or 0) <= 0 then
				break
			end
		end
		task.wait(0.05)
	end
end

function AttackPlayer(enemy, playerChar)
	local hum = enemy:FindFirstChild("Humanoid")
	if not hum or not playerChar then return end

	local attackSpeed = enemy:GetAttribute("attackspeed") or 1
	local plrDamage = enemy:GetAttribute("plrdamage") or 10
	local detectPlayerRange = enemy:GetAttribute("detectplayer") or 12
	local plrDamageDist = enemy:GetAttribute("plrdmgdistance") or 6

	while hum and hum.Health > 0 and playerChar and playerChar:FindFirstChild("HumanoidRootPart") do
		local playerHum = playerChar:FindFirstChild("Humanoid")
		if not playerHum or playerHum.Health <= 0 then break end

		local dist = (playerChar.HumanoidRootPart.Position - enemy.PrimaryPart.Position).Magnitude
		if dist > detectPlayerRange then break end

		if dist > plrDamageDist then
			hum:moveTo(playerChar.HumanoidRootPart.Position)
		else
			playerHum:TakeDamage(plrDamage)
			task.wait(attackSpeed)
			local stillClose = (playerChar.HumanoidRootPart.Position - enemy.PrimaryPart.Position).Magnitude <= plrDamageDist
			if not stillClose or playerHum.Health <= 0 then
				break
			end
		end
		task.wait(0.05)
	end
end

Well I don’t see anything wrong with the attack functions, so it could be related to how you’re finding the towers/players. Also did you set the set networkowner of the NPC to nil? Don’t think it would help much but try setting the HumanoidRootPart’s ownership to nil: HumRoot:SetNetWorkOwnership(nil). Also try putting print statements everywhere to see when it exactly stops working.

You can also use NoobPath if you don’t want to make your own Path System. Never actually used it before but I do believe it’s pretty efficient.

Code Review

In the while loop you don’t need to constantly check for the hum, target, or target.PrimaryPart:

if not (enemy and target and target.PrimaryPart) then return end

if not hum then return end
-- your other code

while hum and hum.Health > 0 and target and target.PrimaryPart do

Since you’ve already checked if they exist, you can remove if from the while statement and be left with this: while hum.Health > 0 do. Unless the instance for some reason gets destroyed while it’s looping, in which you should be doing hum.Parent ~= nil, you don’t need to put those in the while loop.