Basic tower system optimized?

never made an actually working tower system until recently so i want to know if its optimized enough or how i can improve optimization as tower defense games are known for being unoptimized

main tower module

local tower = {}
local TowerStats = require(game:GetService("ReplicatedStorage").TowerStats)
tower.__index = tower

local TowerMods = {}

for _, v in pairs(game:GetService("ServerScriptService").TowerModule:GetChildren()) do
	if v:IsA("ModuleScript") then
		TowerMods[v.Name] = v
	end
end
print(TowerMods)


function tower.new(towerType, towerInstance)
	local self = setmetatable({}, tower)
	self.towerType = towerType
	self.level = 0
	self.Instance = towerInstance
	return self
end

--[[ functions that happen once ]]--
function tower:newRange(towerD, range)
	local physicalRange = Instance.new("Part")
	physicalRange.Parent = towerD.Instance
	physicalRange.Anchored = true
	physicalRange.Transparency = 1
	physicalRange.CanCollide = false
	physicalRange.Shape = "Cylinder"
	physicalRange.BrickColor = BrickColor.new("Bright blue")
	physicalRange.Size = Vector3.new(20, range*3, range*3)
	print(towerD.Instance)
	physicalRange.Position = towerD.Instance.Position + Vector3.new(0, -1, 0)
	physicalRange.CFrame = physicalRange.CFrame * CFrame.Angles(0, 0, math.rad(90))

	return physicalRange
end

function tower:visibleRange(towerD, range)
	local visibleRange = Instance.new("Part")
	visibleRange.Parent = towerD.Instance
	visibleRange.Anchored = true
	visibleRange.Transparency = 1
	visibleRange.CanCollide = false
	visibleRange.Shape = "Cylinder"
	visibleRange.BrickColor = BrickColor.new("Bright blue")
	visibleRange.Size = Vector3.new(0.25, range*3, range*3)
	print(towerD.Instance)
	visibleRange.Position = towerD.Instance.Position + Vector3.new(0, -1, 0)
	visibleRange.CFrame = visibleRange.CFrame * CFrame.Angles(0, 0, math.rad(90))
	visibleRange.Name = "visibleRange"

	return visibleRange
end

function tower:CreateStats(towerD, statVal, name)
	local stat = Instance.new("NumberValue")
	stat.Name = statVal
	stat.Value = TowerStats[name][0][statVal]
	stat.Parent = towerD.Instance
	return stat
end
--[[ kinda universal ]]--

function tower:Sound(soundName, towerD)
	local newSound = game.ReplicatedStorage.Sounds[soundName]:Clone()
	newSound.Parent = towerD
	newSound:Play()
	return newSound
end

--[[ constant activation ]]--


function tower:Target(check)
	local enemyChosen
	local farthest = -1
	for index, enemy in check do
		local Node = enemy:GetAttribute("Node") 
		if game:GetService("Workspace").Nodes:FindFirstChild("Node"..tostring(Node+1)) == nil then
			continue
		end
		local Distance = (enemy.Position - game:GetService("Workspace").Nodes:FindFirstChild("Node"..tostring(Node+1)).Position).Magnitude
		print((Node)+(1/Distance))
		if (Node)+(1/Distance) > farthest and enemy:GetAttribute("Health") > 0 then
			farthest = (Node)+(1/Distance)
			enemyChosen = enemy
		end
	end
	return enemyChosen
end

function tower:BasicLaser(enemyChosen, towerSelected)
	local Direction = (enemyChosen.Position - towerSelected.Position)
	local Midpoint = (towerSelected.Position + Direction/2)
	local Bullet = Instance.new("Part")
	Bullet.Name = "Bullet"
	Bullet.CFrame = CFrame.new(Midpoint, enemyChosen.Position)
	Bullet.Size = Vector3.new(0.5, 0.5, (enemyChosen.Position - towerSelected.Position).Magnitude)
	Bullet.Anchored = true
	Bullet.CanCollide = false
	Bullet.BrickColor = BrickColor.new("Neon orange")
	Bullet.Material = Enum.Material.Neon
	Bullet.Transparency = 0.5
	Bullet.Parent = game:GetService("Workspace").Cache
	return Bullet
end

function tower:Rotate(towerSelected, enemyChosen)
	local rotatedCFrame = CFrame.new(towerSelected.Position, enemyChosen.Position)
	towerSelected.CFrame = rotatedCFrame
end
function tower:BeginAttacking()
	
	if TowerMods[self.towerType] then
		local module = require(TowerMods[self.towerType])
		module.attackLoop(self.Instance, self)
	end
end
return tower

example tower using module:

local carrot = {}
local TowerStats = require(game:GetService("ReplicatedStorage").TowerStats)
local TowerModule = require(game:GetService("ServerScriptService").TowerModule)

