Help Me Optimize My Entity System

Hello Developers, I Have Created An Entity System For My Tower Defense Game, Which Can Handle 2000 Mobs At Once At 40 - 100 CPU,1000 Mobs At Once At 30-40 CPU but I want to optimize it Here Is My Server Side Entity Creator:

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local RemotesFolder = ReplicatedStorage:WaitForChild("RemotesFolder")
local RemoteEvent = RemotesFolder:WaitForChild("EntityCreatedEvent")

local Mob = {}
Mob.__index = Mob

Mob.new = function()
	local self = setmetatable({}, Mob)
	self.remoteEvent = RemoteEvent
	return self
end

Mob.createEntity = function(self, id, quantity)
	local newEntities = {}
	table.insert(newEntities, id)
    self.remoteEvent:FireAllClients(newEntities, quantity)
end

return Mob

here is my entity reciver on the client:

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")

local Instances = ReplicatedStorage:WaitForChild("Instances")
local Mobs = Instances:WaitForChild("Mobs")
local Towers = Instances:WaitForChild("Towers")

local InstancesWorkspace = game.Workspace:WaitForChild("Instances")
local MobsWorkspace = InstancesWorkspace:WaitForChild("Mobs")
local TowersWorkspace = InstancesWorkspace:WaitForChild("Towers")

local NodesFolder = game.Workspace:WaitForChild("Nodes")

local CatRom = require(ReplicatedStorage.Modules:WaitForChild("CatRom"))
local enemyInfo = require(ReplicatedStorage.Modules:WaitForChild("enemyInfo"))

local EntityController = {}
EntityController.__index = EntityController

EntityController.new = function(remoteEvent, mobs, mobsWorkspace)
	local self = setmetatable({}, EntityController)
	self.remoteEvent = remoteEvent
	self.mobs = mobs
	self.mobsWorkspace = mobsWorkspace
	return self
end

