I have a module for a Tower defense game where every tower placed down uses it to function. Everything works fine, until after random points where I get an error in my output stating “Stack overflow” in the function to find a target along the path towards the base. It lags out the game heavily, and also causes some problems.
I have a unit in the game which fires at a rate of 0.05 seconds a shot when fully upgraded, and when placing 14 of them down, and letting the game play, it starts causing this issue. (This occurred before with other units all attacking at once.)
the picture below is a screenshot of what happens after the error occurs (It freezes the game periodically):
The Tower.Attack function gets called and can be stacked over 4999 times, until it eventually stops and starts running fine, but sometimes it keeps erroring, and I am quite stuck on trying to fix it.
This is the Tower Module script which handles each and every unit when placed. The important areas are the Tower.Attack and Tower.FindTarget function (Not the FindNextTarget function, which goes unused right now).
local PhysicsService = game:GetService("PhysicsService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Debris = game:GetService("Debris")
local TweenService = game:GetService("TweenService")
local Sounds = workspace.GameSounds
local CheckZoobieModule = require(ReplicatedStorage.Modules.CheckZoobieModule)
local Events = ReplicatedStorage:WaitForChild("Events")
local Functions = ReplicatedStorage:WaitForChild("Functions")
local ErrorEvent = Events:WaitForChild("ErrorText")
local ChangeTowerModeFunction = Functions:WaitForChild("ChangeTowerMode")
local SpawnTowerFunction = Functions:WaitForChild("SpawnTower")
local SellTowerFunction = Functions:WaitForChild("SellTower")
local AnimateTowerEvent = Events:WaitForChild("AnimateTower")
local RequestTowerFunction = Functions:WaitForChild("RequestTower")
local AnimateMultiTowerEvent = Events:WaitForChild("AnimateMultiTower")
local ShowStunnedTowerEvent = Events:WaitForChild("StunTowers")
local Map = nil
local MaxTowers = 40
local Tower = {}
function Tower.Optimize(TowerToOptimize)
local Humanoid = TowerToOptimize:FindFirstChild("Humanoid")
if TowerToOptimize:FindFirstChild("HumanoidRootPart") then
TowerToOptimize.HumanoidRootPart:SetNetworkOwner(nil)
elseif TowerToOptimize.PrimaryPart ~= nil then
TowerToOptimize.PrimaryPart:SetNetworkOwner(nil)
end
if not Humanoid then return end
Humanoid:SetStateEnabled(Enum.HumanoidStateType.Seated, false)
Humanoid:SetStateEnabled(Enum.HumanoidStateType.Jumping, false)
Humanoid:SetStateEnabled(Enum.HumanoidStateType.Running, false)
Humanoid:SetStateEnabled(Enum.HumanoidStateType.GettingUp, false)
Humanoid:SetStateEnabled(Enum.HumanoidStateType.Climbing, false)
Humanoid:SetStateEnabled(Enum.HumanoidStateType.Landed, false)
Humanoid:SetStateEnabled(Enum.HumanoidStateType.Ragdoll, false)
Humanoid:SetStateEnabled(Enum.HumanoidStateType.Freefall, false)
end
function Tower.SetCollisionGroup(TowerToSet)
for i, v in pairs(TowerToSet:GetDescendants()) do
if v:IsA("BasePart") and v.CollisionGroup ~= "Towers" then
v.CollisionGroup = "Towers"
end
end
end
function Tower.Stun(TowerToStun, StunTime) -- Unusued as of now
if TowerToStun and TowerToStun.Config.Debuffs:FindFirstChild("Stunned") then
local Stunned = Instance.new("IntValue")
Stunned.Name = "Stunned"
Stunned.Parent = TowerToStun.Config
Stunned.Value = StunTime
Debris:AddItem(Stunned, StunTime)
end
end
-- IMPORTANT FUNCTION BELOW ----------------------------------------------------
function Tower.FindTarget(NewTower:Model, Range:number, Mode:string, CanSeeHidden:Instance)
local BestTarget = nil
local BestWaypoint = nil
local BestHealth = nil
local BestDistance = nil
if Map == nil then
Map = workspace.CurrentMap:FindFirstChildOfClass("Folder")
end
for i, mob in ipairs(workspace.CurrentMobs:GetChildren()) do -- Part where the error occurs
local DistanceToMob = (mob.HumanoidRootPart.Position - NewTower.HumanoidRootPart.Position).Magnitude
local DistanceToWaypoint = (mob.HumanoidRootPart.Position - Map.Waypoints[mob.MovingTo.Value].Position).Magnitude
local IsHidden = mob.Config:FindFirstChild("Hidden")
local CanSeeHidden = NewTower.Config:FindFirstChild("HiddenDetection")
local IsFlying = mob.Config:FindFirstChild("Flying")
local CanSeeFlying = NewTower.Config:FindFirstChild("FlyingDetection")
local function Check()
if NewTower == nil then return end
if DistanceToMob <= Range and mob.Humanoid.Health > 0 then -- Also can happen here
if Mode == "Near" then
Range = DistanceToMob
BestTarget = mob
elseif Mode == "First" then
if not BestWaypoint or mob.MovingTo.Value >= BestWaypoint then
if BestWaypoint and mob.MovingTo.Value > BestWaypoint then
BestDistance = nil
end
BestWaypoint = mob.MovingTo.Value
if not BestDistance or DistanceToWaypoint < BestDistance then
BestDistance = DistanceToWaypoint
BestTarget = mob
end
end
elseif Mode == "Last" then
if not BestWaypoint or mob.MovingTo.Value <= BestWaypoint then
if BestWaypoint then
BestDistance = nil
end
BestWaypoint = mob.MovingTo.Value
if not BestDistance or DistanceToWaypoint > BestDistance then
BestDistance = DistanceToWaypoint
BestTarget = mob
end
end
elseif Mode == "Strong" then
if not BestHealth or mob.Humanoid.Health > BestHealth then
BestHealth = mob.Humanoid.Health
BestTarget = mob
end
elseif Mode == "Weak" then
if not BestHealth or mob.Humanoid.Health < BestHealth then
BestHealth = mob.Humanoid.Health
BestTarget = mob
end
end
end
end
if CanSeeHidden or CanSeeFlying then
Check()
else
if not IsHidden or IsFlying then
Check()
end
end
end
return BestTarget
end
function Tower.FindNextTarget(PreviousTarget, NewTower)
if Map == nil then
Map = workspace.CurrentMap:FindFirstChildOfClass("Folder")
end
local bestWaypoint2 = nil
local bestDistance2 = nil
local bestTarget2 = nil
for i, mob in ipairs(workspace.Mobs:GetChildren()) do
if mob ~= PreviousTarget and not mob:FindFirstChild(NewTower.Config.StringValue.Value) then
local distanceToMob = (mob.HumanoidRootPart.Position - PreviousTarget.HumanoidRootPart.Position).Magnitude
local distanceToWaypoint = (mob.HumanoidRootPart.Position - Map.Waypoints[mob.MovingTo.Value].Position).Magnitude
if not bestWaypoint2 or mob.MovingTo.Value >= bestWaypoint2 then
bestWaypoint2 = mob.MovingTo.Value
if not bestDistance2 or distanceToWaypoint < bestDistance2 then
bestDistance2 = distanceToWaypoint
bestTarget2 = mob
end
end
end
end
return bestTarget2
end
function Tower.Damage(Target, Damage)
if Target.Config:FindFirstChild("Immunity") then return end
if Target.Config:FindFirstChild("Tank") and Target.Config:FindFirstChild("Tank").Value then
if Target.Config:FindFirstChild("ShieldHP") then
Target.Config.ShieldHP.Value -= 1
if Target.Config.ShieldHP.Value <= 0 then
Target.Humanoid:TakeDamage(1)
end
else
Target.Humanoid:TakeDamage(1)
end
return
end
if Target.Config:FindFirstChild("DefensePercent") and Target.Config:FindFirstChild("ShieldHP") then -- If Defense and Shield
Target.Config.ShieldHP.Value -= Damage
if Target.Config.ShieldHP.Value <= 0 then
local RoundedDamage = math.round(Damage / (1 + (Target.Config:FindFirstChild("DefensePercent").Value / 100)))
Target.Humanoid:TakeDamage(RoundedDamage)
end
elseif Target.Config:FindFirstChild("ShieldHP") then -- If only Shield
Target.Config.ShieldHP.Value -= Damage
if Target.Config.ShieldHP.Value <= 0 then
Target.Humanoid:TakeDamage(Damage)
end
elseif Target.Config:FindFirstChild("DefensePercent") then -- If only Defense
local RoundedDamage = math.round(Damage / (1 + (Target.Config:FindFirstChild("DefensePercent").Value / 100)))
Target.Humanoid:TakeDamage(RoundedDamage)
else -- If Neither
Target.Humanoid:TakeDamage(Damage)
end
end
function Tower.CheckForRewardMoney(Target, playerWhoKilled, Damage)
--if Target.Humanoid.Health < Damage and Target:FindFirstChild("IsSummon") == nil then
-- player.Money.Value += Target.Humanoid.Health
--elseif Target.Humanoid.Health > Damage then
-- player.Money.Value += Damage
--end
--if Target.Humanoid.Health <= 0 then
-- player.Kills.Value += 1
--end
if Target.Humanoid.Health <= 0 then
if Target:FindFirstChild("IsSummon") then
playerWhoKilled.Kills.Value += 1
for _, player in ipairs(game.Players:GetChildren()) do
if player ~= playerWhoKilled then
player.Money.Value += math.floor(Target.Humanoid.MaxHealth / 10)
end
end
playerWhoKilled.Money.Value += math.floor(Target.Humanoid.MaxHealth / 5)
else
for _, player in ipairs(game.Players:GetChildren()) do
if player ~= playerWhoKilled then
player.Money.Value += math.floor(Target.Humanoid.MaxHealth / 2)
end
end
playerWhoKilled.Kills.Value += 1
playerWhoKilled.Money.Value += Target.Humanoid.MaxHealth
end
end
end
function Tower.Aim(NewTower, Enemy, Duration)
local TargetVector = Vector3.new(Enemy.HumanoidRootPart.Position.X, NewTower.HumanoidRootPart.Position.Y, Enemy.HumanoidRootPart.Position.Z)
local TargetCFrame = CFrame.new(NewTower.HumanoidRootPart.Position, TargetVector)
local Tweeninfo = TweenInfo.new(Duration, Enum.EasingStyle.Back, Enum.EasingDirection.Out, 0, false, 0)
local FaceTargetTween = TweenService:Create(NewTower.HumanoidRootPart, Tweeninfo, {CFrame = TargetCFrame})
FaceTargetTween:Play()
end
-- IMPORTANT FUNCTION BELOW -----------------------------------------------------------
function Tower.Attack(NewTower, player)
local Config = NewTower.Config
local Target = Tower.FindTarget(NewTower, Config.Range.Value, Config.TargetMode.Value, Config:FindFirstChild("HiddenDetection"))
local function Splash(Targeted, Obj, Damage)
Events:WaitForChild("EffectEvent"):FireAllClients(Targeted, Obj)
Tower.Damage(Targeted, Damage)
Tower.CheckForRewardMoney(Targeted, player, Config.Damage.Value)
local Radius = Obj.Config.SplashRadius.Value
local Mobs = workspace.CurrentMobs:GetChildren()
local Targets = {}
for i, target in pairs(Mobs) do
local Distance = (Targeted:WaitForChild("HumanoidRootPart").Position - target:WaitForChild("HumanoidRootPart").Position).Magnitude
if Distance <= Radius and target ~= Targeted then
table.insert(Targets, target)
Tower.Damage(target, Damage)
Tower.CheckForRewardMoney(target, player, Config.Damage.Value)
end
end
end
if Target and Target:FindFirstChild("Humanoid") and Target.Humanoid.Health > 0 then
if NewTower.Config.Debuffs:FindFirstChild("Stunned") then
repeat
task.wait()
until NewTower.Config.Debuffs:FindFirstChild("Stunned") == nil or NewTower == nil or NewTower.Config.Debuffs:FindFirstChild("Stunned") == nil and NewTower == nil
else
if Config:FindFirstChild("Burst") and Config:FindFirstChild("BurstDelay") then
local Debuffs = Config:FindFirstChild("Debuffs")
local BRange = Config:FindFirstChild("Range")
local BTarget = Config:FindFirstChild("TargetMode")
local BDelay = Config:FindFirstChild("BurstDelay")
for i = 1, Config.Burst.Value do
if not BRange then return end
if not BTarget then return end
if NewTower == nil then return end
Target = Tower.FindTarget(NewTower, BRange.Value, BTarget.Value, Config:FindFirstChild("HiddenDetection"))
if Target and Target:FindFirstChild("Humanoid") and Target.Humanoid.Health > 0 then
Tower.Aim(NewTower, Target, 0)
if NewTower.Animations:FindFirstChild("Attack") then
AnimateTowerEvent:FireAllClients(NewTower, "Attack", Target)
elseif NewTower.Animations:FindFirstChild("Left") or NewTower.Animations:FindFirstChild("Right") then
AnimateMultiTowerEvent:FireAllClients(NewTower, "Left", "Right", Target)
end
if NewTower.Config:FindFirstChild("SplashRadius") then
Splash(Target, NewTower, Config.Damage.Value)
else
Tower.Damage(Target, Config.Damage.Value)
Tower.CheckForRewardMoney(Target, player, Config.Damage.Value)
end
task.wait(Config.Cooldown.Value)
end
end
if BDelay == nil then
return
else
task.wait(Config.BurstDelay.Value)
--task.wait(BDelay.Value)
end
else
if NewTower == nil then return end
Tower.Aim(NewTower, Target, 0)
if NewTower.Animations:FindFirstChild("Attack") then
AnimateTowerEvent:FireAllClients(NewTower, "Attack", Target)
elseif NewTower.Animations:FindFirstChild("Left") or NewTower.Animations:FindFirstChild("Right") then
AnimateMultiTowerEvent:FireAllClients(NewTower, "Left", "Right", Target)
end
if NewTower.Config:FindFirstChild("SplashRadius") then
if NewTower.Config:FindFirstChild("SplashDamage") then
Tower.Damage(Target, Config.Damage.Value)
Tower.CheckForRewardMoney(Target, player, Config.Damage.Value)
Splash(Target, NewTower, Config.SplashDamage.Value)
else
Splash(Target, NewTower, Config.Damage.Value)
end
else
Tower.Damage(Target, Config.Damage.Value)
Tower.CheckForRewardMoney(Target, player, Config.Damage.Value)
end
task.wait(Config.Cooldown.Value)
end
end
end
task.wait()
if NewTower and NewTower.Parent then
Tower.Attack(NewTower, player) -- Where it stacks and then causes lag
else
task.wait(0.1)
return
end
end
function Tower.ProjectileConfig(NewTower, player, offset)
local config = NewTower.Config
local projSpeed = config:FindFirstChild("ProjSpeed").Value
local Projectile = ReplicatedStorage.Projectiles:FindFirstChild(config.Projectile.Value):Clone()
local Projectile2 = Projectile.Handle
Projectile.Parent = workspace.CurrentEffects
Projectile.CFrame = NewTower.HumanoidRootPart.CFrame
Projectile2.CFrame = NewTower.HumanoidRootPart.CFrame
Debris:AddItem(Projectile,config.ProjLive.Value)
local tweeninfo = TweenInfo.new(100/projSpeed, Enum.EasingStyle.Linear, Enum.EasingDirection.InOut)
local tweeninfo2 = TweenInfo.new(config.ProjLive.Value, Enum.EasingStyle.Exponential, Enum.EasingDirection.In)
local tran = {Transparency = 1}
local target2 = {Position = (NewTower.HumanoidRootPart.Position + Vector3.new()) + offset}
TweenService:Create(Projectile2,tweeninfo2,tran):Play()
TweenService:Create(Projectile2,tweeninfo,target2):Play()
TweenService:Create(Projectile,tweeninfo,target2):Play()
Projectile.Touched:Connect(function(hit)
if hit.Parent.Parent == workspace.CurrentMobs then
if hit.Parent.Humanoid.Health > 0 then
local towers = workspace.CurrentTowers:GetChildren()
for i, mytower in ipairs(towers) do
if mytower.Config.Owner.Value == player.Name and mytower == NewTower then
CheckZoobieModule.CheckZoobieHit(NewTower, hit, player)
end
end
end
end
end)
end
function Tower.Sell(player, Model)
if Model and Model:FindFirstChild("Config") then
if Model.Config.Owner.Value == player.Name then
player.Money.Value += (Model.Config.PlacementPrice.Value / 2)
player.PlacedTowers.Value -= 1
Model:Destroy()
return true
else
warn("Player Doesn't own that Tower")
return false
end
end
warn("Unable to Sell that Tower")
return false
end
SellTowerFunction.OnServerInvoke = Tower.Sell
function Tower.Spawn(player, name, Cframe, Previous)
local AllowedToSpawn = Tower.CheckSpawn(player, name, Previous)
local TowerToSpawn
local Config
local OldMode = nil
local Price = 0
local Owner
if Previous then
OldMode = Previous.Config.TargetMode.Value
TowerToSpawn = ReplicatedStorage.Towers.Upgrades[name]
Price = TowerToSpawn.Config.PlacementPrice.Value
Config = TowerToSpawn:FindFirstChild("Config")
else
TowerToSpawn = ReplicatedStorage.Towers[name]
Price = TowerToSpawn.Config.PlacementPrice.Value
Config = TowerToSpawn:FindFirstChild("Config")
end
if AllowedToSpawn and player.Money.Value >= Price then
local NewTower = TowerToSpawn:Clone()
if Previous == nil then
player.PlacedTowers.Value += 1
else
Previous:Destroy()
end
local OwnerValue = Instance.new("StringValue")
OwnerValue.Name = "Owner"
OwnerValue.Value = player.Name
OwnerValue.Parent = NewTower.Config
local TargetMode = Instance.new("StringValue")
TargetMode.Name = "TargetMode"
TargetMode.Value = OldMode or "First"
TargetMode.Parent = NewTower.Config
NewTower.Parent = workspace.CurrentTowers
NewTower.HumanoidRootPart.CFrame = Cframe
Tower.Optimize(NewTower)
Tower.SetCollisionGroup(NewTower)
local Height = (NewTower.HumanoidRootPart.Size.Y / 2) + NewTower["Left Leg"].Size.Y
local Offset = Vector3.new(0, -Height, 0)
local B = Instance.new("Part")
B.Name = "Border"
B.Color = Color3.new(0.666667, 0, 0)
B.Transparency = 1
B.Material = Enum.Material.Neon
B.TopSurface = Enum.SurfaceType.Smooth
B.BottomSurface = Enum.SurfaceType.Smooth
B.CanCollide = true
B.CanTouch = false
B.Anchored = true
B.CastShadow = false
B.Parent = NewTower
B.CFrame = NewTower.HumanoidRootPart.CFrame + Offset - Vector3.new(0, 0.1, 0)
B.Orientation = Vector3.new(0,0,0)
if NewTower:FindFirstChild("RankNo") then
if NewTower.RankNo.Value == 9 then
B.Size = Vector3.new(11, 0.5, 11)
else
B.Size = Vector3.new(7, 0.5, 7)
end
else
B.Size = Vector3.new(7, 0.5, 7)
end
B.CollisionGroup = "Towers"
player.Money.Value -= NewTower.Config.PlacementPrice.Value
coroutine.wrap(Tower.Attack)(NewTower, player)
return NewTower
elseif player.Money.Value < Price then
warn(player.Name .. " doesn't have enough money to buy this tower.")
ErrorEvent:FireClient(player, "You don't have enough money to place this tower")
return false
else
warn("Tower Don't exist: ", name)
return false
end
end
SpawnTowerFunction.OnServerInvoke = Tower.Spawn
function Tower.ChangeMode(player, Model)
if Model and Model:FindFirstChild("Config") then
if Model.Config.Owner.Value == player.Name then
local TargetMode = Model.Config.TargetMode
local Modes = {"First", "Last", "Near", "Strong", "Weak"}
local ModeIndex = table.find(Modes, TargetMode.Value)
if ModeIndex < #Modes then
TargetMode.Value = Modes[ModeIndex + 1]
else
TargetMode.Value = Modes[1]
end
return true
else
warn("Player is not owner of " .. Model.Name)
ErrorEvent:FireClient(player, "You don't own this tower")
return false
end
else
warn("Unable to change target mode")
return false
end
end
ChangeTowerModeFunction.OnServerInvoke = Tower.ChangeMode
function Tower.CheckSpawn(player, Name, previous)
local TowerExists = ReplicatedStorage.Towers:FindFirstChild(Name, true)
if TowerExists then
--if TowerExists.Config.PlacementPrice.Value <= player.Money.Value then
if previous or player.PlacedTowers.Value < MaxTowers then
return true
else
warn("Player Reached Max Limit")
ErrorEvent:FireClient(player, "You reached the limit on towers")
end
--else
-- warn("Lol Broke player")
--end
else
warn("Tower doesn't Exist")
end
return false
end
RequestTowerFunction.OnServerInvoke = Tower.CheckSpawn
return Tower
If anyone has any insight on this and can point me to any solutions possible, then I’d be very thankful for it.