Hello, I am currently making a Tower Defense Game and my main goal is to optimize the enemy movement system so I’m able to put a large amount of enemy units inside my game while also maintaining a good level of performance in my game.
The current issue is that my game has poor performance, having horrible High Recv Network running at around hundreds of KB/s for around 200 units. While in comparison with other tower defense games, their performance is much better while being able to run even more enemies at the same time.
The current solutions that I’ve tried so far were, reading other articles about tower defense game development, performance related posts, reading the Astro Force performance improvement blog (which made me switch my entire system from actual models on the server to pure tables being used as data).
I’ve tried reaching out to other developers as well as asking other people for help and although I have received a huge amount of help I wasn’t able to really reach my end goal and there wasn’t overall large improvement in the performance.
My enemy units utilize a Catmull Rom Spline node generation system which takes in points pre-defined on the path and then outputs nodes which are stored in a table and those nodes are the nodes at which the enemy units will be travelling towards.
I believe that currently, the issue with my code probably has something to do with the main code execution flow, the while true do… statement which executes all the code a bunch of times, updating the server to client which probably causes most of the performance trouble. And other then that, I’m all but out of ideas.
= = =
Here’s the Server Code:
wait(5)
local UpdateClients = game.ReplicatedStorage:WaitForChild("Spawned")
--- Spline Calculations
-- example for p0 with tension
local dot = game.ServerStorage.dot
local tableOfPaths = {}
function DrawPath()
local NodesTable = {}
local tensionMain = math.random(1, 100)/100 -- math.random(90, 100)/100
local counter = 0
-- To DO: the point calculation is already in this table, just run DrawPath() for each NPC individually and then have them recalculate where to go
-- this will cause them to be scattered and different
local Points = {
}
for i=1, #game.Workspace.Points:GetChildren(), 1 do
Points[i] = game.Workspace.Points[i].Position
end
-- print("Tension is: " .. tensionMain)
local NumPoints = #Points
local LastPoint = Points[1]
for i = 1, NumPoints - 1 do
local p0 = Points[i - 1] or Points[1]
local p1 = Points[i]
local p2 = Points[i + 1]
local p3 = Points[i + 2] or Points[NumPoints]
-- print(i)
for j = 0, 1, 0.05 do
local tension = tensionMain
local t = j
local t2 = t * t
local t3 = t2 * t
local p =
(
(-tension * t + 2 * tension * t2 - tension * t3) * p0 +
(2 + (tension - 6) * t2 + (4 - tension) * t3) * p1 +
(tension * t - 2 * (tension - 3) * t2 + (tension - 4) * t3) * p2 +
(-tension * t2 + tension * t3) * p3
) * 0.5
--[[
-- EnemyPosVal Object Purposes + Visual Demonstration of Points
local visual_dot = dot:Clone()
visual_dot.Parent = game.Workspace.Nodes
visual_dot.Position = p
-- counter = counter + 1
visual_dot.Name = counter
]]
counter = counter + 1
NodesTable[counter] = CFrame.new(p)
end
end
return NodesTable
end
-- Pre-Process Generate a Bunch of Paths so we don't have to generate new ones later on
for i = 1, 100 do
tableOfPaths[i] = DrawPath()
end
-- NPC Spawning Table Putting Function
local ServerTableOfEnemies = {}
local EnemyId = "1";
function SpawnNPC()
local EnemyInfo = {
["EnemyId"] = EnemyId;
["Node"] = "1";
-- ["NodeProgress"] = 0;
["Speed"] = 4;
["Path"] = tableOfPaths[math.random(1,100)];
["CFrame"] = CFrame.lookAt(game.Workspace.Points["1"].Position, game.Workspace.Points["2"].Position);
["Done"] = false;
}
ServerTableOfEnemies[EnemyInfo.EnemyId] = EnemyInfo;
EnemyId = EnemyId + 1;
-- print(EnemyId)
end
-- Actual Movement Handling
function HandleMovement()
local ClientTableOfEnemies = {}
for i,Enemy in pairs(ServerTableOfEnemies) do
if (Enemy.Done == true) then
-- in the future fire some event to notify clients about deleting the NPC since its dead or reached the end
-- make another check about the NPC having 0 health or less in the future
-- print("Done")
else
Enemy.CFrame = Enemy.CFrame:ToWorldSpace(CFrame.new(0,0,-0.1 * Enemy.Speed))
local magnitude = (Enemy.CFrame.Position - Enemy.Path[Enemy.Node + 1].Position).Magnitude
-- print(Enemy.Node)
if (Enemy.CFrame.Position - Enemy.Path[Enemy.Node + 1].Position).Magnitude <= 1 then
Enemy.Node += 1
if Enemy.Node == #Enemy.Path then
Enemy.Done = true
else
Enemy.CFrame = CFrame.new(Enemy.Path[Enemy.Node].Position, Enemy.Path[Enemy.Node + 1].Position)
end
end
local Precision = 10^2
local EulerAnglesThingy = Vector3.new(Enemy.CFrame:ToEulerAnglesXYZ())
local ClientEnemyInfo = {
["EnemyId"] = Enemy.EnemyId;
["Vector3Int16_Position"] = Vector3int16.new(Enemy.CFrame.Position.X * Precision, Enemy.CFrame.Position.Y * Precision, Enemy.CFrame.Position.Z * Precision);
["Vector3Int16_Rotation"] = Vector3int16.new(EulerAnglesThingy.X * Precision, EulerAnglesThingy.Y * Precision, EulerAnglesThingy.Z * Precision);
-- ["Done"] = Enemy.Done;
-- ["Node"] = Enemy.Node;
-- ["Path"] = Enemy.Path;
}
ClientTableOfEnemies[i] = ClientEnemyInfo
end
end
UpdateClients:FireAllClients(ClientTableOfEnemies)
end
-- Main Routine
local enemyCount = 200;
task.spawn(function()
for _ = 1, enemyCount do
task.wait(0.1) -- Spawn delay between each unit
SpawnNPC()
end
end)
while true do
task.wait(0.05)
HandleMovement()
end
===
Here’s the Client Code:
game.ReplicatedStorage.Spawned.OnClientEvent:Connect(function(ClientTableOfEnemies)
local Precision = 10^2
for i, enemy in pairs(ClientTableOfEnemies) do
local CFrameNoOrientation = CFrame.new(enemy.Vector3Int16_Position.X / Precision, enemy.Vector3Int16_Position.Y / Precision, enemy.Vector3Int16_Position.Z / Precision)
local CFrameWithOrientation = CFrameNoOrientation * CFrame.Angles(enemy.Vector3Int16_Rotation.X / Precision, enemy.Vector3Int16_Rotation.Y / Precision, enemy.Vector3Int16_Rotation.Z / Precision)
if not game.Workspace.Mobs:FindFirstChild(enemy.EnemyId) then
local cloneClientSide = game.ReplicatedStorage.ClientSide:Clone()
cloneClientSide.Name = enemy.EnemyId
cloneClientSide.Parent = game.Workspace.Mobs
cloneClientSide:PivotTo(CFrame.new(cloneClientSide.Position, CFrameWithOrientation.Position))
else
local cloneClientSide = game.Workspace.Mobs[enemy.EnemyId]
-- local theLerping = cloneClientSide.CFrame:Lerp(enemy.CFrame, Speed)
cloneClientSide.CFrame = CFrameWithOrientation --CFrame with new position + new rotation
end
end
end)
Additionally, I will be attaching a screenshot visualizing how the nodes really look when generated and what the game looks like so far when everything is work, while the performance statistics are show as well.
Note: the DrawPath() function for the node generation system is pre-processed and then stored in a table from which the enemy NPCs later on randomly grab a path from the table and use. So that function has nothing to do with with performance issues as far as I’m concerned.
(Also pay attention that one of the red blob units are stuck at the very start for some weird reason, I don’t know why that is, it’s the unit at index 1 of the table of all the enemy units, when I spawn only 1 unit, the unit moves, but when I spawn more then 1, it doesn’t move for some odd reason.)