How to handle multiple NPCs attacking a player

In most Roblox games, enemies independently chase you and bunch up, colliding into each other and attacking all at once. While this can be sufficient in a zombie game, a more advanced combat game would get chaotic and overwhelming. In many popular games outside of Roblox melee NPCs tend to wait for fellow melee NPCs to finish their attack before striking. They walk up to different points around the player, instead of all heading directly for the player and ending up bunched up over other NPCs.

While I have some ideas as to how I can make a less chaotic system such that enemies consider each other and spread out and attack at different moments, it seems like a common enough mechanic that its worth checking if any of you know any guides to achieving this or have implemented this yourself/have tips to achieve this.

You can store all the NPC inside a folder in the workspace, then handle all the same NPC which have the same attacks and cooldowns using a temporary while loop including for i, Npc in pairs (Folder:GetChildren())do

There is an example i juste made for you, of course there is simple and better optimized way to do it using tables and modules but yeah it is just an example ^^:

--//Variables//--
local WorkspaceService = game:GetService("Workspace")
local FolderNPC = WorkspaceService:WaitForChild("NPC" ,300)

local NormalZombieEnabled = false
local StrongZombieEnabled = false

--//Functions//--
local function NormalZombie()
	NormalZombieEnabled = true
	
	while NormalZombieEnabled == true do
		task.wait()
		for _,Child in pairs(FolderNPC:GetChildren())do
			if Child:IsA("Model")and Child.Name == "NormalZombie" then
				--Detection / Move / Attack / cooldown
			end
		end
	end
end

local function StrongZombie()
	StrongZombieEnabled = true

	while StrongZombieEnabled == true do
		task.wait()
		for _,Child in pairs(FolderNPC:GetChildren())do
			if Child:IsA("Model")and Child.Name == "StrongZombie" then
				--Detection / Move / Attack / cooldown
			end
		end
	end
end

local function Activation(NPC)
	if NPC.Name == "NormalZombie" and NormalZombieEnabled == false then
		coroutine.resume(coroutine.create(function()
			NormalZombie()
		end))
	elseif NPC.Name == "StrongZombie" and StrongZombieEnabled == false then
		coroutine.resume(coroutine.create(function()
			StrongZombie()
		end))
	end
end

local function Desactivation(NPC)
	local Remaining = false
	
	for _,Child in pairs(FolderNPC:GetChildren())do
		if Child:IsA("Model")and Child.Name == NPC then
			Remaining = true
		end
	end
	
	if Remaining == false and NPC.Name == "NormalZombie" then
		NormalZombieEnabled = false
	elseif Remaining == false and NPC.Name == "StrongZombie" then
		StrongZombieEnabled = false
	end
end

--//Connections//--
FolderNPC.ChildAdded:Connect(function(NPC)
	if NPC:IsA("Model") and NPC:FindFirstChild("Humanoid")then
		Activation(NPC)
	end
end)

FolderNPC.ChildRemoved:Connect(function(NPC)
	Desactivation(NPC)
end)