Enemy System Optimization

Hey, I’ve made an entity system that can handle around 400-500 enemies before fps starts to decrease horribly and cpu and ping go all the way up. I’m trying to figure out if there’s a way I can decrease the CPU from spiking up randomly and prevent my ping from going high.
The only positive about this system at the moment is theres barely any gaps between enemies and the recv is low.
If anyone has any suggestions,tips,etc let me know as I really need it and can’t find anything that will work for my system.

Server Sided Script:

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local Nodes = workspace.BaseplateMap.Nodes
local Modules = ReplicatedStorage.Modules
local Events = ReplicatedStorage.Event

local EnemyInfo = require(Modules.EnemyInfo)

local Percision = 10^2
local counter = 0

local StartTime = workspace:GetServerTimeNow()
local enemy = {}
local currentenemies = {}
function enemy.ReturnEnemies()
	return currentenemies -- returns the enemy table
end
function enemy.Spawn(name)
	counter += 1
	local EnemyTable = {
		["Name"] = name;
		["ID"] = counter;
		["ModelID"] = EnemyInfo[name].ModelID;
		["CFrame"] = CFrame.new(workspace.BaseplateMap.Start.Position,workspace.BaseplateMap.Nodes[1].Position);
		["Node"] = 1;
		["Path"] = 1;
		["Speed"] = EnemyInfo[name].Speed;
		["Health"] = EnemyInfo[name].Health;
		["MaxHealth"] = EnemyInfo[name].MaxHealth;
		["Buffs"] = EnemyInfo[name].Buffs
	}
	currentenemies[counter] = EnemyTable
	coroutine.wrap(ServerSidedMovement)(counter,workspace:GetServerTimeNow())
end

