[Tower Defense Game] Towers constantly switching targets whenever a different type of enemy comes into range

Hi devs!

Today. I was trying to make my tower find enemies from a table regarding their positions and alpha value. It works really well when there are 1 or more enemies with the same name or type. But for some reason when a enemy with a different name came into range the towers constantly switch random targets and sometimes it even stops attacking

Video showing the issue::

alt video:
https://cdn.discordapp.com/attachments/1333065859614244995/1421041297229348874/bandicam_2025-09-26_15-51-08-630.mp4?ex=68d79756&is=68d645d6&hm=36fb17e3c7ef937d9b9e93ebe327ac0b99ba0d215c4bf192ae01803db00d9897&


Here are my scripts involving targetting

Enemy

local activeCharacters = {}

function class.new(name)
	local modelTemplate = game.ReplicatedStorage.Common.Server:FindFirstChild(name)
	if not modelTemplate then
		warn(tostring(name) .. "' does not exist in ReplicatedStorage.Characters")
		return nil
	end

	local model = modelTemplate:Clone()

	model:SetAttribute("Alpha", 0)
	model:SetAttribute("Speed", model:GetAttribute("Speed") or 1)
	model:SetAttribute("Health", model:GetAttribute("Health") or 100)
	model.Parent = workspace.Characters

	local self: Enemy  = setmetatable({}, class) :: any
	self.Model = model :: Model
	self.CFrameIndex = 1
	
	for _, playerObj in plr:GetPlayers() do
		myPacket.SpawnEnemy:FireClient(playerObj, self.Model)
	end

	local rotationalOffset = Vector3.new(0, 0, math.rad(0))
	local minOffset = -1.1111
	local maxOffset = 1.1111
	local offset = (math.random() * (maxOffset - minOffset)) + minOffset

	local rotationalCFrame = CFrame.Angles(rotationalOffset.X, rotationalOffset.Y, rotationalOffset.Z)
	local offsetCFrame = CFrame.new(offset, 0, 0)

	if #wayPositions == 0 then
		for i = 1, #workspace.Path:GetChildren() do
			local waypoints = workspace.Path[i]
			local adjustedPositions = waypoints.Position
			table.insert(wayPositions, adjustedPositions)
		end
	end

	self.NewPath = BezierPath.new(wayPositions, 2.5)
	self.PathLength = self.NewPath:GetPathLength()

	if self.PathLength == 0 then
		warn("Path length is zero for " .. model.Name)
		model:Destroy()
		return nil
	end 

	self.Alpha = 0
	self.Speed = model:GetAttribute("Speed")
	self.Health = self.Model.Health.Value
	self.Destroyed = false
	
	self.EnemyCFrame = CFrame.new(0,0,0)
	
	table.insert(activeCharacters, self)

	local canCFrame = true

	while self.Alpha < 1 and self.Model do
		if not self.Model then return end
		if not self.Model:FindFirstChild("Alpha") then return end
		if self.Model then
			if self.Destroyed then return end
			if self.Health <= 0 then
				for i = 1, #activeCharacters do
					if activeCharacters[i] == self then
						table.remove(activeCharacters, i)
						break
					end
				end
				self.Alpha = 1
				self.Model:Destroy()
				self.Destroyed = true
				return
			end
			if self.Alpha < 1 then
				self.Alpha = self.Alpha + (self.Model:GetAttribute("Speed") * game:GetService("RunService").Heartbeat:Wait()) / self.PathLength * workspace.GlobalInfo.Tick.Value
				if self.Alpha > 1 then self.Alpha = 1 end
				
				local newCFrame = self.NewPath:CalculateUniformCFrame(self.Alpha)
				self.EnemyCFrame = newCFrame
			else
				game.ReplicatedStorage.GlobalSounds.BaseHit:Play()
				self.Model:Destroy()
				self.Destroyed = true
			end
		end
	end
	workspace.Map.Base.Health.Value -= self.Model.Health.Value
	self.Model:Destroy()
	self.Destroyed = true
	game.ReplicatedStorage.GlobalSounds.BaseHit:Play()

	return self
end

function class:Spawn(name, amount, cooldown, waitBetween)
	task.delay(waitBetween / workspace.GlobalInfo.Tick.Value, function()
		for i = 1, amount do
			task.spawn(function()
				class.new(name)
			end)
			task.wait(cooldown / workspace.GlobalInfo.Tick.Value)
		end
	end)