local towerName = script.Name

function carrot.attackLoop(tower, self)
	local range = TowerModule:CreateStats(self, "range", towerName)
	local rate = TowerModule:CreateStats(self, "rate", towerName)
	local damage = TowerModule:CreateStats(self, "damage", towerName)
	
	local attacking = false
	
	local physicalRange = TowerModule:newRange(self, range.Value)
	local visibleRange = TowerModule:visibleRange(self, range.Value)
	
	local Params = OverlapParams.new()
	Params.FilterType = Enum.RaycastFilterType.Include
	Params.FilterDescendantsInstances = {workspace.Enemies}

	local on = false
	tower.ClickDetector.MouseClick:Connect(function(person)
		if on == false then
			on = true
			game:GetService("ReplicatedStorage").ShowRange:FireClient(person, self.Instance, on)
		else
			on = false
			game:GetService("ReplicatedStorage").ShowRange:FireClient(person, self.Instance, on)
		end
	end)
	
	local function attack()
		if attacking then return end
		local check = workspace:GetPartsInPart(physicalRange, Params)
		if #check == 0 then return end
		if check then
			local enemyChosen = TowerModule:Target(check)
			
			
			if enemyChosen and enemyChosen:GetAttribute("Health") > 0 then
				TowerModule:Rotate(tower, enemyChosen)
				local Health = enemyChosen:GetAttribute("Health")
				local Bullet = TowerModule:BasicLaser(enemyChosen, tower)
				enemyChosen:SetAttribute("Health", Health-damage.Value)
				local AttackSound = TowerModule:Sound("carrot_attack.wav", self.Instance)
				
				game:GetService("Debris"):AddItem(Bullet, 0.3)
				game:GetService("Debris"):AddItem(AttackSound, 2)
				
				task.spawn(function()
					for i = 1, 3 do
						Bullet.Transparency+=0.1
						task.wait(0.08)
					end
				end)
				
			end
		end
		task.wait(rate.Value)
		attacking = false
	end
	while task.wait() do
		if not attacking then
			attack()
		end
	end
end
return carrot

Using Services
If you are using a service multiple times then make a variable for it. (2.5x faster)
Instead of:

for i = 1, 10 do
 game:GetService("ReplicatedStorage").Prefab:Clone().Parent = workspace
end

do

local RS = game:GetService("ReplicatedStorage")

for i = 1, 10 do
 RS.Prefab:Clone().Parent = workspace
end

Referencing Workspace
Use workspace when you want to access the workspace. (168x faster)
Instead of:

game:GetService("Workspace")

do

workspace


Parenting Order
Instead of setting the parent as the first property, change it as the last, also don’t use the 2nd argument of Instance.new() (I know you didn’t, but just in case). (1.25x faster)
Instead of:

local p = Instance.new("Part") -- or Instance.new("Part", workspace)
p.Parent = workspace -- you'd skip this if you used the 2nd argument
p.BrickColor = BrickColor.new("New Yeller") 
p.Name = "Yep"

do

local p = Instance.new("Part")
p.BrickColor = BrickColor.new("New Yeller") 
p.Name = "Yep"
p.Parent = workspace 


Stop Using Debris Service
Instead of using the Debris service (which is deprecated btw) to destroy instances with a delay use this method below (recommended by Roblox staff as well (source)). (1.6x faster)
Instead of:

game:GetService("Debris"):AddItem(Instance, .1)

do

task.delay(.1, function()
Instance:Destroy()
end)


Use Attributes
They might be more tedious to work with, but it’s definitely worth it (in most cases). (25x faster (for initializing))
Instead of:

local parts = folder:GetChildren()
for _, v in parts do
    local health = Instance.new("NumberValue")
    health.Value = 100
    health.Name = "Health"
    health.Parent = v
end
-- you'd do something like this maybe later on the line		
part.Health.Value = 50

do

local parts = folder:GetChildren()
for _, v in parts do
    v:SetAttribute("Health", 100)
end
-- you'd do something like this maybe later on the line		
part:SetAttribute("Health", 50)


(The set time for the attributes is 6μs and for values it’s 10μs (1.6x faster))

Conclusion
These were just some of the more obvious mistakes that I caught when quickly looking over the script, I’m sure there are some more technical improvements that you can make.
Luckily most of these improvements wont take a long time to implement.

Extra Info
All of these benchmark results were done on scripts with larger scales and not on just 10 iterations of a loop. Most of the times when I stated how many times faster something is I rounded the value.

Edit Info

  • Added attribute vs value benchmark
2 Likes

becomes:

No need to set them one by one when you can do it one step! Definitely faster especially in cases where you are creating many towers.

You should adopt this principle into all .new functions. (enemies and other classes)

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.