Pathfinding gets stuck most of the time with over 3 NPC's

I am using pathfinding to make my zombies game, and I run into quite a lot of issues with it.

I represent here the path the zombie takes, and for some reason when there are above like 3 zombies it starts to lag and only run once per second, and my client memory is extremely high for some reason:
https://gyazo.com/64d03fcc2193b041f628661038577403.png

This is the non-smooth movement:
https://gyazo.com/aeaa59fded055f89f2920e626e205d2e

And this is the smooth movement I expect to have with all my zombies:
https://gyazo.com/7c128e1386fe41baa52178cd79cdaf41

My server runs one master script, that uses collection service to pretty much control all the zombies.
I use runService.Stepped to repeat the process, and this may not be efficient at all and i’d like some support and help on the topic.

This is my main code:
(My Zombies are called Zetos in this instance)

rs.Stepped:Connect(function()
	for _,zeto in pairs(CollectionService:GetTagged("Zeto")) do -- Getting the Tagged Zombies
		if zeto:FindFirstChild("Humanoid") then
			if zeto.Humanoid.Health > 0 and zeto.Spawning.Value == false then -- checks if they are spawning

				local human = zeto.Humanoid
				local hroot = zeto1.HumanoidRootPart
				local head = zeto1:FindFirstChild("Head")
				local vars = zeto1.vars
				
				human:SetStateEnabled(Enum.HumanoidStateType.Physics,false) -- i heard this helps?

				local nrstt = GetTorso(hroot.Position,zeto) -- nrstt is a function that finds the player location
				if nrstt ~= nil and human.Health > 0 then -- if player detected	
					vars.Chasing.Value = true
					
					local function checkw(t) -- a function that pretty much calculates the current waypoint and returns when requested. (calculates everything in a NUTSHELL)
						local ci = 3
						if ci > #t then
							ci = 3
						end
						if t[ci] == nil and ci < #t then
							repeat ci = ci + 1 wait() until t[ci] ~= nil
							return Vector3.new(1,0,0) + t[ci]
						else
							ci = 3
							return t[ci]
						end
					end
					
					path = pfs:FindPathAsync(hroot.Position, nrstt.Position)
					waypoint = path:GetWaypoints()
					local connection;
					
					local direct = Vector3.FromNormalId(Enum.NormalId.Front)
					local ncf = hroot.CFrame * CFrame.new(direct)
					direct = ncf.p.unit
					local rootr = Ray.new(hroot.Position, direct)
					local ignorel = {workspace.Zetos.Storage,hroot,workspace.Rays}
					local phit, ppos = game.Workspace:FindPartOnRayWithIgnoreList(rootr, ignorel)

					if path and waypoint or checkw(waypoint) then
						if checkw(waypoint) ~= nil and checkw(waypoint).Action == Enum.PathWaypointAction.Walk then
							
							human:MoveTo( checkw(waypoint).Position )
							
							local part = Instance.new("Part") -- this is just to visualize the nodes
							part.Shape = "Ball"
							part.Material = "Neon"
							part.Size = Vector3.new(0.6, 0.6, 0.6)
							part.Position = checkw(waypoint).Position
							part.Anchored = true
							part.CanCollide = false
							part.Parent = game.Workspace
							
							game:GetService("Debris"):AddItem(part,.1)

							human.Jump = false
						end
						
						if checkw(waypoint) ~= nil and checkw(waypoint).Action == Enum.PathWaypointAction.Jump then
							connection = human.Changed:connect(function()
								human.Jump = true
							end)
							human:MoveTo( waypoint[4].Position )
						else
							human.Jump = false
						end

						if connection then
							connection:Disconnect()
						end
						
					else
						for i = 3, #waypoint do
							human:MoveTo( waypoint[i].Position )	
						end
					end
					path = nil
					waypoint = nil
				elseif nrstt == nil then -- if player not detected
					vars.Chasing.Value = false
					path = nil
					waypoint = nil
					human.MoveToFinished:Wait()
				end
			end
		end
	end
end)

Now, I need help realizing what’s wrong with the script, or my methods?

1 Like

FindPathAsync was deprecated for a good reason. Use pfs:CreatePath() to get a path object and :ComputeAsync(p0,p1) on it and then :GetWaypoints().

Path
ComputeAsync
GetWaypoints

Hey, thanks for the response, but where do I replace/add ComputeAsync?

