How can I optimize my entity system?

Hey I was wondering I could get help on optimizing my entity system

This system works by the client rendering the enemies and the server moving them, it gets around 1300 enemies before fps drop starts to happen does anyone know how I can improve this?

I tried looking for post to help but didn’t find anything useful any help is appreciated :slight_smile:

Server Side

local mob = {}
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Events = ReplicatedStorage:WaitForChild("Events")
local LastUpdate = tick()
local UpdateDT = 0.1
local ClientInfo = {}
local Positions = {}

game:GetService("RunService").Heartbeat:Connect(function(DeltaTime)
	local map = workspace:WaitForChild("Map") 
	
	for number, data in pairs(Positions) do 
		
		local CurrentWaypoint = map.Waypoints:FindFirstChild(data.Waypoint)
		local OldWaypoint = map.Waypoints:FindFirstChild(data.Waypoint - 1)
		
		if not OldWaypoint then 
			OldWaypoint = map.Waypoints.Start
		end
		
		if not CurrentWaypoint then 
			map.Base.Humanoid:TakeDamage(data.SpeedHealth.Y)
			ClientInfo[number] = {Dead = true}
			Events.Movement:FireAllClients(ClientInfo)
			Positions[number] = nil
			continue
		end
		
		local distance = (OldWaypoint.Position - CurrentWaypoint.Position).Magnitude
		
		data.Moved += DeltaTime * data.SpeedHealth.X / distance
		if data.Moved >= 1 then
			data.Waypoint += 1
			data.Moved = 0
		end
		
		ClientInfo[number] = Vector2int16.new(math.floor(CurrentWaypoint.Position.X*50), math.floor(CurrentWaypoint.Position.Z*50))
	end
	
	if tick() - LastUpdate >= UpdateDT then
		LastUpdate = tick()
		Events.Movement:FireAllClients(ClientInfo)
	end
end)


local num = 0 
function mob.Spawn(name, quantity, dly)
	
	local Exists = ReplicatedStorage.Enemies:FindFirstChild(name)
	
	if Exists then 
		for i = 1, quantity do 
			num += 1
			Positions[num] = {
				
				Waypoint = 1,
				SpeedHealth = Vector2int16.new(math.round(Exists.Configuration.Speed.Value), math.round(Exists.Humanoid.Health)),
				Moved = 0
				
			}
			Events.Render:FireAllClients(name, num)
			task.wait(dly)
		end
	end
	
end

return mob

Client side

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Events = ReplicatedStorage:WaitForChild("Events")

local LastUpdate = tick()
local ud = 0.001

local Enemies = {}
local positions = {}

local map = workspace:WaitForChild("Map")
local base = map:WaitForChild("Base")

local PS = game:GetService("PhysicsService")

Events.Movement.OnClientEvent:Connect(function(info)
	positions = info
end)


game:GetService("RunService").Heartbeat:Connect(function(deltaTime)
	if tick() - LastUpdate >= ud then 
		LastUpdate = tick()
		for Enemyobject, data in pairs(Enemies) do 
			if Enemyobject and positions[data] then 					
				
				local pos = positions[data]			
				
				if not pos.X or not pos.Y then 
					Enemies[Enemyobject] = nil
					Enemyobject:Destroy()
					continue
				end
				
				local Position = Vector3.new(pos.X/50, Enemyobject.PrimaryPart.Position.Y, pos.Y/50)
				local Distance = (Enemyobject.PrimaryPart.Position - Position).Magnitude
					
				local speed = Enemyobject.Configuration.Speed.Value
					
				local cframe = CFrame.new(pos.X/50, Enemyobject.PrimaryPart.Position.Y, pos.Y/50)
				Enemyobject:PivotTo(Enemyobject.PrimaryPart.CFrame:Lerp(cframe, deltaTime*speed/Distance))						
			end
		end
	end	
end)

