Better alternative to WaitForChild

I see. If I could clarify, sometimes it would be better to just write it all out, instead of trying to create a function that looks efficient but could actually give you unexpected consequences?

Yes! Absolutely. Writing it out, in this context, is the more maintainable option.

3 Likes

You can store an array of all the sound names then load them into another or the same array:

local SoundService = game:GetService("SoundService")
local SoundNames = {"Sound1", "Sound2", "Sound3", "Sound4"}
local LoadedSounds = {}

for _, SoundName in ipairs(SoundNames) do
    table.insert(LoadedSounds, SoundService:WaitForChild(SoundName))
end

--then to access sound 1 you would do:

print(LoadedSounds[1])


Only do this if your grouping a bunch of sounds that will be used the same way but randomly, apart from that it will be much more readable if you assign the sound to meaningful variable names. Also I recommend you have one module that plays all the sounds in your game, that way if the client wants to disable sound in some setting you just update that module.

This is equivalent to local toWaitChild = game.WaitForChild, for the record. No reason to wrap it in a function when you’re keeping the parameter order.


To tidy this up, though, you might want to require a Module that creates/waits for the sounds so it doesn’t clutter the main script. Splitting stuff into sections goes a long way to making neater code.

3 Likes

These are some very interesting solutions. You need to compare performance over readability or repetition though. Additional functions, loops, scripts, etc. might decrease performance and be less future-proof to rbx engine updates than simply repeating the :WaitForChild() function.

In most of my projects I put sounds and such in folders, wait for the folder child, then use the children in my code. I haven’t had any issues with it thus far.

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Sounds = ReplicatedStorage:WaitForChild("SoundsFolder")

I think the most important takeaway from this is realizing that code is unique to the programmer and sometimes different methods make more sense to others. If it’s obscure enough, we write comments to help others understand our code.

3 Likes

Just use WaitForChild if the target instance is one hierarchical level from the parent.
If it is multiple levels below, please use this WaitForPath implementation @ChipioIndustries made for everyone:

1 Like

If you’d like a function to wait for many children, I made this and feel free to use it

function SharedUtils.WaitFor(par, list, timeout)
	local items = {}
	local start = tick()
	
	local function doList(par, list)
		for i,name in pairs(list) do
			if type(name) == "table" then
				local item = par:WaitForChild(i, math.max(.01, timeout - (tick() - start)))
				if not item then return nil end
				items[i] = item
				
				doList(item, name)
			elseif type(i) == "string" and type(name) == "string" then -- Rename
				local item = par:WaitForChild(i, math.max(.01, timeout - (tick() - start)))
				if not item then return nil end
				items[name] = item
			else
				local item = par:WaitForChild(name, math.max(.01, timeout - (tick() - start)))
				if not item then return nil end
				items[name] = item
			end
		end
		
		return items
	end
	
	
	return doList(par, list)
end

Example use:


local CharacterMembers = {
	"Humanoid",
	Head = {
		"face"
    },
    "HumanoidRootPart",
	"UpperTorso",
	LowerTorso={"Root"},
	"LeftUpperLeg",
	"RightUpperLeg",
	"LeftLowerLeg",
	"RightLowerLeg",
	"LeftUpperArm","LeftLowerArm","RightUpperArm","RightLowerArm","LeftHand","RightHand","LeftFoot","RightFoot"
}

local CharacterChildren = SharedUtils.WaitFor(character,CharacterMembers,5)
local face = CharacterChildren.face -- works
local leftlowerleg = CharacterChildren.LeftLowerLeg -- also works

However, keep in mind that you don’t need to use WaitForChild if:

  • A: You’re simply accessing something from ReplicatedStorage which exists at the start of the game(isn’t instanced by the server) - you only need to wait for things that are created by the server after the game starts, or things in the workspace

  • B: You can access client sided things by cloning them from ReplicatedStorage instead

What I mean by point B is, for the player gui for instance, I usually have a custom “StarterGui” folder in ReplicatedStorage. The client just clones assets from there into their playergui directly, so they don’t have to wait on anything being instanced from the server

6 Likes

I wrote this module just now because Roblox is having loading issues client-side and it broke my game because I hate having to write WaitForChild. Roblox should implement a safe-get that doesn’t require tons of typing.

function(parent, path)
	for name in string.gmatch(path, "[^%.]+") do
		parent = parent:WaitForChild(name)
	end
	return parent
end