You will probably have to rewrite the entire code, or add a second table containing all paths which get added as a zombie spawns in. After this, you just need to use :ComputeAsync(p0,p1) and :GetWaypoints().

Another thing to note is that you could speed up the script by adding a third table to store the player’s positions since the last update (0,0,0 by default) and check if the distance between the current and the last position is more than the range they can damage at. If it is, use :ComputeAsync and update the table value; otherwise, just skip this and use :GetWaypoints directly.

Will re-writing the code and replacing it with these new functions, smooth up the stuff?

In addition - I should still keep it in the RunService.Stepped function?

Would you mind sharing your GetTorso function’s code or what it does? I have a feeling like that may play a factor in this a bit or maybe a lot.

Sure, here you go:

	local chars = game.Workspace:GetChildren()
	local chaseRoot = nil
	local chaseTorso = nil
	local chasePlr = nil
	local chaseHuman = nil
	local mag = 800
	for i = 1, #chars do
		chasePlr = chars[i]
		if chasePlr:IsA'Model' and chasePlr ~= zeto then
			chaseHuman = getHumanoid(chasePlr)
			chaseRoot = chasePlr:FindFirstChild'HumanoidRootPart'
			if chaseRoot ~= nil and chaseHuman ~= nil and chaseHuman.Health > 0 and not chaseRoot.Parent:FindFirstChild("HasBlade") then
				if (chaseRoot.Position - part).magnitude < mag and (chaseRoot.Position - part).magnitude > 7 then
					chaseName = chasePlr.Name
					chaseTorso = chaseRoot
					mag = (chaseRoot.Position - part).magnitude
				elseif (chaseRoot.Position - part).magnitude < 7 and zeto.Attacked.Value == false  then
					chaseName = chasePlr.Name
					chaseTorso = chaseRoot
					mag = (chaseRoot.Position - part).magnitude
					zeto.Attacked.Value = true
					local aAnim = zeto.Humanoid:LoadAnimation(script.ZetoAnimations.Attack)
					aAnim:Play()
					createBlade(zeto.HumanoidRootPart,zeto)
					wait(zeto.Configuration.AttackCooldown.Value)
					if zeto:FindFirstChild("Attacked") then
					zeto.Attacked.Value = false
					end
				end
			end
		end
	end
	return chaseTorso
end```

It could. When I said rewrite, I meant adding tables to avoid the checks, such as players alive, updateable zombies, moving the function to calculate the waypoints out of the Stepped event, etc…

There it is, that’s the culprit. Your GetTorso code is iterating through the workspace every frame when it needs to find a torso, which is insanely expensive and taxing on performance. Even worse when multiple NPCs do it.

You’ll need to collect Humanoids a better way, such as only iterating through the characters of players returned from Players.GetPlayers or creating a separate “ZetoTargetList” tag which is added to character Humanoids on spawn as well as anything else an enemy should target.

5 Likes

Alright, I will try this in a few moments and update you and @nooneisback@!

Colbert, this seems to actually work!

But I will use your advie too @nooneisback

But still, I have some things to change, like seems like adding the humanoid to a tag will be a better way.

Looking good there. From here, you can focus on the improvement of your code and touching up on that pathfinding. Anything that looks like it may have major performance-taxing implications, try to find an alternate way to do it. Don’t bother with a few miliseconds worth of saving though (micro-optimisations).

Typically, I prefer having an array of the targets I need to go through since that’s a lot easier on me, which means the usage of the CollectionService to tag humanoids. Before then, I used Roblox’s implementation of collecting and caching humanoids to get my target arrays, though it’s expensive to compute each iteration and doesn’t suit my overall needs.

ROBLOX_HumanoidList ModuleScript
local humanoidList = {}
local storage = {}

function humanoidList:GetCurrent()
	return storage
end

local function findHumanoids(object, list)
	if object then
		if object:IsA("Humanoid") then
			table.insert(list, object)
		end

		for _, child in pairs(object:GetChildren()) do
			local childList = findHumanoids(child, list)
		end
	end
end

local updateThread = coroutine.create(function()
	while true do
		storage = {}
		findHumanoids(game.Workspace, storage)
		wait(3)
	end
end)

coroutine.resume(updateThread)

return humanoidList

It’s of course modifiable, but there’s nothing better than just adding a tag to your humanoid targets and iterating through those whenever humanoids need to be found. It’s less taxing and you rarely need to handle any edge cases, if any at all.

Good luck with that game of yours.

1 Like

Thank you colbert, but regarding the script you sent, what is it used for?

I have create a new tag-group and when the Character is added it is added to the tag group:

			CollectionService:AddTag(h, "Targets")
			
			h.Died:Connect(function()
				CollectionService:RemoveTag(h, "Targets")
			end)

and in my script I have made this:

	local mag = 800
	local chasePlr
	local chaseRoot
	local chaseHuman
	for _,hum in pairs(CollectionService:GetTagged("Targets")) do
		chaseHuman = hum
		chasePlr = hum.Parent
		chaseRoot = chasePlr:FindFirstChild("HumanoidRootPart")
		if chasePlr:IsA'Model' and chasePlr ~= zeto then
			if chaseRoot ~= nil and chaseHuman ~= nil and chaseHuman.Health > 0 and not chaseRoot.Parent:FindFirstChild("HasBlade") then
				if (chaseRoot.Position - part).magnitude < mag and (chaseRoot.Position - part).magnitude > 7 then
					chaseName = chasePlr.Name
					mag = (chaseRoot.Position - part).magnitude
				elseif (chaseRoot.Position - part).magnitude < 7 and zeto.Attacked.Value == false  then
					chaseName = chasePlr.Name
					mag = (chaseRoot.Position - part).magnitude
					zeto.Attacked.Value = true
					local aAnim = zeto.Humanoid:LoadAnimation(script.ZetoAnimations.Attack)
					aAnim:Play()
					createBlade(zeto.HumanoidRootPart,zeto)
					wait(zeto.Configuration.AttackCooldown.Value)
					if zeto:FindFirstChild("Attacked") then
					zeto.Attacked.Value = false
					end
				end
			end
		end
	end
	return chaseRoot
end```

