Table.remove() not finding table values correctly then deleting table

Context: I am creating a zombie spawning system for my game, and all of the zombies are controlled using one script and collection service. I’m not very good at coding AI, so I followed the youtuber Y3llow Mustang’s “AI CONTROLLER” video. This video showed me how to create a zombie system. I am trying to add on to this system, and that is where the problem lies.

I want to achieve a system where zombies can spawn by touching a part (for testing purposes, as the round system to my game is yet to be made). This works so far by duplicating ServerStorage zombies into workspace, and giving them a “Zombie” tag. (Both of these events happen in the same touched event.)

HOWEVER! Seemingly randomly, my code will start to remove every zombie in a table every time a “removeZombie” funciton is called, only after I’ve already removed (killed with a sword) 2 or 3 zombies from this table (everytime a zombie dies, it gets removed). Moreover, when the zombies start to be unintentionally removed, the index from table.find() starts to print “nil”, before removing the entire table one by one so the table eventually prints “{ }”. However, when these zombies are removed unintentionally, they are not dead humanoids. The zombie characters still follow walkpoints, but they cannot deal any damage.

I have tried bugtesting the problem myself and I have looked for similar issues on the roblox DevForum. Whether it is because I am blind, bad at searching, or the post doesn’t exist, I don’t know. However, I have not been able to find a similar issue faced by another person.
After that, you should include more details if you have any. Try to make your topic as descriptive as possible, so that it’s easier for people to help you!

VIDEO:

-- This code is where my video shows most of the prints coming from

function removeZombie(zombie)
	local index = table.find(AliveZombies, zombie)
	table.remove(AliveZombies, index)
	if index then
		print("After ".. index.. " remove")
	else
		print(index)
	end

	print(AliveZombies)
	wait(5)
	zombie.char:Destroy()
end

--this function is the only function that calls "removeZombie"


function AddZombie(zombieHumanoid) 
	table.insert(AliveZombies, {
		char = zombieHumanoid.Parent,
		root = zombieHumanoid.RootPart,
		human = zombieHumanoid,
		target = nil,
		grabAnim = zombieHumanoid:LoadAnimation(zombieHumanoid.Parent.Grab)
	})
	for _, zombie in pairs(AliveZombies) do
		if zombie.human == zombieHumanoid then
			zombie.human.Died:Connect(function() removeZombie(zombie) end) --called here
			for i, v in pairs(zombie.char:GetDescendants()) do
				if v:IsA("BasePart") and v:CanSetNetworkOwnership() then
					v:SetNetworkOwner(nil)
				end
			end
			ZombieSpawner(moveHandler, zombie)
			break
		end
	end
end

--these are the only two functions that call "AddZombie" ( where the argument zombieHumanoid comes from)


CollectionService:GetInstanceAddedSignal("Zombie"):Connect(function(zombieHumanoid) 
	AddZombie(zombieHumanoid)
end)

function initalize()
	for _, v in pairs(CollectionService:GetTagged("Zombie")) do
		local found = false
		for _, x in pairs(AliveZombies) do
			if x.human == v then
				found = true
			end
		end
		
		if not found then
			AddZombie(v)
		end
	end
end

Apologies for the code dump-- I’m not entirely sure where the problem originates and therefore tried to include all the code that might be originating the problem.

One more piece of code-- this is the code that spawns the zombies when you touch the part shown in the video.

local cooldown = false

workspace.SpawnZombies.Touched:Connect(function(hit)
	if hit.Parent:IsA("Model") then
		local player = Players:GetPlayerFromCharacter(hit.Parent)
		if player then
			if cooldown == false then
				cooldown = true
				for i = 1, 5, 1 do
					local Zclone = ServersStorage.Enemies.Basic.ZombieNormal:Clone()
					Zclone.Parent = workspace.Enemies.Basic
					Zclone.HumanoidRootPart.CFrame = CFrame.new(workspace.SpawnZombies.Position) * CFrame.new(-15,2,0)
					game:GetService("CollectionService"):AddTag(Zclone.Humanoid, "Zombie")
				end
				wait(1)
				cooldown = false
			end
		end
	end
end)

Thank you very much for reading this! Any help is appreciated, as I am kind of a noob to collectionservice and Ai. Have a good day! :slight_smile:

2 Likes

The reason your AliveZombies list suddenly empties itself is that when table.find can’t locate the zombie you passed in, it returns nil. You then call table.remove(AliveZombies, nil), which is essentially the same as table.remove(AliveZombies) - it removes the last entry from the list.

Each time you try to remove a zombie that isn’t in the table you are accidentally removing the last indice instead, so after a few calls the entire array becomes empty. To fix it you must only call table.remove when table.find actually returns a valid index (i.e index ~= nil)

function removeZombie(zombie)
    local idx = table.find(AliveZombies, zombie)
    if not idx then
        warn("Zombie is not in AliveZombies")
        return
    end
    table.remove(AliveZombies, idx)
    zombie.char:Destroy()
end

Also, instead of scanning an array with table.find consider keeping a dictionary of zombies so your code runs in O(1):

local ZombieMap = {}

function AddZombie(zH)
    table.insert(AliveZombies, zH)
    ZombieMap[zH] = #AliveZombies
    zH.Died:Connect(function() removeZombie(zH) end)
end

function removeZombie(zH)
    local idx = ZombieMap[zH]
    if not idx then return end
    table.remove(AliveZombies, idx)
    ZombieMap[zH] = nil
    zH.Parent:Destroy()
end

1 Like

Thank you so much! I really do appreciate the time you took to comment this. This was exactly the issue! :slight_smile:
On another note, I will look into (totally not copy) the other AddZombie() system using a dictionary as you recommended. Have a good day!

1 Like

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