Pathfinding becomes worse as more enemies continue to spawn

Heya there.

I’ve created a pathfinding module that handles the pathfinding of the majority of the enemies in my game. How it works is simple, if an enemy detects a player near them, target them. Otherwise, target the objective (The thing players are protecting).

The issue is that the enemy’s pathfinding seems to progressively worsens when more enemies spawn as seen in the video below. You can see that the first enemy acts fine, but as more enemies spawn, the first enemy starts to twitch out to the point where they can’t even move.

Here’s the pathfinding module.

--[[SERVICES]]--
local PathfindingService = game:GetService("PathfindingService")
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")

--[[MODULE]]--
local PathfindingModule = {}

function PathfindingModule.Pathfind(NPC, AggroRange)
	--//Preperation
	local Objective = game.Workspace:FindFirstChild("Crystal")
	--Enemy
	local RootPart = NPC.PrimaryPart
	local Humanoid = NPC.Humanoid
	local CharacterSize = NPC:GetExtentsSize()

	--RootPart:SetNetworkOwner(nil)

	--//Pathfinding Agents
	local PathfindingAgents = {
		AgentRadius = (CharacterSize.X + CharacterSize.Z)/4,
		AgentHeight = CharacterSize.Y,
		AgentCanJump = true
	}

	--[[PATHFINDING]]--
	local function FollowPath(Destination)
		local NewPath = PathfindingService:CreatePath(PathfindingAgents)
		NewPath:ComputeAsync(RootPart.Position, Destination)

		if NewPath.Status == Enum.PathStatus.Success then
			local Waypoints = NewPath:GetWaypoints()

			for _, Waypoint in ipairs(Waypoints) do

				if Waypoint.Action == Enum.PathWaypointAction.Jump then
					Humanoid.Jump = true
				end

				--Just in case
				NewPath.Blocked:Connect(function(blockedWaypointIdx)
					print("The enemy's path is blocked. Recalculating.")
					if Objective and Objective.PrimaryPart then
						FollowPath(Objective.PrimaryPart.Position)
					end
				end)

				Humanoid:MoveTo(Waypoint.Position)
				local Timer = Humanoid.MoveToFinished:Wait(1)
				if not Timer then
					print("Path timed out. Recalculating.")
					if Objective and Objective.PrimaryPart then
						FollowPath(Objective.PrimaryPart.Position)
					end
				end
			end
		else
			print("Path failed. Recalculating. Sorry.")
		end
	end

	local function FindNearestTarget()
		local NearestTarget = nil
		local PlayerList = Players:GetPlayers()

		for _, Player in pairs(PlayerList) do
			local Character = Player.Character or Player.CharacterAdded:Wait()

			if Character and Character.PrimaryPart then
				local Distance = (Character.PrimaryPart.Position - RootPart.Position).Magnitude
				if Distance <= AggroRange then
					NearestTarget = Character
				else
					NearestTarget = Objective
				end
			end
		end

		return NearestTarget
	end

	RunService.Heartbeat:Connect(function()
		local Target = FindNearestTarget()

		if Target and Target.PrimaryPart then
			--//Checking distance to be safe.
			local Distance = (Target.PrimaryPart.Position - RootPart.Position).Magnitude

			if Distance <= AggroRange then
				Objective = Target
			elseif not Target and Distance >= AggroRange then
				Objective = game.Workspace:FindFirstChild("Crystal")
			end
		else
			print("Character's RootPart cannot be found.")
		end

		if Objective and Objective.PrimaryPart then
			FollowPath(Objective.PrimaryPart.Position)
		end
	end)
end

return PathfindingModule
2 Likes

Pathfinding is extremely expensive, especially in a case like this where you’re pathfinding on every heartbeat, pathfinding yields and heartbeat ignores yielding calls so your pathfinding isn’t finishing which results in that jittering. You should only be pathfinding when you absolutely have to, in a case like this, there’s a somewhat simple optimization you can make which will radically improve the performance here.

What you should try is calculate the line of sight between the enemy, and the target. This can be done most simply with a raycast from the position of the head of the enemy to the position of the target.

You would use a filter to exclude the target and the enemy themselves from the raycast, then if the raycast hits nothing then that means there’s no wall in the way, and you can tell the enemy to skip pathfinding and just walk directly towards the target’s position with Humanoid:MoveTo().

If the raycast hits something that means there’s a wall in the way then you can path to the target, you should only recalculate this path when they reach the end of their current path as well (or if they switch targets).

I would recommend to clamp the length of the raycast as longer rays are more expensive and if the player is very far from an enemy they’re likely behind a wall anyways.

Raycasting is fast enough that it might be okay to do it every frame for a few enemies, but I would strongly recommend clamping the speed at which you raycast, maybe once every 5th of a second would be fine. (You would use a while loop for this, like while the enemy is alive or something since Heartbeat() ignores task.wait.)

So I’d essentially just constantly raycast using the enemy head and if it hits something, pathfind? Alright.

1 Like