Update.
I tried making the optimization. Here’s what I achieved:
-- Services
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
-- Modules
local Network = require(ReplicatedStorage.Shared.Network)
-- Init
local Step = 1 / 20
local Timer = 0
-- Data
local Components = {
Count = 0,
Position = { X = {}, Y = {}, Z = {} },
Health = {},
State = {},
Target = {},
Type = {},
}
local SpawnModule = {}
-- Local Functions
local function GetPosition(Position1, Position2)
local Speed = 30
local direction = (Position2 - Position1).Unit
local newPos = Position1 + direction * Speed * (1 / 20)
return newPos
end
local function GetPlayerInRaidus(Position, MinStuds: number): Model?
for _, Player in Players:GetPlayers() do
local Character = Player.Character
if not Character then
continue
end
local HRP = Character:FindFirstChild("HumanoidRootPart")
if not HRP then
continue
end
local Distance = (Position - HRP.Position).Magnitude
if Distance <= MinStuds then
return Character
end
end
return nil
end
local function Clear(id: number)
Components.Position.X[id] = nil
Components.Position.Y[id] = nil
Components.Position.Z[id] = nil
Components.Health[id] = nil
Components.State[id] = nil
Components.Target[id] = nil
Components.Type[id] = nil
end
-- Functions
function SpawnModule.Spawn(Type: string, Position: {})
local PosX, PosY, PosZ = Position.X, Position.Y, Position.PosZ
Components.Count += 1
Components.Position.X[Components.Count] = PosX
Components.Position.Y[Components.Count] = PosY
Components.Position.Z[Components.Count] = PosZ
Components.Health[Components.Count] = 100
Components.State[Components.Count] = 1
Components.Target[Components.Count] = nil
Components.Type[Components.Count] = Type
Network:FireAllClients("spawnEnemy", Components.Count, Type, PosX, PosY, PosZ, 100, 1)
return Components.Count
end
function SpawnModule.UpdateEnemies(dt)
Timer += dt
if Timer < Step then
return
end
Timer = 0
local FinalData = {}
for i = 1, Components.Count do
local PosX, PosY, PosZ = Components.Position.X[i], Components.Position.Y[i], Components.Position.Z[i]
local ComputedPos = Vector3.new(PosX, PosY, PosZ)
local Target = Components.Target[i]
local Health = Components.Health[i]
if Health <= 0 then
Clear(i)
continue
end
local Character = GetPlayerInRaidus(ComputedPos, 50)
if not Character then
Components.State[i] = 1
Components.Target[i] = nil
continue
end
Components.State[i] = 2
if not Target then
Components.Target[i] = Character
else
local HRP = Character:FindFirstChild("HumanoidRootPart")
if HRP then
local currentPos = GetPosition(ComputedPos, HRP.Position)
if currentPos then
Components.Position.X[i] = currentPos.X
Components.Position.Y[i] = currentPos.Y
Components.Position.Z[i] = currentPos.Z
ComputedPos = Vector3.new(currentPos.X, currentPos.Y, currentPos.Z)
end
end
end
FinalData[i] = {}
FinalData[i].Position = ComputedPos
FinalData[i].State = Components.State[i]
FinalData[i].Target = Components.Target[i]
end
Network:FireAllClients("syncEnemy", FinalData)
end
-- Return
return SpawnModule
Also, I did some tests, but I can’t tell if they are good or no:
10 NPC at a time (maximum Recv. = 16 KB/s)

100 NPC at a time (Maximum Recv. = 80 KB/s)

500 NPC at a time (Maximum Recv. 700 KB/s)
FPS was around the same at every test, because I used Parts to render current position of enemies instead of custom Rigs.