Module Function for units in a tower defense game gets an Stack overflow error

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.

1 Like

Your function Check is recursive. Every time you call a function your program jumps to the memory related to the function but adds some data to the stack. This data is going to be information on how to get back to where you were before running the function, and every local variable inside the function. The stack though has a fixed size and so recursion will keep adding data into the stack until you hit it’s limit and it won’t be able to store information there anymore crashing your program. The stack is pretty big though, so you’ve probably accidentally created in infinite loop in your recursion (or just have way too many things to check). So to fix it make sure that you always have an exit condition that will trigger in recursion, and only use recursion for problems that won’t call itself too deeply. You also may just want to avoid recursion. Except for very specific cases it tends to be slower (unless the language optimizes it which lua doesn’t) and can be trickier to debug than iterative approaches.

tldr you are calling check too many times inside itself. You also don’t appear to ever change any of the things it looks for to know to not call check again making it infinite.

I see, that is very informative and really helps me narrow down what the main issue is. Although, with how delicate the code is in it’s current state, I’m afraid to make any drastic changes which might break it. What should I do to the code to fix it?