Optimization suggestions, networking stats review

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

2 Likes

maybe im psycho and those network stats are solid, idk

2 Likes

This seems good BUT if you want even MORE control over the data size and how big certain things should be in bytes you should 100% look at ByteNet Max | Upgraded networking library w/ buffer serialisation, strict Luau and RemoteFunction support | v0.1.9

They have support for integer bytes from I think 8 to 32 which is insanely useful including other optimisations and also other types are able to be chosen for certain byte sizes too!!!

As a computer nerd this matters a lot to me and is super useful. I recommend it if you’re conscious about your networking.

2 Likes

thank you fellow computer nerd ;D will definitely use that

Glad you are. also a nerd like my self. I assume you already understand bytes which will help you even more with this and feel you will benefit the most out of it like me ;D

Might help if you have used other languages like Cpp which support different bytes for types (pretty sure cpp does this I have forgotten)

1 Like

Yep it does and hopefully this will be enough to optimize my spaghetti code

1 Like

Its stupidly straight forward I will be very honest.

No need to organise remote folders anymore just make a module script, have it return a table of or a single namespace and then have relevant packets inside and all you need to do is require the module, index the packets and the namespace you want to use and then go from there.

1 Like

is it better if i use both, bytemax and packet framework or one of them? i think they’ll conflict

okay so I tried this today, but it made almost no difference.I guess the one and only way is to render enemies on the client instead of on the server…

Who in the world uses two separate packet libs and thinks its a good idea to use them both anyway? Its a NO btw.

I have never had a good experience with packets also the lack of documentation is stupid.

2 Likes

I am not sure how in anyway you’re gonna expect that the networking will make rendering better?

Why wouldn’t you just render entities/models on the client anyway?

Also it wont make much difference if you don’t bother to use the bytenet custom types for integers etc and it wont make a difference during development. It shines the most under load or in a server that’s got players.

1 Like

yeah you are right, will try rendering them on client, thanks

1 Like

Make a part with nothing on it or anything needed, use that part for the server then have the client interpret that part as an real model instead and then it will replicate between all clients in theory if you do it correctly.

1 Like

i was thinking about using tables for server, but ill try that, thanks once again!!

I think you should use heartbeat + batching to achieve best effect possible

I’m currently working on a game and i need about 400 enemies to be on, what i use is batching, this mean i’m processing one batch per frame, and if batch have about 15 enemies inside, it can process a lot of enemies each second

In order to optimize it even more, every 3-5 updates i re-calculate each batch’s path, and send new results to client in a buffer, then client can process this request, this way you can have very optimized network

You could also experiment with time-accumulation techniques to optimize your enemies even further, because sending update each frame might not be good idea

this is very bad idea, it will cost network, it’s better to rely purely on data and send it through buffers to the client

also i don’t think using network library is better than simply having simple remote or even raw creation of buffer, packing data, and sending it to client

If you look at the bytenet max post he has a reply in there comparing the network statistics between his, the fork of the library he is modifying, Roblox’s default networking and also other things too I believe.

problem is that his library uses Roblox’s one, and that buffers usually are enough for compression, it’s the way you spam remotes rather than data you send

Hey, I tried this and got me 10-15 kb RECV, it’s not that bad overall but it will scale up per player.
Now I’m still lost. I’ll still need to calculate enemy’s position so when player shoots it, it should not miss ;D