end

function class:GetTargetingData(Enemy): { any }
	return {
		activeCharacters
	}
end


Tower

function class.new(name, cfra, player)
	local self  = setmetatable({}, class)
	local modelTemplate = game.ReplicatedStorage.Common.Towers.Server:FindFirstChild(name)
	if not modelTemplate then
		warn("Character: Model '" .. tostring(name) .. "' not found in ReplicatedStorage.Characters")
		return nil
	end

	local model = modelTemplate:Clone()
	
	model.Parent = workspace:FindFirstChild("Towers")
	
	self.cfra = cfra
	self.Model = model
	self.Model.TowerCFrame.Value = cfra
	self.module = require(game.ReplicatedStorage.Common.Towers[self.Model.Name])
	
	player.Money.Value -= game.ReplicatedStorage.Common.TowerPrices:FindFirstChild(self.Model.Name).level1.Value --self.Model.Cost.Value
	self.Model.Owner.Value = player.Name
	
	for _, playerObj in plr:GetPlayers() do
		myPacket.SpawnTower:FireClient(playerObj, self.Model)
	end
	
	task.spawn(function()
		local lastAttackTime = 0 --os.clock() - self.module[self.Model.level.Value].firerate

		local connection
		connection = runService.Heartbeat:Connect(function()
			if not self.Model or not self.Model.Parent or self.Model.Health.Value <= 0 then
				if connection then
					connection:Disconnect()
				end
				return
			end

			local now = os.clock()
			if now - lastAttackTime >= self.module[self.Model.level.Value].firerate / workspace.GlobalInfo.Tick.Value then
				local target = class:FindTarget(self.Model, self.module[self.Model.level.Value].range)
				if target and self.Model.Health.Value > 0 then
					local damage = self.module[self.Model.level.Value].damage
					local targetHealth = target[1].Health
					local actualDamage = math.min(damage, targetHealth)

					target[1].Health = targetHealth - actualDamage

					player.Money.Value += actualDamage

					for _, playerObj in plr:GetPlayers() do
						myPacket.TowerAttack:FireClient(playerObj, self.Model, target[1].EnemyCFrame.Position)
					end
				end
				lastAttackTime = now
			end
		end)
	end)
	
	return self
end

function class:FindTarget(Model, range)
	local targetingData = shared.Enemy:GetTargetingData()
	local bestTarget = nil

	local bestWaypoint = nil
	local bestDistance = nil
	local bestHealth = nil
	
	for _, mob in pairs(targetingData) do
		local distanceToMob = (mob[1].EnemyCFrame.Position - Model.TowerCFrame.Value.Position).Magnitude
		local distanceToWaypoint = mob.Alpha

		if distanceToMob <= range then
			if Model.Mode.Value == "Near" then
				range = distanceToMob
				bestTarget = mob
			elseif Model.Mode.Value == "First" then
				if not bestWaypoint or mob.Alpha > bestWaypoint then
					bestWaypoint = mob.Alpha
					bestDistance = nil

					if not bestDistance or distanceToWaypoint < bestDistance then
						bestDistance = distanceToWaypoint
						bestTarget = mob
					end
				end
				if mob.Alpha == bestWaypoint and (not bestDistance or distanceToWaypoint < bestDistance) then
					bestDistance = distanceToWaypoint
					bestTarget = mob
				end
			elseif Model.Mode.Value == "Last" then
				if not bestWaypoint or mob.Alpha < bestWaypoint then
					bestWaypoint = mob.Alpha
					bestDistance = nil

					if not bestDistance or distanceToWaypoint > bestDistance then
						bestDistance = distanceToWaypoint
						bestTarget = mob
					end
				end
				if mob.Alpha == bestWaypoint and (not bestDistance or distanceToWaypoint > bestDistance) then
					bestDistance = distanceToWaypoint
					bestTarget = mob
				end
			end
		else
		end 
	end
	return bestTarget
end

Any help is appreciated!

1 Like

heres more video about the issue with 2 towers attacking

Inserl print() in each section and test again. This is a easy way to detect problem.

I forgot to clarify this was already fixed

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