EntityController.moveEntity = function(self, obj, speed)
	local Points = {}
	local nodeNames = {}

	for _, node in ipairs(NodesFolder:GetChildren()) do
		local nodeName = tonumber(node.Name)
		if nodeName then
			table.insert(nodeNames, nodeName)
		end
	end
		

	table.sort(nodeNames)

	for _, nodeName in ipairs(nodeNames) do
		local node = NodesFolder:FindFirstChild(tostring(nodeName))
		if node then
			table.insert(Points, node.CFrame * CFrame.new(Vector3.new(Random.new():NextInteger(-1, 1), 1.5, 0)))
		end
	end


	local AnimationTrack = obj.AC.Animator:LoadAnimation(obj.Walk)
	AnimationTrack:Play()
	local catRom = CatRom.new(Points, 0, 0)
	local startTime = workspace:GetServerTimeNow()
	local startCFrame = obj.PrimaryPart.CFrame
	local Distance = (Points[#Points].Position - startCFrame.Position).Magnitude
	local duration = Distance / speed

	local renderConn
	renderConn = RunService.RenderStepped:ConnectParallel(function()	
		local currentTime = workspace:GetServerTimeNow()
		local elapsedTime = currentTime - startTime
			
		task.synchronize()

		if elapsedTime >= duration then
			renderConn:Disconnect()
			obj:Destroy()
			return
		end

		local t = elapsedTime / duration
		local cframe = catRom:SolveUniformCFrame(t)
		obj.PrimaryPart.CFrame = cframe
			
		task.desynchronize()
	end)
end


EntityController.createEntities = function(self, entityIds, quantity)
    for _, entityId in ipairs(entityIds) do
		local entityInfo = enemyInfo[entityId]
		if entityInfo then
			for i = 1, quantity do
				local entityClone = self.mobs:WaitForChild(tostring(entityId)):Clone()
				entityClone.Parent = self.mobsWorkspace
				entityClone.Name = entityInfo["Name"]

				if entityClone:IsA("Model") then
					local primaryPart = entityClone.PrimaryPart
					if primaryPart then
						coroutine.wrap(function()
							self:moveEntity(entityClone,entityInfo.Speed)
						end)()
						task.wait(0.025)
					else
						warn("Entity clone does not have a PrimaryPart: " .. entityClone.Name)
					end
				else
					warn("Invalid entity clone type: " .. entityClone.ClassName)
				end
			end
		else
			warn("Entity info not found for entity with ID: " .. entityId)
		end
	end
end

return EntityController

Thank You!

2 Likes

no one helps this is so sad.:frowning:

I think general profilling like the micro profiler will help you, and us get a firmer grasp on what is eating up your CPU time. The only thing that seems off at a glance is this function

	renderConn = RunService.RenderStepped:ConnectParallel(function()	
		local currentTime = workspace:GetServerTimeNow()
		local elapsedTime = currentTime - startTime
			
		task.synchronize()

		if elapsedTime >= duration then
			renderConn:Disconnect()
			obj:Destroy()
			return
		end

		local t = elapsedTime / duration
		local cframe = catRom:SolveUniformCFrame(t)
		obj.PrimaryPart.CFrame = cframe
			
		task.desynchronize()
	end)

You have this set to parallel but immediately synchronize it, this could be run faster as a normal connection without the synchronization overhead. Or try to trim the synchronized part as much as possible, move local t and local cframe (if SolveUniformCFrame permits) before the syncchronize, maybe check elsewhere for object destruction.

3 Likes

Yeah like @gertkeno said, get rid of the parallel code. You don’t use it for anything even near intensive enough to make it worth the overhead. Even if you move the part of the code that can be parallelized up above the sync, it still wouldn’t be worth it. The CPU churns through each tiny calculation like those you have in about ~10 nanoseconds.

3 Likes

sorry but that didn’t change anything it is still the same as it was before.

No worries I didn’t believe it was the main problem anyway.

I think if you want to optimize this, you have to implement a method to move the entities in groups instead of one by one. Moving around 1000-2000 objects one by one is going to perform badly no matter what. You end up with so many RenderStepped things all running every single frame. It doesn’t need to run every frame to be smooth.

Not sure what your paths look like, but as long as they’re not curved, you could split the paths into different segments, and then create an object along every segment that moves forward at a constant speed. When you spawn entities, weld them to this object, and let the object carry them forward. When they reach a turn in the path, deweld them and weld them to the next one. Obviously this solution would cause everything to go the same speed, and then you’d either need to have more objects at these places or something similar.

I’m not a big fan of that method, but yeah in some regard you have to think outside of the box here.

You may also get better performance just from handling the entities on the server. I don’t see the reasoning for doing it on the client anyway.

Why did you go with catmull rom splines btw? What else did you try before hand?

1 Like

I Tried Making A System With Lerp, Tween, Align Position & Align Orientation but all of them were bad performance Especially tween was heavy on my performance. By The Way Like You Said Move Them In A Group I Don’t Get How Would I Achieve That?

1 Like

Alright so, if you wanted to keep your current system, but try and optimize it a little, you could make each entity only move every 2nd or 3rd frame.

1 Like

its not that big of a problem if i have to remake the entire system. I have also tried using An table and not coroutines
like inserting enemies in a table and looping through them one by one and updating there position, but that was even more Laggy for some reason.

but that was even more laggy for some reason.

It was more laggy because you probably still had it in the renderstepped right, so you were doing too much work before the frame and that froze things up right?

By The Way Like You Said Move Them In A Group I Don’t Get How Would I Achieve That?

Okay. Well if you wanted to move them in a group, you would use your points like you have now, and then create a new object at the position of each point, and then make all of those objects move in the direction of the new point. And then like I said, you can weld your entities to the moving object you’ve created.

At some point you may want to get rid of all these objects and then recreate them so they don’t get too far away. Or just create two of them to begin with for each point so you can cycle between them.

EntityController.moveEntity = function(self, speed)
	for i, obj in ipairs(Enemies) do
		local Points = {}
		local nodeNames = {}

		for _, node in ipairs(NodesFolder:GetChildren()) do
			local nodeName = tonumber(node.Name)
			if nodeName then
				table.insert(nodeNames, nodeName)
			end
		end


		table.sort(nodeNames)

		for _, nodeName in ipairs(nodeNames) do
			local node = NodesFolder:FindFirstChild(tostring(nodeName))
			if node then
				table.insert(Points, node.CFrame * CFrame.new(Vector3.new(Random.new():NextInteger(-1, 1), 1.5, 0)))
			end
		end


		local AnimationTrack = obj.AC.Animator:LoadAnimation(obj.Walk)
		AnimationTrack:Play()
		local catRom = CatRom.new(Points, 0, 0)
		local startTime = workspace:GetServerTimeNow()
		local startCFrame = obj.PrimaryPart.CFrame
		local Distance = (Points[#Points].Position - startCFrame.Position).Magnitude
		local duration = Distance / speed

		local renderConn
		renderConn = RunService.PreRender:Connect(function()	
			local currentTime = workspace:GetServerTimeNow()
			local elapsedTime = currentTime - startTime

			local t = elapsedTime / duration
			local cframe = catRom:SolveUniformCFrame(t)

			if elapsedTime >= duration then
				renderConn:Disconnect()
				obj:Destroy()
				return
			end
			obj.PrimaryPart.CFrame = cframe
		end)
	end
end


EntityController.createEntities = function(self, entityIds, quantity)
    for _, entityId in ipairs(entityIds) do
		local entityInfo = enemyInfo[entityId]
		if entityInfo then
			for i = 1, quantity do
				local entityClone = self.mobs:WaitForChild(tostring(entityId)):Clone()
				entityClone.Parent = self.mobsWorkspace
				entityClone.Name = entityInfo["Name"]
				entityClone:SetAttribute("Health", entityInfo["Health"])
				
				table.insert(Enemies, entityClone)

				if entityClone:IsA("Model") then
					local primaryPart = entityClone.PrimaryPart
					if primaryPart then
						self:moveEntity(entityInfo.Speed)
						task.wait(0.5)
					else
						warn("Entity clone does not have a PrimaryPart: " .. entityClone.Name)
					end
				else
					warn("Invalid entity clone type: " .. entityClone.ClassName)
				end
			end
		else
			warn("Entity info not found for entity with ID: " .. entityId)
		end
	end
end

return EntityController

soo here is the code after i did the for loop for the mobs it is more laggy i don’t get on what you are talking about having it in the renderstepped.

It was more laggy because you probably still had it in the renderstepped right, so you were doing too much work before the frame and that froze things up right?

Are you sure that the problem is the actual code logic, and not the fact you are spawning thousands of humanoids/groups in a tight area?

#1 There Are Not Humanoids.
#2 I Am Spawning Them In A Tight Area For Performance It Doesn’t Matter if i spawn them in a tight area or not.

Can you show a video of you commenting out everything inside the moveEntity function, just teleporting the models close to the player character? Im sure that the issue is having too many draw calls making the CPU render thread slow down

Also, provide a microprofiler HTML page, Itll remove the guessing part here.

Uploading: test.mp4…


um i get this error when i try to upload a video

Try uploading it to youtube and sending the URL here

EDIT: And make sure each character is slightly spaced out, but only very slightly, so every one is still in the camera’s view

https://www.youtube.com/watch?v=mAoZqaI6XbI let me show the micro profile

There is no zombie seen on your video, so of course it would make no draw calls so no CPU overhead