Hello!
I’ve recently been cleaning up my scripts, and found out that I use loops a lot.
ROBLOX doesn’t offer a recursive parameter for WaitForChild, so I usually have been doing ugly stuff like
repeat wait() until Obj:FindFirstChild(Name, true) or (Delay and tick() - Start > Delay)
return Obj:FindFirstChild(Name, true)
this is obviously very inefficient, not to mention that it uses loops instead of yielding the thread.
So, I’ve created this simple function to do this for you!
local function WaitForDescendant(Parent: Instance, Child: string, TimeOut: number?)
local AlreadyFound = Parent:FindFirstChild(Child, true)
if AlreadyFound then
return AlreadyFound
end
local Connection
local TimeOutThread
local Thread = coroutine.running()
Connection = Parent.DescendantAdded:Connect(function(Descendant)
if Descendant.Name == Child then
Connection:Disconnect()
if TimeOutThread and coroutine.status(TimeOutThread) == "suspended" then
task.cancel(TimeOutThread)
end
task.spawn(Thread, Descendant)
end
end)
if TimeOut then
TimeOutThread = task.delay(TimeOut, function()
if Connection.Connected then
Connection:Disconnect()
task.spawn(Thread)
end
end)
end
return coroutine.yield(Thread)
end
Never had issues with either of the problems you stated.
For stack traces, I’m unsure how the function could even error.
Either way, it yields the current thread; it doesn’t create a new one so the error traces should remain intact, no?
Recursive means it scans the descendants too. Like if there is a part deep inside a model, you’d have to call on it’s parent instance directly, and not an instance above it.
As @VortexColor mentioned, this is the equivalent of :FindFirstChild(Name, true) for WaitForChild. I also agree that it’s old code, and I’ve revised the function since to use typechecking along with the new task API, which also supports a (proper) timeout:
local function WaitForDescendant(Parent: Instance, Child: string, TimeOut: number?)
local AlreadyFound = Parent:FindFirstChild(Child, true)
if AlreadyFound then
return AlreadyFound
end
local Connection
local TimeOutThread
local Thread = coroutine.running()
Connection = Parent.DescendantAdded:Connect(function(Descendant)
if Descendant.Name == Child then
Connection:Disconnect()
if TimeOutThread and coroutine.status(TimeOutThread) == "suspended" then
task.cancel(TimeOutThread)
end
task.spawn(Thread, Descendant)
end
end)
if TimeOut then
TimeOutThread = task.delay(TimeOut, function()
if Connection.Connected then
Connection:Disconnect()
task.spawn(Thread)
end
end)
end
return coroutine.yield(Thread)
end
Decided to make an updated version of this, let me know what I can change.
local function WaitForDescendant = function(Parent: Instance, Child: string, TimeOut: number, Instance_Type) --//INSTANCE_TYPE can be left blank but is used for type checking
local Already_Initialized = Parent:FindFirstChild(Child, true)
if Already_Initialized then return Already_Initialized end --//Descendant was already initialized
--//
if not TimeOut then --//Timeout is used as a safety measure similar to :WaitForChild(Instance, (time)) where time = TimeOut
TimeOut = 60
end
--//
local InstanceAddedCheck = nil
local TimeOutThread = nil
local Thread = coroutine.running()
--//
InstanceAddedCheck = Parent.DescendantAdded:Connect(function(Descendant) --//Check for descendants being initialized/added to Parent
if Descendant.Name == Child then else return end
if Instance_Type ~= nil then
if Descendant:IsA(Instance_Type) then else return end
end
if InstanceAddedCheck ~= nil then
InstanceAddedCheck:Disconnect()
InstanceAddedCheck = nil
end
if TimeOutThread and coroutine.status(TimeOutThread) == "suspended" then
task.cancel(TimeOutThread)
end
task.spawn(Thread, Descendant)
end)
TimeOutThread = task.delay(TimeOut, function()
if InstanceAddedCheck ~= nil then else return end--or typeof(InstanceAddedCheck) == "RBXScriptConnection" and InstanceAddedCheck.Connected then else return end
InstanceAddedCheck:Disconnect() --//Disconnect + set to nil
InstanceAddedCheck = nil
task.spawn(Thread) --//Resume coroutine? But return nil as TimeOut has been reached
end)
return coroutine.yield(Thread) --//Returns result of coroutine (Descendant)
end
The script performs the same action, but to my knowledge simply using Disconnect() doesnt remove the connection from memory and setting to nil prevents memory leaks. The general consensus here is to set RBXScriptConnections to nil as the console still points to the variable being used even if the function is disconnected.
Once the function stops executing it automatically gets garbage collected, and thus goes out of memory. its the same reason as why you dont have to manually set all variables to nil after e.g. a for loop.
I just wanted to share since this changed my life.
@sleitnick has a powerful utility class for this kind of thing and it incorporates promises for better error handling and integration it’s called WaitFor. Check out his github page and all of the utilities he uses for Roblox development, and for more information about promises and waitfor.
Some of the things this module can do:
WaitFor.Child() – same as WaitForChild()
WaitFor.Children({child1, child2}) – can wait for multiple children in 1 line
WaitFor.Descendant() – This is the one that has “recursive-like” but is not really recursive.
WaitFor.Descendants() – same as number 3 but multiple descendants