In another script, require and call it like module(game, "Workspace.Model.Part"). Quick and easy. It’s basically WaitForPath but a three-liner. Probably better to use WaitForPath, or if you care about performance, build your own barebones implementation based on the code.

Edit: I just upgraded to support multiple paths in one go if you pass an array of strings as an optional third parameter.

return function(parent, path, subpaths)
	local segments = string.split(path, ".")
	for i = 1, #segments do
		parent = parent:WaitForChild(segments[i])
	end
	if subpaths then
		local results = {}
		for i = 1, #subpaths do
			local _segments = string.split(subpaths[i], ".")
			local _parent = parent
			for i = 1, #_segments do
				_parent = _parent:WaitForChild(_segments[i])
			end
			table.insert(results, _parent)
		end
		return table.unpack(results)
	else
		return parent
	end
end

Then you can use like map, cutscenes, maps = module(game, "ServerStorage", {"Chat", "Cutscenes", "Maps"})

1 Like

@Crazyman32 Has a video talking about this:

6 Likes

I’m happy to say I don’t use WaitForChild (or anything like it) in my entire game. My game has one Script and one LocalScript so I have complete control over what gets created and when code runs. All of my gui’s and characters are created using scripts too, which certainly helps.

3 Likes

Wow, this blew up. Thank you for all these possible solutions!

For now, I’ll just use @TheCarbyneUniverse and @beecivil 's solutions of putting every sound into a folder, because I won’t have too much to load anyway. As for the Gui, I’ll keep all the WaitForChild functions, and maybe consider one of the other solutions

2 Likes

Does waiting for an instance also guarantee that every descendant has loaded, not just children?

1 Like

In my experience, sometimes yes but I think that was just luck. Most times no.

Really, you can utilize DataModel:IsLoaded and the DataModel.Loaded event to Wait at the top of a file to sanely check if everything has replicated. Even this has its own holes though.

For most, I’d recommend using an IDE like Visual Studio Code, tooling like Rojo, and just using powerful tools to make WaitForChild chains less painful.

1 Like

Be warned, single scripts is a huge issue with refactoring and modularity. I’d advise you revisit your design paradigm if this truly is the case.

The script is no more than a few lines that starts up the framework. It’s all ModuleScripts. Using one script ensures that things are completely initialized before code runs, hence no WaitForChild needed.

1 Like

Got it! I just instantly saw this and instantly was thrown into a second-hand panic, lol.

1 Like

Does that mean you serialize literally everything you need a reference to just to avoid WaitForChild?

I assume @Tomarty means that all of their game’s logic is written in ModuleScript instances whose execution is deferred once all required objects are initialized and replicated, managed by one Script instance on the server and one LocalScript instance on the client.

If this is the situation, WaitForChild being used in its traditional case of ensuring an instance has replicated before interacting with its reference can be phased out; with a perfect implementation, replication at that point is a guarantee given the scenrio.

1 Like

This is accurate. When a game has multiple Scripts / LocalScripts, it is not necessarily known what order they will be run in. If, for example, you wanted to use _G for quick access to immutable utility functions, and need to ensure the local player exists, you might use code like this:

while not _G.Utility do
    wait()
end
local Players = game:GetService("Players")
while not Players.LocalPlayer do
    Players:GetPropertyChangedSignal("LocalPlayer"):Wait()
end

While usage of _G is debatable, wait-for-everything design becomes difficult to manage in large projects, because scripts need to snap into place over time, yielding back and forth; With ModuleScripts, you can simply run code when what it needs is known to be ready, or even create instances proactively when they are needed (instead of waiting for them.)

This is more difficult to apply within the context of classic Roblox characters / guis. We are all used to creating tools that have individual scripts that are destroyed when no longer needed, waiting for objects in default characters, and never knowing what gui instances will be replicated, but this is all holding us back from creating huge stable experiences:

  • Multiple server scripts for every player does not usually scale well with large servers.
  • Scripts may error if they yield while setting up an player / character / object and the corresponding player leaves.
  • If a tool creates an instance and adds it to the workspace with the intent to destroy it later, it will leak that instance if the tool is destroyed before it cleans it up.

For anyone making a large game, I highly recommend using a ‘Maid’ class to track connections, and consider making a custom scheduler that can be disconnected from instead of using wait. It’s such a headache working with tons of Scripts / LocalScripts that run their own threads that are created / destroyed on a whim.

2 Likes

if you’re defining self, you might as well just do

local toWaitChild = game.WaitForChild;
toWaitChild(workspac, 'Name'); 

no?

1 Like