Hi, I’m trying to get 600+ active enemies/mobs without SENT and RECV skyrocketing… 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
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
local ts = game:GetService("TweenService")
local rs = game:GetService("ReplicatedStorage")
local EnemyClient = {}
local damageGuiTemplate = rs.Assets:WaitForChild("DamageGui")
function EnemyClient.EnemyDied(enemy)
if not enemy or not enemy.Parent or not enemy:GetAttribute("IsDead") then return end
task.delay(.75, function()
local hasParts = false
for _, v in enemy:GetDescendants() do
if v:IsA("BasePart") then
hasParts = true
local tweenInfo = TweenInfo.new(.75, Enum.EasingStyle.Sine, Enum.EasingDirection.In)
local sinkPos = v.Position + Vector3.new(0, -2, 0)
local sinkTween = ts:Create(v, tweenInfo, {Position = sinkPos})
sinkTween:Play()
sinkTween.Completed:Connect(function()
enemy:Destroy()
end)
end
end
end)
end
function EnemyClient.EnemyDamaged(enemy, damage)
if not enemy or not enemy.Parent then return end
local head = enemy: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 offsetX = math.random(-10, 10) / 10
local offsetY = math.random(-10, 10) / 10
local offsetZ = math.random(-10, 10) / 10
damageGui.StudsOffset = Vector3.new(offsetX, 1.2 + offsetY, offsetZ)
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(202, 53, 53)
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 popInfo = TweenInfo.new(
0.15,
Enum.EasingStyle.Quint,
Enum.EasingDirection.Out
)
local popTween = ts:Create(textLabel, popInfo, {
Size = UDim2.new(1, 0, 1, 0),
Rotation = math.random(-12.5, 12.5)
})
local strokePopTween = uiStroke and ts:Create(uiStroke, popInfo, {
Thickness = 1.5
})
popTween:Play()
if strokePopTween then strokePopTween:Play() end
local settleInfo = TweenInfo.new(
0.4,
Enum.EasingStyle.Quad,
Enum.EasingDirection.In
)
local settleTween = ts:Create(textLabel, settleInfo, {
Size = UDim2.new(0.1, 0, 0.1, 0),
Rotation = 0
})
local strokeSettleTween = uiStroke and ts:Create(uiStroke, settleInfo, {
Thickness = 1
})
local driftInfo = TweenInfo.new(
0.45,
Enum.EasingStyle.Linear
)
local guiUpTween = ts:Create(damageGui, driftInfo, {
StudsOffset = damageGui.StudsOffset + Vector3.new(0, 1.2, 0)
})
local textFadeTween = ts:Create(textLabel, driftInfo, {
TextTransparency = 1
})
local strokeFadeTween = uiStroke and ts:Create(uiStroke, driftInfo, {
Transparency = 1
})
popTween.Completed:Connect(function()
settleTween:Play()
if strokeSettleTween then strokeSettleTween:Play() end
guiUpTween:Play()
textFadeTween:Play()
if strokeFadeTween then strokeFadeTween:Play() end
end)
guiUpTween.Completed:Connect(function()
if damageGui and damageGui.Parent then
damageGui:Destroy()
end
end)
end
return EnemyClient
As you can see, I tried compressing the data sent between client and server, it kinda helped but still not enough, RECV is at 9-10 KB/s rn and SENT is at 6-7 KB/s, with ONE player on the server.
How can I handle enemies on the client side while not exposing it to exploiters and having sanity checks?