function ServerSidedMovement(EnemyID,EnemySpawnTime)
	local Connection
	local EnemyTable = currentenemies[EnemyID]
	local SentString = string.pack("BB",EnemyTable.ModelID,EnemyTable.Path)
	Events.SpawnEnemy:FireAllClients(SentString)
	if EnemyTable == nil then return end
	local Start = workspace.BaseplateMap.Start
	for index,value in ipairs(Nodes:GetChildren()) do
		local ReachedWaypoint = false
		local NextWaypoint = Nodes:FindFirstChild(tostring(index))
		local CurrentWaypoint = Nodes:FindFirstChild(tostring(index - 1))
		if CurrentWaypoint == nil then
			CurrentWaypoint = Start	
		end
		local timehappened = 0
		local function MoveEnemy(DeltaTime)
			local Delta = workspace:GetServerTimeNow() - EnemySpawnTime
			EnemySpawnTime = workspace:GetServerTimeNow()
			if CurrentWaypoint ~= Nodes:FindFirstChild(tostring(#Nodes:GetChildren())) and ReachedWaypoint == false then
				timehappened += DeltaTime
				local Speed = EnemyTable.Speed
				local distance = (EnemyTable.CFrame.Position - NextWaypoint.Position).Magnitude
				local Time = distance/Speed
				local alpha = (timehappened/Time) % 1
				local Lerp = EnemyTable.CFrame:Lerp(NextWaypoint.CFrame,Delta*Speed/distance)
				EnemyTable.CFrame = CFrame.new(Lerp.Position,NextWaypoint.Position)
				local MagChecking = (EnemyTable.CFrame.Position - NextWaypoint.Position).Magnitude
				if MagChecking <= 0.4 then
					ReachedWaypoint = true
					Connection:Disconnect()
				end
			end
		end
		Connection = RunService.Heartbeat:Connect(MoveEnemy)
		repeat task.wait() until ReachedWaypoint
	end
	Connection:Disconnect()
	Events.EnemyDie:FireAllClients(EnemyID,EnemyTable.Health,true)
end

return enemy

ClientSided

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local EnemiesFolder = workspace.Enemy
local RunService = game:GetService("RunService")
local Percision = 10^2
local players = game:GetService("Players")
local player = players.LocalPlayer
local GUI = player.PlayerGui
local LastUpdate = workspace:GetServerTimeNow()
local Nodes = workspace.BaseplateMap.Nodes
local Enemies = ReplicatedStorage.Enemies
local Events = ReplicatedStorage.Event
local counter = 0
function quadBezier(t, p0, p1, p2)
	return (1 - t)^2 * p0 + 2 * (1 - t) * t * p1 + t^2 * p2
end
function ClientEnemyMovement(Enemy,EnemySpawnTime)
	local End = workspace.BaseplateMap.End
	local Connection
	local PrimaryPart = Enemy.PrimaryPart
	if Enemy == nil or Enemy.PrimaryPart == nil then return end
	local EnemySpawnTime = EnemySpawnTime
	for index,value in ipairs(Nodes:GetChildren()) do
		local ReachedWaypoint = false
		local NextWaypoint = Nodes:FindFirstChild(tostring(index + 1))
		local CurrentWaypoint = Nodes:FindFirstChild(tostring(index))
		if NextWaypoint == nil then
			NextWaypoint = End	
		end
		local timehappened = 0
		local function MoveEnemy(DeltaTime)
			local Delta = workspace:GetServerTimeNow() - EnemySpawnTime
			EnemySpawnTime = workspace:GetServerTimeNow()
			if CurrentWaypoint ~= Nodes:FindFirstChild(tostring(#Nodes:GetChildren())) and ReachedWaypoint == false then
				timehappened += Delta
				local Speed = Enemy:GetAttribute("Speed")
				local distance = (PrimaryPart.Position - NextWaypoint.Position).Magnitude
				local Time = distance/Speed
				local alpha = (timehappened/Time) % 1
				local Lerp = PrimaryPart.CFrame:Lerp(NextWaypoint.CFrame,Delta*Speed/distance)
				local BigCFrame = CFrame.new(Lerp.Position,NextWaypoint.Position)
				PrimaryPart:PivotTo(BigCFrame)
				local MagChecking = (PrimaryPart.Position - NextWaypoint.Position).Magnitude
				if MagChecking <= 0.4 then
					ReachedWaypoint = true
					Connection:Disconnect()
				end
			end
		end
		Connection = RunService.Heartbeat:Connect(MoveEnemy)
		repeat task.wait() until ReachedWaypoint
	end
	Connection:Disconnect()
end

Events.SpawnEnemy.OnClientEvent:Connect(function(SentString)
	counter += 1
	local unpackedstring = {string.unpack("BB",SentString)}
	unpackedstring[#unpackedstring] = nil
	local ModelID = unpackedstring[1]
	local PathNumber = unpackedstring[2]
	if not EnemiesFolder:FindFirstChild(counter) then
		for i,v in pairs(Enemies:GetChildren()) do
			if v:GetAttribute("ModelID") == ModelID then
				local lilbuddy = Enemies[tostring(v)]:Clone()
				for i, v in pairs(v:GetChildren()) do 
					if v:IsA("BasePart") or v:IsA("MeshPart") then 
						v.CollisionGroup = "Mobs"
					end
				end
				lilbuddy:SetAttribute("EnemyID",counter)	
				lilbuddy.Parent = EnemiesFolder
				lilbuddy.PrimaryPart.CFrame =  workspace.BaseplateMap.Start.CFrame + Vector3.new(0, (lilbuddy.PrimaryPart.Size.Y/2 - 1), 0)
				coroutine.wrap(ClientEnemyMovement)(lilbuddy,workspace:GetServerTimeNow())
			end
		end
	end
end)

Also I’d appreciate it if you don’t recommend me to use humanoids,tweenservice,or any open source modules like matter that do the work for you. Thank you

How many parts is your “Enemy” the lag spike may lay in the part count, you could also try grouping them all together and having them behave as one AI in a Zerg group, or even setting NetworkOwnership as the server and using a magnitude check could really reduce the workload.

my enemy is 6 parts, torso head arms and legs and hrp. everything is clientsided so i cant rlly do networkownership

Bump any help or tips are appreciated

Why are you handling movement on the client and the server? Seems like something’s off.

1 Like

server is for tower attacking and client is for replicating, i only handle visuals on the client so if i were to handle tower attacking on the client then it’d be exploitable

So first of all, I don’t think you need to update the CFrame every 1/60th of a second, the optimal rate imo is 1/10th of a second and dw there won’t be any choppy movements as you are lerping the CFrame.

1 Like

No I know there wont be any choppy movement as it’s the server and theres 0 parts or models on the server side and only on the cliebt. so i would add a tickrate to the server movement right? as my main issue isnt anything with the client more as i’m experiencing cpu spikes and ping increase

Reducing the rate at which the server updates the CFrame ought to reduce the load on the server resulting in lower CPU usage.

1 Like

so how do you think i’d add/decrease the tickrate to 1/10th of a second?

while not ReachedWaypoint do
    --Do stuff

    task.wait(0.1)
end

Also, I’m not sure of this approach (though I believe it is optimal) but you can just send over a serialized CFrame with the enemyID from the server over to the client and then just lerp the CFrame of the enemy model from the client. This reduces the unnecessary lerps on the server and reduces it role down to just doing checks whereas the client handles all visuals. This entirely removes running a core loop of an enemy object on the client and the server and just does it on the server.

1 Like

I spawned around 400-500 enemies here https://gyazo.com/b67f17d42d3a9739fad29704aae46d65
FPS drop still happens and the CPU increase still happens as well for some reason, I was thinking it was because of the lerp on the server but i dont know
image
I also added the while not reachedwaypoint here.

You don’t need the RunService.Heartbeat anymore. Just do

while not ReachedWaypoint do
    MoveEnemy()
    task.wait(0.1)
end
1 Like

What do you mean by Serialized CFrame? when and how would I send it to the client from the server

I did this and It kind of reduced the CPU and ping yet spikes keep happening

Well, adopting that approach will cause a rewrite of your entire server logic, you think you can do that?

Yeah I can do that. (I don’t think this really matters anymore but i changed my code and removed the connection on the server and changed it to this repeat while not ReachedWaypoint do MoveEnemy() task.wait(0.1) end until ReachedWaypoint

But just let me know how I would implement the Serialized CFrame to client I’m fine with rewriting my server code.

That’s just complicating the issue further, and it seems like accomplishes just as much as the previous approach.

you’re right but it looks cleaner imo, it shouldn’t matter anyways since you told me I’d be rewriting my server module, also how would I take that approach.

First of all, what all CFrame data is crucial for your game? For eg, if your game is taking place in a uniform terrain, then you don’t need to send over the Y position data as it is always fixed. And is rotation necessary? If yes on which all axis?