How to optimize tower system

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



6 Likes

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’re welcome : )

1 Like

I am pretty sure it’s lag because it have so many objects, lines, etc

1 Like

heres the solution you have to read all of this :

  • 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.
  • Caching: Cache expensive calculations (e.g., enemy positions) to avoid redundant computations.
  • 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.
3 Likes

Oh thanks, But you did reply a wrong person :sweat_smile:

1 Like

ohhhh im sorry accident sometimes :sweat_smile:

1 Like

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.

1 Like

Can anyone help? This is bugging me a lot

1 Like

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

1 Like

Our enemy system has client side rendering, so the position is refrenced of attrubute

1 Like

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
1 Like

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

here is when to use --!native
link to the post: Luau Native Code Generation Preview [Studio Beta]

You should target every 1-2 seconds, or if enemy is gone, then you can prevent mass spaming 100 per second

Sorry for late reply, but it locks onto an enemy and doesn’t go to any other enemy until the locked enemy dies.

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
1 Like

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.

1 Like

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.

And how would I implement an Octree? My enemies are rendered client sided