Script to find closest zombie root part working at first but as zombies move, it doesn't switch targets

I am trying to make a reliable findNearestTorso() for my turret AI which automatically shoots at zombies. My original (and outdated) findNearestTorso was not cutting it and was barely even detecting the nearest torso so I decided to make a new one, and at first it detects the closest zombierootpart, but as I move them around it begins to struggle and stays detecting the same one.

local Zombies = {} --dictionary 
local ZombiesMagnitude = {} --array

function findLegibleTorsos()
	for i,v in pairs(game.Workspace:GetChildren()) do
		if v:IsA("Model") and v:FindFirstChild("ZombieRootPart") ~= nil then
			local torso = v:FindFirstChild("ZombieRootPart")
			if (torso.Position - mainpart.Position).magnitude < range then
				table.insert(ZombiesMagnitude, #ZombiesMagnitude + 1,(torso.Position - mainpart.Position).magnitude)
				Zombies[(torso.Position - mainpart.Position).magnitude] = torso
			end
		end
	end
end

function findNearestTorso()
	findLegibleTorsos()
	table.sort(ZombiesMagnitude)
	local torso = Zombies[ZombiesMagnitude[1]]
	--clear tables
	for i,v in pairs(Zombies) do
		i = nil
	end
	for i,v in pairs(ZombiesMagnitude) do
		i = nil
	end
	return torso
end`

(the function is being called later down in the script)
Thanks in advance

This is a very arduous method of pathfinding. There are two ways I would approach this, the more efficient way and the advanced but not-fit-for-this-context-way.

METHOD 1: The efficient way

Logically, a turret can only shoot one zombie at a time, which is the closest zombie. So there is no reason to sort the entire array just to find one closest zombie. So instead of sorting every zombie, simply write an algorithm to loop through Zombies and return the closest one:

local Zombies = {}

function findLegibleTorsos()
	-- deletes the old array
	Zombies = {}
	-- constructs the new array of Zombies
	for i,v in pairs(game.Workspace:GetChildren()) do
		if v:IsA("Model") and v:FindFirstChild("ZombieRootPart") ~= nil then
			local torso = v:FindFirstChild("ZombieRootPart")
			if (torso.Position - mainpart.Position).magnitude <= range then
				Zombies[#Zombies + 1] = v -- appends the zombie to the end of the array
			end
		end
	end
end

function findNearestTorso()
	-- loops through the entire array and returns the closest zombie
	local closestZombie = Zombies[1]
	local closestRange = (closestZombie.ZombieRootPart.Position - mainpart.Position).magnitude
	
	for i = 2, #Zombies do -- start at the 2nd index so the first zombie doesnt check itself
		local zombie = Zombies[i]
		local range = (zombie.ZombieRootPart.Position - mainpart.Position).magnitude
		
		if range < closestRange then
			closestZombie = zombie
			closestRange = range
		end
	end
	
	return closestZombie.ZombieRootPart 
end

METHOD 2: Your way, but better
Looking at your code, my first instinct was to introduce you to the table.sort() function’s custom sort function parameter. It can easily perform exactly what your script does in simpler terms. Let me brief you:

local array = {4, 2, 1, 3}

table.sort(array) -- sorts from least to greatest by default
print(table.unpack(array)) -- 1 2 3 4

Now looking a bit closer, table.sort() actually has a second parameter, the sort function. You can make the sort function however you like. For example, you can make it sort in descending order instead.

table.sort(array, function(a, b) -- b is the value at the index directly after a
-- a and b are in the right spot when this is true
	return a > b -- a is in the right spot when it's greater than the next value
end)
print(table.unpack(array)) -- 4 3 2 1

Now, using this custom function, we can actually sort by property of an object. In this case, we should sort the zombies based on how close their Position property is to other character’s

local Zombies = {}

function findLegibleTorsos()
	-- deletes the old array
	Zombies = {}
	-- constructs the new array of Zombies
	for i,v in pairs(game.Workspace:GetChildren()) do
		if v:IsA("Model") and v:FindFirstChild("ZombieRootPart") ~= nil then
			local torso = v:FindFirstChild("ZombieRootPart")
			if (torso.Position - mainpart.Position).magnitude <= range then
				table.insert(Zombies, v) -- appends the zombie to the end of the array
			end
		end
	end
end

function findNearestTorso()
	-- sorts the array based on zombie proximity in order of closest to farthest
	table.sort(Zombies, function(a, b)
		return (a.ZombieRootPart.Position - mainpart.Position).magnitude < (b.ZombieRootPart.Position - mainpart.Position).magnitude
	end)
	return Zombies[1].ZombieRootPart -- because the closest zombie is always the first on the list
end

Personally, I would use the findLegibleTorsos() function every frame to update the turret’s radar. so I would put the sort in that function. But the code above does everything your old code did in a much simpler fashion rather than holding 2 arrays and indexing by magnitude. In addition, you should never set keys that are prone to duplicates. If 2 zombies are exactly 2 studs away, the 2nd one would have overwriten the other zombie, potentially erroring your code.

Edit: sorry about this, but I realized that findLegibleTorsos() stores the torsos on your version, while it sorts the zombie models in mine. I edited the findNearestTorso() function so it would work though.

3 Likes

Thanks! I did not think it could be this easy but you proved me wrong

1 Like

I found a weird issue where the turret AI is detecting a “ghost” zombie that is not even in workspace. I looked through the script thoroughly and made sure that the table was being cleared out so it wasn’t remember some sort of ghost rootpart, but the turret is still shooting at its non-existent rootpart, and the turret will pretend like it is there and will even keep on shooting at it once it becomes the closest “thing” to the turret.

ah, the data leaking totally flew over my head. Make sure to start from a new array each time findLegibleTorsos is called, so in that function, set Zombies to an empty table so it begins from scratch each frame like so:

function findLegibleTorsos()
	-- deletes the old array
	Zombies = {}
	-- constructs the new array of Zombies
	for i,v in pairs(game.Workspace:GetChildren()) do
		if v:IsA("Model") and v:FindFirstChild("ZombieRootPart") ~= nil then
			local torso = v:FindFirstChild("ZombieRootPart")
			if (torso.Position - mainpart.Position).magnitude <= range then
				Zombies[#Zombies + 1] = v -- appends the zombie to the end of the array
			end
		end
	end
end

I’ll fix this in my original reply too.

This seems to have worked
Thankyou!
I will also be sure to look into data leaking more

1 Like

No problem. Also, the word I was looking for was memory leak. A memory leak is when data that you don’t need anymore stays inside a variable, causing old, unnecessary data to pile up in tables. When making arrays, never forget about the array’s and its indexes’ method of deletion.

In this case, we add new zombies into the array each time we call findLegibleTorsos() to update it’s radar, so the old zombies should be deleted from the turret’s radar because that is old information