I am in the initial development stages for a tower defense game, and as one of if not the largest performance drain (everything else is event based,) the targeting and range detection system is extremely important. It is the only code that runs constantly, and I am doing my best to make it as efficient as possible. For that, I want to ask for ideas on how to squeeze the most efficiency possible out of my targeting system, and how I can improve / add on to it.
Currently, my hostile movements are based off of waypoints that they go between.
My first check is to group these hostiles based on their current target waypoint (event based) to have them move themselves to another folder; towers can then check to see if there are any hostiles in the waypoints they cover (which they calculate through some clever Vector3 math once upon being spawned.) They also only perform magnitude checks based upon these waypoints.
My second check involves grouping up towers when a tower in close proximity, and essentially making a “master” range based upon the range of all of those towers. Before those towers will perform any sort of distance or waypoint check, this “master” range will perform the waypoint check and distance check, meaning that a bunch of towers in close proximity won’t perform their checks if the only enemies are across the map.
Besides that, there’s some obvious stuff (if there’s no enemies, don’t bother running.)
Relevant tables that are mentioned:
groupingTable is the table that is responsible for holding the tower groups. It looks like this:
local groupingTable = {
[1] = {["Master"] = towerTable, ["ChildrenTower"] = {towerTable, towerTable}},
[2] = {["Master"] = towerTable, ["ChildrenTower"] = {towerTable, towerTable}},
["Unassigned"] = {towerTable, towerTable}
}
The main loop inside of the server manager for the towers:
-- Main tower hit detection loop
coroutine.wrap(function()
while true do
wait(1/12)
if isDictocc(enemyTable) then -- first check is to see if any enemies exist, since it's a dictionary we use some jank
-- next check is to see if our masters have anything in their waypoints
--print("Enemies exist.")
for _, entry in ipairs(groupingTable) do
local targs = {}
for _, waypoint in pairs(entry["Master"].ValidWaypoints) do
local hostilefold = HostileHolder[waypoint.Name]
local newtargs = hostilefold:GetChildren()
if (#newtargs) > 0 then
-- if there's hostiles in that waypoint, perform distance check
for i, v in pairs(newtargs) do
table.insert(targs, v)
end
end
end
local targetInRange = false
for _, target in ipairs(targs) do
if (target.Position - entry["Master"].Model.Position).Magnitude <= entry["Master"].TowerInfo.Range then
targetInRange = true
break
end
end
if targetInRange then
--print("Hostiles were in group master range.")
for _, OurTower in ipairs(entry["ChildrenTowers"]) do
-- check to see if this tower is on cooldown
if not OurTower.OnAttackCooldown then
--print("We are firing from a group.")
OurTower:Fire(targs)
end
end
end
end
for _, tower in pairs(groupingTable["Unassigned"]) do
if not tower.OnAttackCooldown then
--print("I am an unassigned tower, and I can fire.")
local targetTab = {}
for _, waypoint in ipairs(tower.ValidWaypoints) do
local hostilefold = HostileHolder[waypoint.Name]
for _, targ in ipairs(hostilefold:GetChildren()) do
table.insert(targetTab, targ)
end
end
if #targetTab > 0 then
--print("We found a target in a valid waypoint.")
tower:Fire(targetTab)
end
end
end
end
end
end)()
The :Fire() function of the Tower metatable
function Tower:Fire(TargetTable)
-- finds the hostile we can / should shoot atwh
self.OnAttackCooldown = true
local firstTarget, lastTarget
local firstTargetDist, lastTargetDist = -1, -1
for _, hostile in pairs(TargetTable) do
--print("I am a tower, and I am sweeping for hostiles in range.")
if (hostile.Position - self.Model.Position).Magnitude <= self.TowerInfo.Range then
-- if they pass distance check, figure out if its first or last
local dist = (1000 * tonumber(hostile.Parent.Name)) +
(hostile.Position - Waypoints[tostring(tonumber(hostile.Parent.Name) - 1)].Position).Magnitude
if dist > firstTargetDist then
--print("We have found a first target.")
firstTargetDist = dist
firstTarget = hostile
end
if dist < lastTargetDist then
--print("We have found a last target.")
lastTargetDist = dist
lastTarget = hostile
end
end
end
-- at this point, we already know that we *can* fire at it, so it's just directing how to fire
local target = firstTarget -- make this say other stuff like last and stuff
if target then
if self.TowerInfo.AttackType == "Projectile" then
if self.TowerInfo.ProjectileType == "Linear" then
local projectile = Attack.Projectile.Linear(self.Model.Position, firstTarget.Position, self.TowerInfo)
Attack.HitRegistration.Touch(projectile, self.TowerInfo.Damage,
self.TowerInfo.ProjectilePierce, self.TowerInfo.CanNotDamage)
end
end
coroutine.wrap(function()
wait(self.TowerInfo.AttackSpeed)
self.OnAttackCooldown = false
end)()
else
self.OnAttackCooldown = false
end
end