I am working on an tower system for my TD game. Currently it is very unoptimized, and the targeting is very inaccurate, and has massive lag when there is a lot of enemies.
The enemies are rendered on client, and use the CFrame attribute.
Here is my current code:
--!native
local towerclass = {}
local RunService = game:GetService("RunService")
local ServerStorage = game:GetService("ServerStorage")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
towerclass.__index = towerclass
local towerstats = require(ServerStorage:WaitForChild("TowerStats"))
towerclass.loadedtowers = {
}
local janitor = require(ReplicatedStorage.Janitor).new()
function towerclass.new(tower:string,player:Player)
print("startedplacement for: "..tower.." "..player.Name)
local self = setmetatable({},towerclass)
local towerstattable = towerstats.get(tower)
local placeremote = ReplicatedStorage.placementremotes.promptplacement
local placeposition:CFrame = placeremote:InvokeClient(player,tower)
print("startedplacement")
if not placeposition then
print("placementcancled")
return nil
end
print("retriverpostion: "..tostring(placeposition))
self.position = placeposition
local model:Model = ServerStorage.tower:FindFirstChild(tower).lvl1:Clone()
model.Name = tower
self.lvl = 1
self.model = model
self.range = towerstattable.lvl1.range * 4
self.damage = towerstattable.lvl1.damage
self.firerate = towerstattable.lvl1.firerate
model.PrimaryPart.Anchored = true
model.PrimaryPart.CFrame = placeposition
local map:Model = workspace:FindFirstChild(ReplicatedStorage.Map.Value)
model.Parent = map:WaitForChild("placedtowers")
ReplicatedStorage.DebugValues.TowerCount.Value += 1
for _, basepart:BasePart in pairs(model:GetChildren()) do
if basepart:IsA("BasePart") then
basepart.CollisionGroup = "tower"
end
end
local targetdestroyconnection = nil
local function detectenemies(targeting)
local enemyfolder:Folder = map:WaitForChild("enemies")
local target:Part = nil
for _, enemy in pairs(enemyfolder:GetChildren()) do
local enemyCFrame:CFrame = enemy:GetAttribute("CFrame")
local distance = (enemyCFrame.Position - model.PrimaryPart.CFrame.Position).Magnitude
if targeting == "first" then
if distance <= self.range then
if not target then
target = enemy
if not targetdestroyconnection then
targetdestroyconnection = target.Destroying:Connect(function()
target = nil
targetdestroyconnection:Disconnect()
targetdestroyconnection = nil
end)
end
else
local targettoexitdistance = (target:GetAttribute("CFrame").Position - target:GetAttribute("Exit").Position).Magnitude
local enemytoexitdistance = (enemy:GetAttribute("CFrame").Position - enemy:GetAttribute("Exit").Position).Magnitude
if enemytoexitdistance < targettoexitdistance then
target = enemy
if not targetdestroyconnection then
targetdestroyconnection = target.Destroying:Connect(function()
target = nil
targetdestroyconnection:Disconnect()
targetdestroyconnection = nil
end)
end
end
end
end
end
if targeting == "last" then
if distance <= self.range then
if not target then
targetdestroyconnection = target.Destroying:Connect(function()
target = nil
end)
target = enemy
else
local targettoexitdistance = (target:GetAttribute("CFrame").Position - target:GetAttribute("Exit").Position).Magnitude
local enemytoexitdistance = (enemy:GetAttribute("CFrame").Position - enemy:GetAttribute("Exit").Position).Magnitude
if enemytoexitdistance < targettoexitdistance then
target = enemy
if not targetdestroyconnection then
targetdestroyconnection = target.Destroying:Connect(function()
target = nil
targetdestroyconnection:Disconnect()
targetdestroyconnection = nil
end)
end
end
end
end
end
end
return target
end
local coremodule = ServerStorage.Core:Clone()
coremodule = require(coremodule)
coremodule.change(self)
local target = nil
local animator:Animator = model.AnimationController.Animator
local shootconnection = nil
local shootanimation = animator:LoadAnimation(model.Anims.fire)
local idleanimation = animator:LoadAnimation(model.Anims.idle)
idleanimation:Play()
shootconnection = task.spawn(function()
while true do
target = detectenemies("first")
if target then
model.PrimaryPart.CFrame = CFrame.new(model.PrimaryPart.Position, target:GetAttribute("CFrame").Position)
shootanimation:Play()
target.Health.Value -= self.damage
task.wait(self.firerate)
shootanimation:Stop()
end
task.wait()
end
end)
janitor:Add(shootconnection)
return self
end
return towerclass
I don’t think this will make a huge difference but try to remove nested functions.
They are defined everytime the function is ran, which may be a problem.
When you move all the nested functions from their parent functions to the main script thread, they are only declared once instead of everytime their parent function is called, and there should be virtually no difference.
You mentioned that enemies are rendered on the client side. Consider moving enemy rendering to the server or using a hybrid approach where essential information (like position) is sent to clients, but rendering is handled server-side.
Client-side rendering can cause lag, especially with many enemies. By offloading rendering to the server, you can reduce the load on clients.
Spatial Partitioning: Implement spatial partitioning (e.g., octrees or grids) to efficiently find nearby enemies. This reduces the number of distance checks.
Asynchronous Processing: Use asynchronous tasks to handle expensive operations 1. * The tower placement logic seems straightforward, but consider the following:
Collision Detection: Instead of checking all base parts for collision groups, use a more efficient collision detection method (e.g., raycasting).
Anchoring: Set Anchored to false during placement and adjust it once the tower is correctly positioned. Ensure that tower stats (range, damage, fire rate) are balanced. Overpowered towers can affect gameplay.
Consider adding upgrade levels beyond level 1. Each level could have different stats and abilities. * Break down the script into smaller functions or modules for better readability and maintainability.
Use descriptive variable and function names to improve code understanding.
Wait do you mean Parallal Lua? Because I don’t have that much expensive operations, i do not think this will help that much.
The reason we have client side rendering is because it can run smoothly even if we have up to 1000 enemies. It’s just that the tower targeting is lagging the game. Without towers, there is next to no lag.
Please ask me stuff if you are confused. This is my first reply so idk what to say here… EDIT: Fixed some bugs and cut out some fluff to optimize it a bit more
NOTE: If “self.range” is calculated in studs then the range has to be dividided by two like this “self.range / 2” because 1 radius basically translates to 2 studs so be careful about that.
local params = OverlapParams.new()
params.FilterType = Enum.RaycastFilterType.Include
local function detectenemies(targetMode: "first"|"last")
params.FilterDescendantsInstances = {map.enemies}
local towerPos = model.PrimaryPart.Position
local potentialEnemies = workspace:GetPartBoundsInRadius(towerPos, self.range / 2, params) --This gets all enemy parts that are in the range of the tower within a sphere (range is divided by two cuz i assume the range is in studs)
local range: number? -- this range var is for getting the closest or the furthest enemy
local target: Part?
target = potentialEnemies[1]
for i, potentialEnemyPart: Part in ipairs(potentialEnemies) do
local distance = (towerPos - potentialEnemyPart.Position).Magnitude
if not range then
range = distance
end
if distance <= range and targetMode == "first" then
range = distance
target = potentialEnemyPart
elseif distance >= range and targetMode == "last" then
range = distance
target = potentialEnemyPart
end
end
return target and target:FindFirstAncestorOfClass("Model") --this can return nil if no enemy was found
--^ NOTE: if the enemies have more models inside them this might return that model instead, you could fix this by ungrouping the models inside the enemy models
end
Old Version (don't use this, it has bugs and it sucks)
local params = OverlapParams.new()
params.FilterType = Enum.RaycastFilterType.Include
local function detectenemies(targetMode: "first"|"last")
params.FilterDescendantsInstances = map:WaitForChild("enemies"):GetDescendants()
local towerCFrame = model.PrimaryPart.CFrame
local potentialEnemies = workspace:GetPartBoundsInRadius(towerCFrame.Position, self.range, params) --This gets all enemy parts that are in the range of the tower
local range: number -- this range var is for getting the closest or the furthest enemy
local target: Part
target = potentialEnemies[1]
for i, potentialEnemyPart: Part in pairs(potentialEnemies) do
local distance = (target.Position - potentialEnemyPart.Position).Magnitude
if not range then
range = distance
end
if distance <= range and targetMode == "first" then
range = distance
target = potentialEnemyPart
elseif distance >= range and targetMode == "last" then
range = distance
target = potentialEnemyPart
end
end
if target then
target.Destroying:Once(function()
target = nil
end)
end
return target:FindFirstAncestorOfClass("Model") --this can return nil if no enemy was found
--^ NOTE: if the enemies have more models inside them this might return that model instead, you could fix this by ungrouping the models inside the enemy models
end
This might not improve that much to be honest, i tried my best. You can test it to see if it improves anything.
I would also like to inform you there is better ways to detect enemies than your solution so i would advise to think about how you handle towers so it can be fast and efficient, so please don’t hold back on trying more ways to handle your towers. I never tried something like this so i don’t have exprience in this stuff
If you want to use the CFrame attribute then this is the modified version. But the problem here is the hitbox of the tower is a square instead of a radius (sphere) which may not be what you want
local function detectenemies(targetMode: "first"|"last")
local towerPos = model.PrimaryPart.Position
local target, range
for i, potentialEnemy: Model in pairs(enemies:GetChildren()) do
if not target then
target = potentialEnemy
end
local distance = (towerPos - potentialEnemy:GetAttribute("CFrame").Position).Magnitude
if not range then
range = distance
end
if distance <= range and targetMode == "first" then
range = distance
target = potentialEnemy
elseif distance >= range and targetMode == "last" then
range = distance
target = potentialEnemy
end
end
if (towerPos - target:GetAttribute("CFrame").Position).Magnitude > self.range then --checks if the target is out of range
return nil
end
return target
end
Also --!native probably doesn’t help in your case i don’t know much about it but it seems that your script doesn’t really need it and also because --!natively generated scripts take up more memory and resources so it is not recomended to use it in all of our scripts
Hmm… Can you tell me does the targeted enemy still stays targeted even when it exits the range? and also what does the “Exit” attribute for enemies mean?
It moves on after the targeted enemy leaves the range. Exit means the final destination for the enemy. Also I noticed that the range is a bit bigger than what the range is supposed to be.
I dont know how to fix why your range being a bit bigger but that may be because the code i sent detects in the shape of a square, so the code i sent previously could work if the positions are updated on the server.
But try this to fix the locked target problem, not sure if it will fix it but i am trying alright!
local function detectenemies(targetMode: "first"|"last")
local towerPos = model.PrimaryPart.Position
local target, range
for i, potentialEnemy: Model in pairs(enemies:GetChildren()) do
local distance = (towerPos - potentialEnemy:GetAttribute("CFrame").Position).Magnitude
if distance > self.range then
continue
end
if not target then
target = distance
range = distance
end
if (distance <= range) and (targetMode == "first") then
range = distance
target = potentialEnemy
elseif (distance >= range) and (targetMode == "last") then
range = distance
target = potentialEnemy
end
end
return target
end
The biggest performance gains that you will get will probably be from changing the targeting algo from O(N^2). It’s not easy but a broadphase for target detection would be a big bonus. Maybe look into quad trees.
I concur. A quadtree or octree is probably the best option for increasing performance significantly. I’m working on a tower defense myself right now and implementing an octree has increased performance by a lot.