Procedurally generated npc squadron formations (coroutine help)

Hello! recently, ive tried coding my own enemy AI into my game. I was able to get the core functionality for individual members, but later realized I wanted to have these enemies move in groups.
example

what i want to do:
npc groups consisting of a leader and member(s), with the members following the leader’s position

problems currently arising:

  • core movement sort of works, but the members only move after the leader reaches x waypoint, i hope to have the members move along the leader
  • concerned about performance
  • for some reason after some time my leader gets super slow with pathfinding and takes like 3x longer to reach waypoints than before
-- services --
local pfs = game:GetService("PathfindingService")
local collectionService = game:GetService("CollectionService")

local squadrons = collectionService:GetTagged("Squadron") -- list of all squadrons in workspace

local waypointFolder = workspace.Waypoints

local squadronSizes = {}
local NPCOffsets = {}
 
for _, squad in ipairs(squadrons) do	-- adding the npc offsets into a list, will be useful in moving squad members(not leaders)
	local squadronOffsets = {} --creating a seperate table for each of the squadrons holding member offsets
	squadronSizes[squad] = squad:GetExtentsSize().X / 2
	
	local leader
	
	for _, child in ipairs(squad:GetChildren()) do 	--finding the leader of the squadron
		if collectionService:HasTag(child, "Leader") then
			leader = child
			break
		end
	end
	
	for i, member : Model in squad:GetChildren() do --loop through all members of all squadrons
		if member == leader or not member:IsA("Model") then continue end
		
		local leaderPos = leader.PrimaryPart.Position
		local memberPos = member.PrimaryPart.Position
		
		local offset = leaderPos - memberPos
		
		table.insert(squadronOffsets, offset)
	end
	NPCOffsets[squad] = squadronOffsets
end

function GetRandomWaypoint(waypointId : string)				--getting a random waypoint with given waypointID
	for _, folder in ipairs(waypointFolder:GetChildren()) do
		if folder:GetAttribute("WaypointId") ~= waypointId then continue end -- in the waypoint folder, there should be various other folders, each with their own tag to represent what group of waypoints they hold, i.e. kitchen or hallway

		local waypoints = folder:GetChildren()
		local waypoint = waypoints[math.random(1, #waypoints)]

		return waypoint.Position
	end
end

game.Players.PlayerAdded:Connect(function(player)
	player.CharacterAdded:Connect(function(character)
		local modifier = Instance.new("PathfindingModifier")
		modifier.Label = "Player"
		modifier.PassThrough = true
		modifier.Parent = character
	end)
end)

function MoveLeader(squad : Model, leader : Model, destination : Vector3) --designated script for moving the leader, not necessary for other members as their position is based off leaders position
	local hum : Humanoid = leader:FindFirstChild("Humanoid")
	if not hum then return end
	
	local path: Path = pfs:CreatePath({ 		--initializing path config
		AgentRadius = squadronSizes[squad], --getting the size of the squadron
		AgentCanJump = true,
		AgentCanClimb = false,
		Costs = {
			Player = 0 -- make the pathfinding completely ignore players
		}
	})
	
	path:ComputeAsync(leader.PrimaryPart.Position, destination)
	
	if path.Status == Enum.PathStatus.NoPath then warn("no path found >:(   trying to get to "..tostring(destination)) return end
	
	if path.Status == Enum.PathStatus.Success then
		for _, waypoint in ipairs(path:GetWaypoints()) do
			hum:MoveTo(waypoint.Position)
			hum.MoveToFinished:Wait()
		end
	end
end

while task.wait() do
	for _, squad in ipairs(squadrons) do
		local leader
		
		local squadOffsetTable = {} --saving the offsetTable so we can remove values as we assign offsets to the members
		for _, value in ipairs(NPCOffsets[squad]) do
			table.insert(squadOffsetTable, value)
		end
		
		for i, member : Model in squad:GetChildren() do --loop through all members of all squadrons
			if not member:IsA("Model") or not member:FindFirstChild("HumanoidRootPart") then continue end -- making sure its a rig

			if member:HasTag("Leader") then 				--[FOR SQUAD LEADERS]
				local leaderHum : Humanoid = member:FindFirstChild("Humanoid")
				if not leaderHum then continue end

				leader = member

				local desiredWaypoint = GetRandomWaypoint("hallway")

				MoveLeader(squad, member, desiredWaypoint)
			else 											--[FOR REGULAR SQUAD MEMBERS]
				local hum : Humanoid = member:FindFirstChild("Humanoid")
				local closestOffset
				local closestIndex
				local shortestDistance

				for index, offset in ipairs(squadOffsetTable) do 	-- make the NPC move to the closest offset to its position
					local memberPos = member.PrimaryPart.Position
					local distance = (memberPos - offset).Magnitude

					if shortestDistance == nil or distance < shortestDistance then
						shortestDistance = distance
						closestOffset = offset
						closestIndex = index
					end
				end

				if closestOffset then
					table.remove(squadOffsetTable, closestIndex) -- remove the offset after the closest one is found to prevent multiple NPCS going to the same offset
					hum:MoveTo(leader.PrimaryPart.Position - closestOffset)
				else
					warn("no available offset for member: "..member.Name)
				end
			end
		end
	end
end

this script looks very scary, but really im just trying to figure out how to make the members move with the leader

any advice / tips would be appreciated! :smiling_face_with_three_hearts:

I would recommend that you split the code into many functions (especially the big loop at the end) to imporve readability. (you code is shifting so far to the right I have to use the scroll bar…)

anyways, for your first bullet point, you have this function MoveLeader(squad, member, desiredWaypoint)
this should become coroutine.wrap(MoveLeader)(squad, member, desiredWaypoint)

this would cause leader to prematurly go to waypoints, to fix this, on the last line of for _, squad in ipairs(squadrons) do do leader.Humanoid.MovedToFinish:Wait()

I would recommend using parallel luau to multi thread the NPC code so it doesn’t lag up the FPS