How To Handle Two Scripts Trying to Simultaneously Destroy an Object

I am making a tower defense game at the moment. Currently, towers target enemies within range and deal damage to enemies as expected. The problem arises when two towers simultaneously target and fire at the same enemy within a short time frame of one another.

The Problem:

Two scripts from two separate towers target the same enemy. The first script deals damage to the enemy, causing it to drop to 0 health and destroy itself via a script that is a child to the enemy being destroyed.

Then, the second script attempts to target and deal damage to the same enemy targeted by the first tower, but the object it is targeting has been destroyed. As such, any code referencing the object being targeted now throws an error as the target is nil from being destroyed.

I’m stuck trying to find a way to implement this which will cause the second tower to always see the object it is trying to damage is nil, and fail to cause errors by indexing an object set to nil

The specific line(s) of code causing the error:

-- This is where the error occurs.
			-- If the enemy is destroyed after the first line's conditional executes, it will throw an error.
			-- Despite ClosestBlox being not nil during the conditional, it may be destroyed after executing as true.
			-- This causes issues when many enemies are on screen at once or many towers firing at the same enemy.
			if ClosestBlox then
				local HitsToPop = ClosestBlox:FindFirstChild("Values"):FindFirstChild("HitsToPop")
				if HitsToPop.Value > 0 then
					HitsToPop.Value -= 1
				end
			end

Main Driving Code For Targeting after Target is found:

while true do
	-- Get Enemy Furthest Along Track in Radius
	local ClosestBlox = First()
	
	if ClosestBlox then
		-- If A Blox exists, rayast for visuals
		local raycastParams = RaycastParams.new()
		raycastParams.FilterDescendantsInstances = {Model}
		raycastParams.FilterType = Enum.RaycastFilterType.Blacklist
		raycastParams.IgnoreWater = true

		local RaycastResult = workspace:Raycast(
			PrimaryPart.Position,
			(ClosestBlox.PrimaryPart.Position - PrimaryPart.Position) * 50, 
			raycastParams)

		if RaycastResult then
			-- RayCast Exists, Display Beam
			local distance = (PrimaryPart.Position - ClosestBlox.PrimaryPart.Position).Magnitude
			local p = Instance.new("Part")
			p.Anchored = true
			p.CanCollide = false
			p.Size = Vector3.new(0.1, 0.1, distance)
			p.Color = Color3.new(1, 0, 0)
			p.CFrame = CFrame.new(PrimaryPart.Position, RaycastResult.Position)*CFrame.new(0, 0, -distance/2)
			p.Parent = workspace
			wait(0.1)
			p:Destroy()
		
			if ClosestBlox then
				local HitsToPop = ClosestBlox:FindFirstChild("Values"):FindFirstChild("HitsToPop")
				if HitsToPop.Value > 0 then
					HitsToPop.Value -= 1
				end
			end
		else
			print("no raycast")
		end
	end
	
	wait(FireRate.Value)
end

Full Targeting Code:

local Model = script.Parent
local PrimaryPart = script.Parent.PrimaryPart
local FiringPosition = script.Parent.PrimaryPart.Position
local FireRate = script.Parent:WaitForChild("Values"):WaitForChild("BaseFireRate")
local Radius = script.Parent:WaitForChild("Values"):WaitForChild("AttackRadius")

local FiringModes = require(game.ReplicatedStorage.Modules:WaitForChild("Enums")).FiringModes

local FiringMode = FiringModes.First

