-
What do you want to achieve? Keep it simple and clear!
Optimize enemies to have 500+ of them spawning and dying constantly. -
What is the issue? Include screenshots / videos if possible!
RECV skyrockets (~ 21 kb/s) with my system and eventually bugs out the game -
What solutions have you tried so far? Did you look for solutions on the Creator Hub?
I tried removing humanoids, added render distance, removed parts from server (using folders and attributes now), tried implementing octree but gave up, didn’t understand the logic of how to use it
Here’s the system:
Server
local rs = game:GetService("ReplicatedStorage")
local plrs = game:GetService("Players")
local sss = game:GetService("ServerScriptService")
local ss = game:GetService("ServerStorage")
local http = game:GetService("HttpService")
local runs = game:GetService("RunService")
local DEBUG = false
local assets = rs.Assets
local es = require(ss.EnemyStats)
local oc = require(sss.Modules.OrbController)
local octree = require(rs.Libs.Octree)
local packets = require(rs.Libs.Packets)
local enemyContainer = workspace.Enemies
local playerSpawners = {}
local idsInUse = {}
local enemies = {}
math.randomseed(tick())
local grid = octree.new(Vector3.new(0, 0, 0), 1024)
local maxEnemies = 1000
local minDistance = 25
local maxDistance = 25
local enemyCount = 0
local UPDATE_INTERVAL = 1
local CLIENT_UPDATE_RADIUS = 75
local frameCount = 0
local die = 3
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,
-2,
math.sin(randomAngle) * randomDistance
)
end
local function debugServerPosition(enemyId, pos)
local part = workspace:FindFirstChild("Debug_Server_"..enemyId)
if not part then
part = Instance.new("Part")
part.Anchored = true
part.CanCollide = false
part.Transparency = 0.5
part.Size = Vector3.new(1,1,1)
part.Color = Color3.fromRGB(255, 0, 0)
part.Name = "Debug_Server_"..enemyId
part.Parent = workspace
end
part.Position = pos
end
local function getNearbyPlayers(enemyPos)
return grid:QueryRegion(enemyPos, CLIENT_UPDATE_RADIUS)
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 spawnPos = calculateRandomSpawnPosition(playerPos)
--print("[SpawnEnemy] Spawning at:", spawnPos)
local rand = math.random()
local selectedTypeId = nil
for i, typeData in es do
if rand <= typeData.CumulativeChance then
selectedTypeId = i
break
end
end
if not selectedTypeId then return end
local enemyStats = es[selectedTypeId]
local enemyId
repeat
enemyId = math.random(0, 65535)
until not idsInUse[enemyId]
idsInUse[enemyId] = true
enemies[enemyId] = {
Name = "Enemy_"..enemyId,
Type = selectedTypeId,
Position = spawnPos,
Health = enemyStats.HP,
TargetUserId = target.UserId,
LastAttackTime = 0,
}
local enemyFolder = Instance.new("Folder")
enemyFolder.Name = "Enemy_"..enemyId
enemyFolder:SetAttribute("TargetPos", spawnPos)
enemyFolder:SetAttribute("Type", selectedTypeId)
enemyFolder:SetAttribute("Health", enemyStats.HP)
enemyFolder.Parent = enemyContainer
grid:Insert(target, spawnPos)
enemyCount += 1
task.delay(die, function()
EnemyManager.DamageEnemy(enemyId, enemies[enemyId].Health)
end)
end
function EnemyManager.Attack(enemyId, player)
local enemy = enemies[enemyId]
if not enemy or not player then return end
local character = player.Character
if not character or not character.PrimaryPart then return end
local enemyStats = es[enemy.Type]
local now = os.clock()
--print("attack logic")
end
function EnemyManager.MoveEnemy(enemyId, dt)
local enemy = enemies[enemyId]
if not enemy then return end
local player = plrs:GetPlayerByUserId(enemy.TargetUserId)
if not player or not player.Character or not player.Character.PrimaryPart then return end
local playerPos = player.Character.PrimaryPart.Position
local currentPos = enemy.Position
local dist = (playerPos - currentPos).Magnitude
local enemyStats = es[enemy.Type]
local inRange = dist <= enemyStats.Range
if inRange then
EnemyManager.Attack(enemyId, player)
else
local direction = (playerPos - currentPos).Unit
local newPos = currentPos + Vector3.new(direction.X, 0, direction.Z) * enemyStats.Speed * dt
newPos = Vector3.new(newPos.X, 1, newPos.Z)
enemy.Position = newPos
local enemyFolder = enemyContainer:FindFirstChild("Enemy_"..enemyId)
if enemyFolder then
enemyFolder:SetAttribute("TargetPos", newPos)
end
if DEBUG then debugServerPosition(enemyId, newPos) end
end
end
function EnemyManager.DamageEnemy(enemyId, amount)
local enemy = enemies[enemyId]
if not enemy or not amount then return end
enemy.Health -= amount
-- get the damager from the weapon system
if enemy.Health <= 0 then
enemy.Health = 0
EnemyManager.KillEnemy(enemyId)
end
end
function EnemyManager.KillEnemy(enemyId)
local enemy = enemies[enemyId]
if not enemy then return end
local enemyFolder = enemyContainer:FindFirstChild("Enemy_"..enemyId)
if enemyFolder then
enemyFolder:SetAttribute("Dead", true)
end
local enemyTypeName = es[enemy.Type].Name
oc.SpawnOrb(enemy.Position, "random", plrs:GetPlayerByUserId(enemy.TargetUserId), enemyTypeName)
grid:Remove(enemyId)
enemies[enemyId] = nil
enemyCount -= 1
task.delay(2, function()
idsInUse[enemyId] = nil
end)
end
runs.Heartbeat:Connect(function(dt)
for enemyId, _ in pairs(enemies) do
EnemyManager.MoveEnemy(enemyId, dt)
end
end)
return EnemyManager
Client
local rs = game:GetService("ReplicatedStorage")
local ts = game:GetService("TweenService")
local runs = game:GetService("RunService")
local DEBUG = false
local enemyAssets = require(rs.Modules.EnemyAssets)
local enemyStats = require(rs.Modules.GetEnemyStats)
local damageGuiTemplate = rs.Assets:WaitForChild("DamageGui")
local enemyContainer = workspace:WaitForChild("Enemies")
local enemies = {}
local EnemyClient = {}
local function playAnimation(enemyId, animName, animId, force)
local enemyData = enemies[enemyId]
if not enemyData then return end
local humanoid = enemyData.Model:FindFirstChildOfClass("Humanoid")
if not humanoid then return end
if enemyData.CurrentAnimName ~= animName or force then
if enemyData.CurrentTrack then
enemyData.CurrentTrack:Stop()
end
local anim = enemyData.Animations[animName]
if not anim then
local animObj = Instance.new("Animation")
animObj.AnimationId = animId
enemyData.Animations[animName] = humanoid:LoadAnimation(animObj)
end
enemyData.CurrentTrack = enemyData.Animations[animName]
enemyData.CurrentAnimName = animName
enemyData.CurrentTrack:Play()
end
end
local function debugClientPosition(enemyId, model)
local part = workspace:FindFirstChild("Debug_Client_"..enemyId)
if not part then
part = Instance.new("Part")
part.Anchored = true
part.CanCollide = false
part.Transparency = 0.5
part.Size = Vector3.new(1,1,1)
part.Color = Color3.fromRGB(0, 0, 255)
part.Name = "Debug_Client_"..enemyId
part.Parent = workspace
end
runs.RenderStepped:Connect(function()
if model and model.PrimaryPart then
part.Position = model.PrimaryPart.Position
end
end)
end
local function handleEnemyFolder(folder:Folder)
local id = tonumber(folder.Name:match("Enemy_(%d+)"))
if not id then return end
if enemies[id] then return end
local enemyType = folder:GetAttribute("Type")
if not enemyType then return end
local enemyTemplate = enemyAssets[enemyType]
if not enemyTemplate then warn("Missing enemy asset for type:", enemyType) return end
local model = enemyTemplate.Model:Clone()
model:PivotTo(CFrame.new(folder:GetAttribute("TargetPos")))
model.Name = folder.Name
model:SetAttribute("Type", enemyType)
model.Parent = enemyContainer
for _, part in model:GetDescendants() do
if part:IsA("BasePart") then
part.CollisionGroup = "Enemies"
end
end
enemies[id] = {
Model = model,
State = "Idle",
Animations = {},
CurrentAnimName = nil,
CurrentTrack = nil
}
if DEBUG then debugClientPosition(id, model) end
folder:GetAttributeChangedSignal("TargetPos"):Connect(function()
local pos = folder:GetAttribute("TargetPos")
if not pos then return end
local data = enemies[id]
if data and data.Model then
data.Model.Humanoid:MoveTo(pos)
if data.State ~= "Walking" then
playAnimation(id, "Walk", enemyTemplate.Walk)
data.State = "Walking"
end
end
end)
folder:GetAttributeChangedSignal("Health"):Connect(function()
local hp = folder:GetAttribute("Health")
if hp <= 0 then
EnemyClient.EnemyDied(id)
else
EnemyClient.EnemyDamaged(id, 1) -- damage
end
end)
folder:GetAttributeChangedSignal("Attacking"):Connect(function()
if folder:GetAttribute("Attacking") == true then
EnemyClient.EnemyAttacking(id)
end
end)
folder:GetAttributeChangedSignal("Dead"):Connect(function()
EnemyClient.EnemyDied(id)
end)
model:FindFirstChildOfClass("Humanoid").MoveToFinished:Connect(function(reached)
if reached and enemies[id] and enemies[id].State ~= "Attacking" then
playAnimation(id, "Idle", enemyTemplate.Idle)
enemies[id].State = "Idle"
end
end)
end
function EnemyClient.EnemyDamaged(enemyId, damage)
local enemyModel = enemies[enemyId] and enemies[enemyId].Model
if not enemyModel then return end
local head = enemyModel:FindFirstChild("Head")
if not head then return end
local damageGui = damageGuiTemplate:Clone()
damageGui.Parent = head
damageGui.Adornee = head
damageGui.AlwaysOnTop = true
damageGui.Size = UDim2.new(5, 0, 5, 0)
local offset = Vector3.new(math.random(-1, 1), 1.2 + math.random(), math.random(-1, 1))
damageGui.StudsOffset = offset
local textLabel = damageGui:FindFirstChild("TextLabel")
if not textLabel then return end
textLabel.Text = "-" .. tostring(damage)
textLabel.TextTransparency = 0
textLabel.TextScaled = true
textLabel.BackgroundTransparency = 1
textLabel.Font = Enum.Font.GothamBlack
textLabel.TextColor3 = Color3.fromRGB(236, 62, 62)
textLabel.AnchorPoint = Vector2.new(0.5, 0.5)
textLabel.Position = UDim2.new(0.5, 0, 0.5, 0)
textLabel.Size = UDim2.new(0.25, 0, 0.25, 0)
local uiStroke = textLabel:FindFirstChildOfClass("UIStroke")
if uiStroke then uiStroke.Thickness = 0.5 uiStroke.Transparency = 0 end
local popTween = ts:Create(textLabel, TweenInfo.new(0.15), {Size = UDim2.new(1, 0, 1, 0)})
popTween:Play()
local guiUpTween = ts:Create(damageGui, TweenInfo.new(0.45), {
StudsOffset = damageGui.StudsOffset + Vector3.new(0, 1.2, 0)
})
local textFadeTween = ts:Create(textLabel, TweenInfo.new(0.45), {TextTransparency = 1})
popTween.Completed:Connect(function()
guiUpTween:Play()
textFadeTween:Play()
end)
guiUpTween.Completed:Connect(function()
if damageGui and damageGui.Parent then
damageGui:Destroy()
end
end)
end
function EnemyClient.EnemyAttacking(enemyId)
local data = enemies[enemyId]
if not data then return end
local enemyType = data.Model:GetAttribute("Type")
data.State = "Attacking"
playAnimation(enemyId, "Attack", enemyAssets[enemyType].Attack, true)
task.delay(0.75, function()
if enemies[enemyId] and enemies[enemyId].State == "Attacking" then
playAnimation(enemyId, "Idle", enemyAssets[enemyType].Idle)
enemies[enemyId].State = "Idle"
end
end)
end
function EnemyClient.EnemyDied(enemyId)
local data = enemies[enemyId]
if not data then return end
local model = data.Model
if data.CurrentTrack then
data.CurrentTrack:Stop()
end
for _, part in model:GetDescendants() do
if part:IsA("BasePart") or part:IsA("Decal") then
local tween = ts:Create(part, TweenInfo.new(1), {Transparency = 1})
tween:Play()
end
end
task.delay(1, function()
model:Destroy()
end)
enemies[enemyId] = nil
end
for _, folder in enemyContainer:GetChildren() do
handleEnemyFolder(folder)
end
enemyContainer.ChildAdded:Connect(function(folder)
task.wait()
handleEnemyFolder(folder)
end)
return EnemyClient
Spawner script (Located in ServerScriptService)
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
My guess is that constantly removing/adding folders lags out the server, but I was not able to make only client-side implementation and I can’t quite understand how hit detection works in those systems, would appreciate if anyone could help me