Hello, so I’m making some game and I need to have around 500-600 enemies on the map (maybe a bit more, anyway <1000)
Here’s the script:
local ss = game:GetService("ServerStorage")
local rs = game:GetService("ReplicatedStorage")
local plrs = game:GetService("Players")
local ps = game:GetService("PhysicsService")
local sss = game:GetService("ServerScriptService")
local assets = rs:WaitForChild("Assets")
local eAI = require(sss.Modules.EnemyAI)
local oc = require(sss.Modules.OrbController)
local es = require(ss.EnemyStats)
local packets = require(rs:WaitForChild("Libs").Packets)
local enemyContainer = workspace:WaitForChild("Enemies")
local playerSpawners = {}
local enemyConnections = {}
local enemyCount = 0
local minDistance = 20
local maxDistance = 20
local maxEnemies = 1000
local die = 1.5 -- remove for prod
local EnemyManager = {}
local function calculateRandomSpawnPosition(playerPos)
local randomAngle = math.rad(math.random(0, 360))
local randomDistance = math.random(minDistance, maxDistance)
return playerPos + Vector3.new(
math.cos(randomAngle) * randomDistance,
1,
math.sin(randomAngle) * randomDistance
)
end
local function updatePlayerSpawners(player)
if player.Character and player.Character.PrimaryPart then
playerSpawners[player.UserId] = player.Character.PrimaryPart.Position
end
end
plrs.PlayerAdded:Connect(function(player)
local charConnection
charConnection = player.CharacterAdded:Connect(function()
updatePlayerSpawners(player)
end)
player.AncestryChanged:Connect(function()
if not player.Parent then
charConnection:Disconnect()
playerSpawners[player.UserId] = nil
end
end)
end)
plrs.PlayerRemoving:Connect(function(player)
playerSpawners[player.UserId] = nil
end)
function EnemyManager.SpawnEnemy(target)
if not target:IsA("Player") then return end
if enemyCount >= maxEnemies then return end
updatePlayerSpawners(target)
local playerPos = playerSpawners[target.UserId]
if not playerPos then return end
local spawnpoint = calculateRandomSpawnPosition(playerPos)
local rand = math.random()
local selectedType = nil
for _, typeData in es.Types do
if rand <= typeData.Chance then
selectedType = typeData.Name
break
end
end
--print(rand, es.Types, selectedType)
local enemyStats = es[selectedType]
local enemy = assets[selectedType]:Clone()
enemy:PivotTo(CFrame.new(spawnpoint + Vector3.new(0, 2, 0)))
enemy:SetAttribute("IsDead", false)
enemy:SetAttribute("DeathTime", 0)
enemy:SetAttribute("Type", selectedType)
enemy.Parent = enemyContainer
local enemyHum = enemy:WaitForChild("Humanoid")
enemyHum.BreakJointsOnDeath = false
enemyHum.MaxHealth = enemyStats.HP
enemyHum.Health = enemyStats.HP
enemyHum.WalkSpeed = enemyStats.Speed
enemyCount += 1
eAI.StartAI(enemy, target)
for _, part in enemy:GetDescendants() do
if part:IsA("BasePart") then
part.CollisionGroup = "Enemies"
part:SetNetworkOwner(target)
end
end
local lastHealth = enemyHum.Health
enemyConnections[enemy] = {}
local healthChangedConn = enemyHum.HealthChanged:Connect(function(newHealth)
if newHealth < lastHealth then
local damage = lastHealth - newHealth
packets.enemyDamaged:Fire(enemy, damage)
end
lastHealth = newHealth
end)
table.insert(enemyConnections[enemy], healthChangedConn)
-- remove for prod
task.delay(die, function()
if enemyHum and enemyHum.Health > 0 then
enemyHum.Health = 0
end
end)
local diedConnection
diedConnection = enemyHum.Died:Connect(function()
diedConnection:Disconnect()
enemyCount -= 1
if enemyConnections[enemy] then
for _, conn in enemyConnections[enemy] do
if typeof(conn) == "RBXScriptConnection" then
conn:Disconnect()
end
end
enemyConnections[enemy] = nil
end
enemy:SetAttribute("IsDead", true)
enemy:SetAttribute("DeathTime", tick())
packets.enemyDied:Fire(enemy)
eAI.StopAI(enemy)
oc.SpawnOrb(enemy.PrimaryPart.Position, "random", target, enemy:GetAttribute("Type"))
end)
end
task.spawn(function()
while true do
task.wait(.5)
for _, enemy in enemyContainer:GetChildren() do
if enemy:GetAttribute("IsDead") and tick() - enemy:GetAttribute("DeathTime") > 1.75 then
enemy:Destroy()
end
end
end
end)
return EnemyManager
As you can see, I’m using the Packets framework for optimization, it indeed helps in some way, but it’s not enough. Also here’s the enemyAI script:
local runs = game:GetService("RunService")
local sss = game:GetService("ServerScriptService")
local ss = game:GetService("ServerStorage")
local plrs = game:GetService("Players")
local pc = require(sss.Modules:WaitForChild("PlayerController"))
local es = require(ss:WaitForChild("EnemyStats"))
local EnemyAI = {}
local activeAIs = {}
function EnemyAI.StartAI(enemy, initialTarget)
local enemyHumanoid = enemy:WaitForChild("Humanoid")
local enemyType = enemy:GetAttribute("Type")
local enemyStats = es[enemyType]
if not enemyStats then return end
if activeAIs[enemy] then
EnemyAI.StopAI(enemy)
end
local lastUpdate = 0
local lastAttack = 0
local UPDATE_INTERVAL = 0.1
local function getNearestPlayer(enemyPos)
local closestPlayer, minDist = nil, enemyStats.Range
for _, player in plrs:GetPlayers() do
local char = player.Character
if char and char.PrimaryPart and char.Humanoid and char.Humanoid.Health > 0 then
local dist = (char.PrimaryPart.Position - enemyPos).Magnitude
if dist <= minDist then
closestPlayer, minDist = player, dist
end
end
end
return closestPlayer
end
local heartbeatConnection
heartbeatConnection = runs.Heartbeat:Connect(function(dt)
lastUpdate = lastUpdate + dt
if lastUpdate < UPDATE_INTERVAL then return end
lastUpdate = 0
if not enemy or not enemy.Parent or enemyHumanoid.Health <= 0 then
EnemyAI.StopAI(enemy)
return
end
local enemyPos = enemy.PrimaryPart and enemy.PrimaryPart.Position
if not enemyPos then return end
local target = getNearestPlayer(enemyPos)
if not target or not target.Character or not target.Character.PrimaryPart then
EnemyAI.StopAI(enemy)
return
end
local targetHumanoid = target.Character.Humanoid
local targetPos = target.Character.PrimaryPart.Position
if targetHumanoid.Health <= 0 then
EnemyAI.StopAI(enemy)
return
end
local distance = (targetPos - enemyPos).Magnitude
local currentTime = tick()
if distance <= enemyStats.Range and currentTime - lastAttack >= enemyStats.AttackSpeed then
local playerController = pc.Controllers[target]
if playerController and targetHumanoid.Health > 0 then
playerController:TakeDamage(enemyStats.Damage)
lastAttack = currentTime
end
elseif distance > enemyStats.Range then
enemyHumanoid:MoveTo(targetPos)
end
end)
local ancestryConnection
ancestryConnection = enemy.AncestryChanged:Connect(function()
if not enemy:IsDescendantOf(game) then
EnemyAI.StopAI(enemy)
end
end)
activeAIs[enemy] = {
Heartbeat = heartbeatConnection,
Ancestry = ancestryConnection
}
end
function EnemyAI.StopAI(enemy)
local connections = activeAIs[enemy]
if not connections then return end
if connections.Heartbeat and connections.Heartbeat.Connected then
connections.Heartbeat:Disconnect()
end
if connections.Ancestry and connections.Ancestry.Connected then
connections.Ancestry:Disconnect()
end
activeAIs[enemy] = nil
end
return EnemyAI
Currently, I have this temporary spawner script (I’ll change this into wave system for production)
local plrs = game:GetService("Players")
local em = require(script.Parent.Modules.EnemyManager)
task.wait(1.5)
while true do
for _, target in plrs:GetChildren() do
if not target then break end
em.SpawnEnemy(target)
task.wait(.25)
end
end
So when I launch the game, RECV is at ~10 and SENT is at ~8, after some time RECV drops to ~8, SENT stays the same. I’m new to networking in Roblox, and I wonder if those stats are okay. Obviously, mob count will increase per player, so RECV and SENT will do that as well.
Do you guys have any ideas how I could optimize those? Just reworking the spawners not to be attached to the players won’t work, as that’s the core logic of the game