Still not sure if it's the most efficient way, but thats how iv'e done it.

Oh, don’t worry about that code. I just posted it to show you Roblox’s implementation of collecting humanoids which was to iterate through the workspace and cache any humanoids, except it was done every three seconds while retrieval was instant. This is what I used before CollectionService. The one you’re currently using is perfectly fine as it is.

1 Like

Thanks, i’ll update any other things I may have trouble with.

Hey colbert, this seems to be happening again with an average amount of 8-10 enemies

This time i’m not pretty sure what’s happening

It seems like it might be happening when i’m pretty far away from the target for some reason…

I re-wrote the function of getting the player’s location to this cause I was not sure how to get the closest player to the creature, cause otherwise with the other code using collection service it only followed it’s first impression target until it was dead and only then switched to the next player.

I’ll list some important lines that might be causing it, but i’m still in no-way sure about the reason behind this…

function FindNearest(position,zeto)
    local lowest = math.huge
    local NearestPlayer = nil
    for i,v in pairs(game.Players:GetPlayers()) do
        if v and v.Character then
            local distance = (v.Character.HumanoidRootPart.Position - position).magnitude
            if distance < lowest and distance > 7 and v.Character.Humanoid.Health > 0 then
                lowest = distance
                NearestPlayer = v
			elseif distance < 7 and zeto.Attacked.Value == false and zeto.Active.Value == false and v.Character.Humanoid.Health > 0 then
				lowest = distance
				NearestPlayer = v
				zeto.Attacked.Value = true
				local aAnim = zeto.Humanoid:LoadAnimation(zeto.Configuration.Attack)
				aAnim:Play()
				createBlade(zeto.HumanoidRootPart,zeto)
				wait(zeto.Configuration.AttackCooldown.Value)
				if zeto:FindFirstChild("Attacked") then
				zeto.Attacked.Value = false
				zeto.Active.Value = false
				end
           end
        end
    end
	if NearestPlayer then
    return NearestPlayer.Character.HumanoidRootPart
	end
end

Now, it definitely has something to do with the distance of the player, cause after debugging, I moved the player’s character away from the creatures, and they started stuttering, and after moving the player back into the map area it took the creatures about 10-15 seconds to recover their path-finding from what it seemed until they started moving normally again…

It definitely has to do something with my code cause people make path-finding creatures perfectly…

Update, this draft was on for the entire day, I am pretty much sure it’s the detection range, which causes them to just freeze for a while… I don’t know the issue.