Events.Render.OnClientEvent:Connect(function(name, EnemyNumber)
	
	local map = workspace:WaitForChild("Map")
	local start = map:WaitForChild("Waypoints"):WaitForChild("Start")
	
	local enemy = ReplicatedStorage.Enemies:FindFirstChild(name):Clone()
	enemy.Parent = workspace.Enemies
	
	enemy.PrimaryPart.Anchored = true
	enemy.PrimaryPart.CanCollide = false
	
	for i, v in pairs(enemy:GetChildren()) do 
		if v:IsA("BasePart") then 
			v.CollisionGroup = "Mobs"
		end
	end
	
	
	enemy.PrimaryPart.CFrame = start.CFrame + Vector3.new(0, (enemy.PrimaryPart.Size.Y/2 - 1), 0)
	
	Enemies[enemy] = EnemyNumber 
end

Theres a lot that could be clearer here. I think it would help if you made a simple diagram on what is being sent from server to client and when / how often. That will help you identify what is consuming all the performance. Make sure to identify all the different loops that are running on each, I count three but I could be wrong. You could also use the profiler to measure how much time each part of the code is using, it is a very powerful tool, but what it tells you will be more helpful if you make a diagram first. That way you know what can be changed.

My only advice so far is to reduce the number of indexing, and also cache the results of FindFirstChild as it is slower than direct indexing.

You could use BulkMoveTo() for smoother fps and faster CFraming multiple parts.

Instead of using DeltaTime from heartbeat, you could also use GetServerTimeNow() to keep all enemies synchronized.

I tried using BulkMoveTo in the past I couldn’t figure out how to get it smooth and working correctly.
Do you mind showing me an example of how I could use GetServerTimeNow() sorry I’m not very familiar with these.

For BulkMoveTo you can just keep using the BulkMoveTo function every heartbeat to make it smooth with no lag

An example would be:

local Parts = {workspace.Part0, workspace.Part1}
local CFrameList = {part0CFrame, part1CFrame}

workspace:BulkMoveTo(Parts, CFrameList, Enum.BulkMoveMode.FireCFrameChanged)
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Events = ReplicatedStorage:WaitForChild("Events")

local LastUpdate = tick()
local ud = 0.001

local Enemies = {}
local positions = {}

local map = workspace:WaitForChild("Map")
local base = map:WaitForChild("Base")

local PS = game:GetService("PhysicsService")

Events.Movement.OnClientEvent:Connect(function(info)
	positions = info
end)

local startTime = workspace:GetServerTimeNow()
game:GetService("RunService").Heartbeat:Connect(function()
        local deltaTime = (workspace:GetServerTimeNow()-startTime)

        startTime = workspace:GetServerTimeNow()
	if tick() - LastUpdate >= ud then 
		LastUpdate = tick()
		for Enemyobject, data in pairs(Enemies) do 
			if Enemyobject and positions[data] then 					
				
				local pos = positions[data]			
				
				if not pos.X or not pos.Y then 
					Enemies[Enemyobject] = nil
					Enemyobject:Destroy()
					continue
				end
				
				local Position = Vector3.new(pos.X/50, Enemyobject.PrimaryPart.Position.Y, pos.Y/50)
				local Distance = (Enemyobject.PrimaryPart.Position - Position).Magnitude
					
				local speed = Enemyobject.Configuration.Speed.Value
					
				local cframe = CFrame.new(pos.X/50, Enemyobject.PrimaryPart.Position.Y, pos.Y/50)
				Enemyobject:PivotTo(Enemyobject.PrimaryPart.CFrame:Lerp(cframe, deltaTime*speed/Distance))						
			end
		end
	end	
end)

Events.Render.OnClientEvent:Connect(function(name, EnemyNumber)
	
	local map = workspace:WaitForChild("Map")
	local start = map:WaitForChild("Waypoints"):WaitForChild("Start")
	
	local enemy = ReplicatedStorage.Enemies:FindFirstChild(name):Clone()
	enemy.Parent = workspace.Enemies
	
	enemy.PrimaryPart.Anchored = true
	enemy.PrimaryPart.CanCollide = false
	
	for i, v in pairs(enemy:GetChildren()) do 
		if v:IsA("BasePart") then 
			v.CollisionGroup = "Mobs"
		end
	end
	
	
	enemy.PrimaryPart.CFrame = start.CFrame + Vector3.new(0, (enemy.PrimaryPart.Size.Y/2 - 1), 0)
	
	Enemies[enemy] = EnemyNumber 
end

Instead of using delta time from heartbeat you should get the servers time and just find the delta time from the server so it’s synced from the server.

I have rewritten the client-side script and my fps dropped at around 1200 enemies (150 fps with no enemies and 50 fps with 1200) I don’t know if its supposed to be better than this and that I’m doing something wrong

local mob = {}
local RunService = game:GetService("RunService")
local Mobs = {}

local waypoints = workspace.Waypoints
local start = workspace.Start
local health = game.ReplicatedStorage.Value
local bitpack = require(script.Parent.SimpleBit)

local Parts = {}
local CFrames = {}

local StartTime = workspace:GetServerTimeNow()

local function UpdatePositions()
	local delta = (workspace:GetServerTimeNow()-StartTime)

	StartTime = workspace:GetServerTimeNow()
	
	for model, data in pairs(Mobs) do
		local Waypoint = waypoints:FindFirstChild(data.Waypoint)
		local OldWaypoint = waypoints:FindFirstChild(data.Waypoint-1) or start
		local speed = data.Speed

		if Waypoint and OldWaypoint then
			local Id = model:GetAttribute("ID")
			local Distance = (OldWaypoint.Position - Waypoint.Position).Magnitude
			
			
			local cframe = OldWaypoint.CFrame:Lerp(Waypoint.CFrame, data.Moved)
			data.Moved += delta*speed/Distance
			if data.Moved>=1 then
				data.Waypoint +=1
				data.Moved = 0
			end
			CFrames[Id] = cframe
		end
	end
end


game:GetService("RunService").Heartbeat:Connect(function()
	UpdatePositions()
	workspace:BulkMoveTo(Parts, CFrames, Enum.BulkMoveMode.FireCFrameChanged)
end)

local id = 0
local mobs = workspace.NPCs
local a = script.Parent.Abnormal
function mob.Spawn()
	for i = 1,5000 do
		id +=1
		local new =a:Clone()
		new.Parent = mobs
		new:SetAttribute("ID", id)

		
		Mobs[new] = {
			Waypoint = 1,
			Speed =6,
			Moved = 0
		}
		Parts[id] = new.PrimaryPart
		task.wait(.1)
	end
end

return mob

Make sure you aren’t using any humanoids. They can cause lag. Make sure can touch and canquery and cancollide is all set to false for slight performance increase. Make sure humanoidrootpart is only anchored for enemies nothing else to remove all physics. Also make sure to use profiler for the rendering part because it can help solve performance issues.

Also since it is client sided fps will probably drop a bit but ping would be very low because your enemies are being handled locally. If it was done on server then fps would probably be same but ping would be insanely high. So doing client sided is best. But still if it were handled locally with 1000s of enemies your fps should be good. I’ve tested my game with enemies handled locally on my phone and i still get around 60 fps and below 1kb of recv data with 1000s of enemies on max graphics with similar methods.

I used microprofiler and it said bulkmoveto was causing some lag and the “UpdatePositions” function as well how could I further optimize it? Is there a way to get waypoint without storing it in a table.

Sorry for the late replys btw

Run BulkMoveTo if there’s anything in the Parts & CFrames table.
Run UpdatePositions if there’s any entity in your mob table.

You can make them run under a tick rate, implement an accumulator in your Heartbeat connection to achieve this as so:

-- // Example of an accumulator
-- // You may replace the deltaTime from Heartbeat to (workspace:GetServerTime() - StartTime) if 
-- // it "synchronizes" with the server if you wish.

local tickRate = 1/20 -- Make it only run 20 times a second
Heartbeat:Connect(function(deltaTime)
	accumulatorDelta += deltaTime

	while accumulatorDelta >= tickRate do
		accumulatorDelta -= tickRate
        if next(Mobs) then
           --// next is similar to #Parts > 0, I use this in if statements to check
           --// if a dictionary is not empty.
           UpdatePositions()
        end
        if #Parts > 0 and #CFrames > 0 then
            workspace:BulkMoveTo(Parts, CFrames, Enum.BulkMoveMode.FireCFrameChanged)
        end
	end
end)

You can use Parallel LUA for this, create an Actor/Parallel Thread for each group of 25 mobs though you have to use a BindableEvent/Signal module to send entities processed by the actor back to the main server script.

2 Likes

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.