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
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.
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.
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?
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?
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.
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?
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