How to make a script that controls multiple npcs w/ pathfinding?

Hello, so I have a script inside an NPC, and I know that having multiple npcs with one script in each of them would cause a lot of lag, so I tried making a script that controls every npc tagged with ZombieNPC, but it didn’t optimize it; in fact it made it even laggier, here is the script:

local CollectionService = game:GetService("CollectionService")
local MaxDistance = 500
local PathfindingService = game:GetService("PathfindingService")
local ServerStorage = game:GetService("ServerStorage")
local Target
local BlockedConnection = nil
local function PathfindingFunction(Enemy, Character) -- Find a new path and then make NPC walk it
	local path = PathfindingService:CreatePath({
		AgentRadius = 3,
		AgentHeight = 6,
		AgentCanJump = true
	})
	local HumanoidRootPart = Character.HumanoidRootPart
	local Humanoid = Character.Humanoid
	path:ComputeAsync(HumanoidRootPart.Position, Enemy.HumanoidRootPart.Position)
	local waypoints = path:GetWaypoints()
	if path.Status == Enum.PathStatus.Success then
		for i, v in pairs(waypoints) do
			if v ~= waypoints[5] then
				Humanoid:MoveTo(v.Position)
				if v.Action == Enum.PathWaypointAction.Jump then
					Humanoid.Jump = true
				end
				if Enemy.Humanoid.Health == 0 then
					Humanoid:MoveTo(HumanoidRootPart.Position)
					break
				end
			elseif v == waypoints[5] then
				break
			end
		end
		if BlockedConnection == nil then
			BlockedConnection = path.Blocked:Connect(function()
				PathfindingFunction(Enemy, Character)
				Humanoid.Jump = true
				BlockedConnection:Disconnect()
				-- recompute the path
			end)
		end
	end
end

local function ClosestTarget(Character) -- Find the closest target
	local Humanoid = Character.Humanoid
	local HumanoidRootPart = Character.HumanoidRootPart
	local PreviousMaxDistance = MaxDistance
	for i, v in ipairs(workspace:GetChildren()) do
		if v ~= Character and v:FindFirstChild("Humanoid") and v.Humanoid.DisplayName ~= Humanoid.DisplayName and v:FindFirstChild("HumanoidRootPart") and (v.HumanoidRootPart.Position - HumanoidRootPart.Position).magnitude <= MaxDistance and v.Humanoid.Health > 0 then
			Target = v
			MaxDistance = (Target.HumanoidRootPart.Position - HumanoidRootPart.Position).magnitude
		end
	end
	MaxDistance = PreviousMaxDistance
	return Target
end

-- Get already existing zombies
for i, v in ipairs(workspace:GetChildren()) do
	if v:HasTag("ZombieNPC") then
		local ZombieCoroutine
		ZombieCoroutine = coroutine.create(function()
			local d = false
			local Character = v
			local HumanoidRootPart = Character.HumanoidRootPart
			while task.wait() do
				local target = ClosestTarget(Character)
				if target then
					if (target.HumanoidRootPart.Position - HumanoidRootPart.Position).magnitude > MaxDistance then
						target = nil
					end
				elseif target == nil then
					target = nil
				end
				if target then
					if (target.HumanoidRootPart.Position - HumanoidRootPart.Position).magnitude > 5 and target.Humanoid.Health ~= 0 then
						PathfindingFunction(target, Character)
						-- follow with pathfinding
					elseif (target.HumanoidRootPart.Position - HumanoidRootPart.Position).magnitude <= 5 and target.Humanoid.Health ~= 0 then
						spawn(function()
							if not d then
								d = true
								-- stop the pathfinding and stop the character
								target.Humanoid:TakeDamage(10)
								task.wait(0.2)
								d = false
							end
						end)
					end
				elseif not target then
				end
			end
		end)
		coroutine.resume(ZombieCoroutine)
		v.Humanoid.Died:Connect(function()
			-- Ragdoll
			coroutine.yield(ZombieCoroutine)
			coroutine.close(ZombieCoroutine)
		end)
		
	end
end

-- If any new zombie is added in the workspace, then this script checks that
workspace.ChildAdded:Connect(function(object)
	if object:HasTag("ZombieNPC") then
		local ZombieCoroutine
		ZombieCoroutine = coroutine.create(function()
			local d = false
			local Character = object
			local HumanoidRootPart = Character.HumanoidRootPart
			while task.wait() do
				local target = ClosestTarget(Character)
				if target then
					if (target.HumanoidRootPart.Position - HumanoidRootPart.Position).magnitude > MaxDistance then
						target = nil
					end
				elseif target == nil or target.Humanoid.Health == 0 then
					target = nil
				end
				if target then
					if (target.HumanoidRootPart.Position - HumanoidRootPart.Position).magnitude > 5 then
						PathfindingFunction(target.HumanoidRootPart.Position, Character)
						-- follow with pathfinding
					elseif (target.HumanoidRootPart.Position - HumanoidRootPart.Position).magnitude <= 5 then
						spawn(function()
							if not d then
								d = true
								-- stop the pathfinding and stop the character
								Character.Humanoid:MoveTo(HumanoidRootPart.Position)
								target.Humanoid:TakeDamage(10)
								task.wait(0.2)
								d = false
							end
						end)
					end
				elseif not target then
				end
			end
		end)
		coroutine.resume(ZombieCoroutine)
		object.Humanoid.Died:Connect(function()
			-- Ragdoll
			coroutine.yield(ZombieCoroutine)
			coroutine.close(ZombieCoroutine)
		end)
	end
end)

How can I make this script less laggy and better?

3 things:

  • Pathfinding may be laggy no matter what
  • If the script is server side, then the lag could simply be the delay between sent packets
  • depending on the scenario, for loops may be slower than an individual script for each enemy.

You don’t need to put the same script into each npc, you should instead use CollectionService, which makes it alot easier and if you change something in the script, you don’t need to do it multiple times.