I am pretty much gonna post an entire block of my code, sorry about this… :worried:

	for _,zeto in pairs(CollectionService:GetTagged("Zeto")) do
		if zeto:FindFirstChild("Humanoid") then
			if zeto.Humanoid.Health > 0 and zeto.Spawning.Value == false then
				spawn(function()
					local s, msg = pcall(function()
						if zeto.Humanoid then
							if zeto.Humanoid.Health > 0 then
								local zeto1 = zeto
								local human = zeto.Humanoid
								local hroot = zeto1.HumanoidRootPart
								local head = zeto1:FindFirstChild("Head")
								local vars = zeto1.vars
								
								local path
								local waypoint
								local npath
								local chaseName = nil
			
								local nrstt = FindNearest(hroot.Position,zeto)
								if nrstt ~= nil and human.Health > 0 then -- if player detected	
									vars.Chasing.Value = true
									
									local function checkw(t)
										local ci = 3
										if ci > #t then
											ci = 3
										end
										if t[ci] == nil and ci < #t then
											repeat ci = ci + 1 wait() until t[ci] ~= nil
											return Vector3.new(3,0,0) + t[ci]
										else
											ci = 3
											return t[ci]
										end
									end
			
									path = pfs:CreatePath()
									path:ComputeAsync(hroot.Position, nrstt.Position)
									waypoint = path:GetWaypoints()
									local connection;
									
									local direct = Vector3.FromNormalId(Enum.NormalId.Front)
									local ncf = hroot.CFrame * CFrame.new(direct)
									direct = ncf.p.unit
									local rootr = Ray.new(hroot.Position, direct)
									local ignorel = {workspace.Zetos.Storage,hroot,workspace.Rays}
									local phit, ppos = game.Workspace:FindPartOnRayWithIgnoreList(rootr, ignorel)
									
									if path and waypoint or checkw(waypoint) then
										if checkw(waypoint) ~= nil and checkw(waypoint).Action == Enum.PathWaypointAction.Walk then
											
											human:MoveTo( checkw(waypoint).Position )
											
											--[[local part = Instance.new("Part")
											part.Shape = "Ball"
											part.Material = "Neon"
											part.Size = Vector3.new(0.6, 0.6, 0.6)
											part.Position = checkw(waypoint).Position
											part.Anchored = true
											part.CanCollide = false
											part.Parent = game.Workspace
											
											game:GetService("Debris"):AddItem(part,.1)]]
											human.Jump = false
										end
										
										if checkw(waypoint) ~= nil and checkw(waypoint).Action == Enum.PathWaypointAction.Jump then
											connection = human.Changed:connect(function()
												human.Jump = true
											end)
											human:MoveTo( waypoint[4].Position )
										else
											human.Jump = false
										end
				
										if connection then
											connection:Disconnect()
										end
									
									else
										for i = 3, #waypoint do
											human:MoveTo( waypoint[i].Position )	
										end
									end
									path = nil
									waypoint = nil
								elseif nrstt == nil then -- if player not detected
									vars.Chasing.Value = false
									path = nil
									waypoint = nil
									human.MoveToFinished:Wait(.2)
								end
							end
						end
					end)
				end)
			end
		end
	end
end)

Just need some help finding out the issue and debugging that’s all :slight_smile:
Second Edit: It seems like they seem to stutter when I go up on stairs

1 Like

id think its really because of the distance,i usally think of the poor design of the pathfinding system its self such as how NPCS can detect players around parts or objects

I actually read over this code several times over and I can’t seem to find any source of lag. I have an inkling that pathfinding computations are involved considering ComputePathAsync does yield.

As usual, the best kind of advice I can offer for “debugging” is using prints around your code. Not to see which prints run or not, but instead to check the time it takes to complete a block of code.

local pre = tick()
wait()
print(tick() - pre) -- Prints time difference before and after

Another option is observing the MicroProfiler. See any bars that span over several frames? That may be worth addressing.

I made another post about this, but it seems that if I go into an elevated area, such as going up stairs, the humanoids just lose track and stand still / lag like in the example.

But it does seem like, it suddenly jumps to 1-1.5’s
https://gyazo.com/ffb38bbc11cba8bbbb1c7a44c14950fa

oh and suddenly it jumped…

https://gyazo.com/e34c1ef85ece7f0eb3f47c186122b21e

The deprecated version of ComputeRawPathAsync and GetPointCoordinates seems to be working perfectly, no lags or stutters but I don’t know how to detect when to jump or not…

1 Like

Where did you put those time check blocks? Was it around only the path computation or a whole chunk of code? It seems like one of your computations is fairly expensive. Nearly two seconds to complete is pretty bad for time, so the block that’s taking that long is worth investigating.