Search code examples
luaroblox

Attempt to index nil with 'FindFirstChild' - Lua


I am creating a fighting simulator game in Roblox, currently I am trying to make it so NPC's don't spawn in the spawn area I have set. I have done so by creating an invisible, non collidable part that detects when something touches it. It checks if the object that touched it was a humanoid and if it's a player and destroys it if it's not a player. What I'm most confused about is that it works perfectly fine, npc's don't spawn in the spawn area, however I still receive an error "Attempt to index nil with 'FindFirstChild' and my print message I have set for debugging also doesn't fire. Maybe I shouldn't pay too much attention to it since it does in fact destroy npc's that spawn in it. But for the sake of gaining knowledge (and maybe preventing bugs in the future), I'm trying to figure out why.

My first guess was it was registering an object touching it before the npc fully loaded in, meaning it tried finding a "Humanoid" part before it existed, but removed it after it found one which would explain why it's throwing an error but also still destroying the npc. I did so by creating a boolean that would wait until it found a Humanoid part and destroy it after one was found. This did not stop the error. I have also tried replacing 'FindFirstChild' with 'WaitForChild' and still the error occurred. The last thing I thought might be the issue was that I have other objects in the spawn area. I assumed maybe something else that didn't have a Humanoid property might be firing it. However this was also proven not to be the case. Down below is the script to destroy npc in spawn area, and below that is the code that spawns the npc's.

NPC Remover Script:

local part = script.Parent
part.Transparency = 1

local function onTouch(hit)
    if hit.Parent:FindFirstChild("Humanoid") and not game.Players:GetPlayerFromCharacter(hit.Parent) then
        hit.Parent:Destroy()
        print("Destroyed")
    end
end

part.Touched:Connect(onTouch)

Npc Spawn Script:

local Area1_Enemies = game.Workspace.Area1.Enemies -- Grabs enemies folder in Area 1

local noob = "Noob"
local basicSpawnTime = 0.5 -- Sets Basic Type Npc Spawn Rate

while true do
    local randZ = math.random(-744, -473)
    local noobCount = 0
    
    for _, v in pairs(Area1_Enemies:GetChildren()) do -- Counts every enemey named Noob in Area1 Enemies folder
        
        if v.Name == noob then
            noobCount += 1
        end
    end
    
    if noobCount < 50 then -- If Area 1 Folder has less then 5 Noobs, spawn more
        local noobClone = game.ServerStorage.Enemies.Area1.Noob:Clone()
        noobClone.Parent = game.Workspace.Area1.Enemies
        noobClone.HumanoidRootPart.CFrame = CFrame.new(math.random(-61, 273), 6, randZ)
    end
    task.wait(basicSpawnTime) -- Time before npc respawns
end

Solution

  • The error attempt to index nil with X is telling you that you are trying to do something with an object that no longer exists. And I believe that you might have a race condition in the code that destroys the NPC.

    It's a common problem that a character model will cause the Touched event to fire multiple times, because you might have both feet touching a block, or the torso and the hands. So here's the series of events that I think is happening :

    1. An NPC character model touches the part and multiple parts trigger the Touched event. For the sake of example, let's say it's both legs. So that means that 2 Touched events are queued up to be processed.
    2. The first onTouch handler detects that this is an NPC and Destroy()'s the part's Parent, effectively removing the whole model.
    3. Calling the Destroy() function sets the part's Parent to nil and starts cleaning up references to the part.
    4. Then, the next onTouch handler tries to execute.
    5. Since hit has been destroyed by the previous call, hit.Parent is nil, and you call hit.Parent:FindFirstChild which trips the error because you are trying to call a function on a nil object.

    So since you are deleting objects, it might make sense to do some sanity checks and escape if an object has been queued for deletion.

    local function onTouch(hit)
        -- escape if this object has already been deleted
        if hit == nil or hit.Parent == nil then
            return
        end
    
        local hasHumanoid = hit.Parent:FindFirstChild("Humanoid")
        local isNotPlayer = not game.Players:GetPlayerFromCharacter(hit.Parent)
        if hasHumanoid and isNotPlayer then
            hit.Parent:Destroy()
            print("Destroyed")
        end
    end