function First()
	local AllBlox = game.workspace.Map:WaitForChild("BloxOnTrack"):GetChildren()
	local AllBloxInRadius = {}
	local ClosestNodeBlox = {}

	if (#AllBlox < 1) then
		wait(0.1)
		return nil
	end
	
	
	for i = 1, #AllBlox do
		local BloxPosition = AllBlox[i].PrimaryPart.Position
		if ( (FiringPosition - BloxPosition).Magnitude <= Radius.Value) then
			table.insert(AllBloxInRadius, AllBlox[i])
		end
	end
	
	if #AllBloxInRadius > 0 then
		local HighestNode = 0

		for i = 1, #AllBloxInRadius do
			if(HighestNode < AllBloxInRadius[i]:WaitForChild("Values"):WaitForChild("CurrentNode").Value) then
				HighestNode = AllBloxInRadius[i]:WaitForChild("Values"):WaitForChild("CurrentNode").Value
			end
		end

		if (#AllBloxInRadius < 1) then
			return
		end

		for i = 1, #AllBloxInRadius do
			if(AllBloxInRadius[i]:WaitForChild("Values"):WaitForChild("CurrentNode").Value == HighestNode) then
				table.insert(ClosestNodeBlox, AllBloxInRadius[i])
			end
		end


		local NextNodePosition = game.workspace.Map:WaitForChild("PathNodes"):WaitForChild(tostring(HighestNode + 1)).Position
		local ClosestMagnitude = (ClosestNodeBlox[1].PrimaryPart.Position - NextNodePosition).Magnitude
		local ClosestBlox = ClosestNodeBlox[1]

		for i = 2, #ClosestNodeBlox do
			local Magnitude = (ClosestNodeBlox[i].PrimaryPart.Position - NextNodePosition).Magnitude
			if ( Magnitude < ClosestMagnitude) then
				ClosestMagnitude = Magnitude
				ClosestBlox = ClosestNodeBlox[i]
			end
		end
		return ClosestBlox
	else
		return nil
	end
	
end

while true do
	local ClosestBlox = First()
	
	if ClosestBlox then
		local raycastParams = RaycastParams.new()
		raycastParams.FilterDescendantsInstances = {Model}
		raycastParams.FilterType = Enum.RaycastFilterType.Blacklist
		raycastParams.IgnoreWater = true

		local RaycastResult = workspace:Raycast(
			PrimaryPart.Position,
			(ClosestBlox.PrimaryPart.Position - PrimaryPart.Position) * 50, 
			raycastParams)

		if RaycastResult then
			local distance = (PrimaryPart.Position - ClosestBlox.PrimaryPart.Position).Magnitude
			local p = Instance.new("Part")
			p.Anchored = true
			p.CanCollide = false
			p.Size = Vector3.new(0.1, 0.1, distance)
			p.Color = Color3.new(1, 0, 0)
			p.CFrame = CFrame.new(PrimaryPart.Position, RaycastResult.Position)*CFrame.new(0, 0, -distance/2)
			p.Parent = workspace
			wait(0.1)
			p:Destroy()
			if ClosestBlox then
				local HitsToPop = ClosestBlox:FindFirstChild("Values"):FindFirstChild("HitsToPop")
				if HitsToPop.Value > 0 then
					HitsToPop.Value -= 1
				end
			end
		else
			print("no raycast")
		end
	end
	
	wait(FireRate.Value)
end

1 Like

Try checking if it’s parent is set to nil. For example,

if ClosestBlox and ClosestBlox.Parent then
1 Like

Also, you can detect the Blox in a certain radius easier using WorldRoot | Roblox Creator Documentation.

1 Like

I only learned about the new WorldRoot functions after writing the initial implementation so it just hasn’t been changed over yet. Thank you though!

This wouldn’t do much unfortunately as the item returned by ClosestBlox is a model parented to a folder.

If there’s more details about the code I can provide to help let me know. Thank you for the suggestion and effort :heart:

You could pcall() the :Destroy() in both scripts and that way it won’t error if the object was already destroyed by the other script.

1 Like

After deciding to read your code and attempt to understand it, I have realized that you are still learning Lua. I’ve decided to give you a list of improvements you could make (code-wise), just to help you learn.

  1. Instead of using the length of a table while looping through it, you can use https://education.roblox.com/en-us/resources/pairs-and-ipairs-intro.
  2. You can also use the task | Roblox Creator Documentation library for things like wait and coroutines. In short, it works with the Task Scheduler | Roblox Creator Documentation better than the previous global functions did.
  3. You don’t have to use game.workspace because there is already a global variable called workspace. Just a little tip so you don’t have to write five extra characters. :wink:
  4. When checking if a table has any values inside, compare it’s length using #array > 0 instead of #array < 1, as it is generally more readable. However, this is just an organizational tip that can be ignored.


Now for the actual code.

To start with, this line of code is useless:

It’s already inside a conditional statement saying:

In other words, it’s not needed.

Second, you can use the pairs iterator function to iterate through loops. It’s slightly faster than ipairs. This will give you the index key and the value in that index. For example, you could replace this;

with this:

for _,Blox in pairs(AllBlox) do
	if ((FiringPosition - Blox.PrimaryPart.Position).Magnitude <= Radius.Value) then
		table.insert(AllBloxInRadius, AllBlox[i])
	end
end

The _ is the same value as the i you had beforehand. The only difference is that this loop won’t sort through the array in order. But it doesn’t need to so it’s fine. You can iterate through the array how you want to but I think you’ll find, going forward, that pairs and ipairs are much more useful.

Third, when sorting through to find the closest Blox, you truly only have to use one loop. You could do it like this:

local function First()
	local AllBlox = workspace:WaitForChild("Map"):WaitForChild("BloxOnTrack"):GetChildren()
	if #AllBlox > 0 then return end -- return if there are no blox in the array
	
	local radius = Radius.Value -- the current radius value
	
	local ClosestBlox -- the closest blox
	local ClosestDistance = radius -- the distance the closest blox is from the tower
	
	for _,Blox in pairs(AllBlox) do
		local distance = (FiringPosition - Blox.PrimaryPart.Position).Magnitude
		
		if (distance <= radius) and (distance < ClosestDistance) then -- if blox is in radius and its distance is less than the current
			ClosestDistance = distance
			ClosestBlox = Blox
		end
	end
	
	return ClosestBlox
end
2 Likes

Actually it should work. Did you try it?

The code is erroring because the instance it’s referring to has been destroyed. When an instance is destroyed, as stated in the Instance | Roblox Creator Documentation documentation, its parent is set to nil and not able to be changed further. This means that if instance has already been destroyed its parent will be nil. Thus, it won’t pass through the conditional statement.

1 Like

Hi, I apologize for the slow response I had some things in real life come up.

Anyway, I really appreciate the help and effort! Yes I am still learning lua and truthfully was a little nervous about posting my full code because I know it has errors haha. I’m coming from primarily Python and C++ and have not spent nearly as much time in Lua so far. These are some great tips and I’ll be making use of them, thank you.

However, I have tested the conditional statement extensively to see exactly why the error was happening. The error occurs if the Object being targeted by both scripts is destroyed after the conditional statement executes but before the referencing of the properties of ClosestBlox occurs. It is a very small timing window for the bug to occur but it does happen when I have many enemies on screen at once, and if towers are firing with a high fire rate.

I am going to look into pcall() and other workarounds to the problem in the meantime, as pcall seems to recreate the try catch behavior I was looking for from other languages, at least partially.

1 Like

This ended up working and solved the issue. Did not know about pcall(). thanks!