Avoiding using WaitForChild a lot in GUI scripts

There is an issue many have been facing where referencing GUI items from scripts in the same PlayerGui container immediately after the script starts running throws an error unless a WaitForChild command is used like this:

ObjectName = script.Parent:WaitForChild("ObjectName")

For a while, I’ve used this solution myself to get the scripts to stop throwing errors trying to reference things like ScreenGuis and other scripts, however, this is annoying for three reasons. One is that IntelliSense doesn’t work with things referenced by WaitForChild, making it harder to develop. Another is that it won’t ever throw an error to alert the scripter that a mistake is made. And finally, one has to write a LOT of WaitForChild phrases.

Eventually, I found a cheap way to solve them all.

Hypothesis

I figured that the game engine clones every child inside of the StarterGui service one at a time to the Player’s PlayerGui when the player’s character spawns, however, as it adds each LocalScript clone, the game engine starts the script before letting the cloning process continue.

Test

This behavior can be proven by running a test with three scripts and a ScreenGui, as shown in the picture.

image

Every script has this code to print out what it sees when it starts.

print("--- What " .. script.Name .. " Sees in Player Gui ---")
for i, item in ipairs(script.Parent:GetChildren()) do
	print(item.Name)
end

There are no yielding functions, so the engine will have no chance of continuing cloning objects as said before.

Starting this up, this is what I get:

image

As you can see, some scripts didn’t see some other scripts and none of them saw the ScreenGui when they started.

Solution

Assuming that the whole game loads with StreamingEnabled off before cloning the GUI, that PlayerGui cloning behaves the way mentioned before, and that it clones each item before adding rather than cloning every descendant of every item individually, I figured that the solution is turning every script into a ModuleScript and making a lazy waiter script to start them all after everything is cloned, using the StarterGui as a reference of what’s supposed to be in there.

Another benefit of using ModuleScripts is you don’t have to use BindableFunctions nor BindableEvents as much to communicate between GUI scripts, as you can cram native Lua functions in a table returned by ModuleScripts.

I converted all the LocalScripts into ModuleScripts, and then added a LocalScript loader to test this.

-- Wait until everything is loaded before starting the scripts.
for i, item in ipairs(game.StarterGui:GetChildren()) do
	script.Parent:WaitForChild(item.Name)
end

-- Run a "require" against every ModuleScript in here to start them up. 
for i, item in ipairs(script.Parent:GetChildren()) do
	if item:IsA("ModuleScript") then
		require(item)
	end
end

Now the setup looks like this:

image

And the test scripts look like this because you have to return something from a ModuleScript:

ScreenGui = script.Parent.ScreenGui -- Just testing referencing.

print("--- What " .. script.Name .. " Sees in Player Gui ---")
for i, item in ipairs(script.Parent:GetChildren()) do
	print(item.Name)
end

return nil

Even after deleting and adding another ScreenGui, this is what I get when I start it:

image

None of them crashed, and they see everything needed!

Conclusion

So to sum it up, all you need to do is turn all your GUI scripts into ModuleScripts, make a single LocalScript that waits until everything they need load before starting them up, and enjoy the development ease. :slight_smile:

A few side notes

You don’t need to worry about waiting for workspace and ReplicatedStorage objects unless you have StreamingEnabled on or they are created after the game server starts.

Developing ForbiddenJ’s Quarry with this solution proves that each object is cloned whole, as I don’t get any reference errors accessing the children of the ScreenGuis after waiting for the objects themselves, however, trying to prove it here would make the post even more complicated.

If a ModuleScript crashes or yields indenfinitely in the middle of starting, the whole starter stops, preventing more scripts from starting. Though I would do a pcall or ypcall if I knew how to make errors show up in the console as errors anyway.

I would assume the same behavior applies to the PlayerScripts container, however, unlike player scripts, GUI things only load when the player’s character spawns. Also, the GUI elements are destroyed and re-cloned when a player respawns unless you change some properties, which may mess up your references when working from PlayerScripts.

Keep those in mind.

87 Likes

Pretty cool tactic. Never would have thought to use modules like that. Might try it out myself!

4 Likes

This is the most alpha-male thing I’ve seen all day.

You are really smart, and this experiment was really amazing- you saw a obstacle and you found a solution to get around it, really nice work!

I’ll be sticking to my WaitForChild’s though… habits.

10 Likes

I have a question. When do you need to use WaitForChild() ?

1 Like

Several users have explained this before. I believe Sharksie and Anaminus have in particular. As I was unable to find their explanations after searching, I’ll explain in brief:

  • There are, to steal the phrases from Anaminus, “statically” loaded instances, and “dynamically” loaded instances. Static instances are everything in your Studio’s game explorer. Dynamic instances are those which are created by script at runtime.
  • Code from game.ReplicatedFirst can run before the game is loaded, meaning that static instances cannot be guaranteed to exist. Use Instance:WaitForChild() here always, even for static instances.
  • All server-side code, and all client-side code besides that in game.ReplicatedFirst will not run before all static instances are loaded.
  • Use Instance:WaitForChild() on dynamic instances whenever it is possible the code will run before the instance is created.

If you’re using Instance:WaitForChild() there is almost always a better way of doing things. The exception to this rule is that you need to wait for dynamically instantiated player containers, like Player.Backpack, Player.PlayerGui, and so on.

If you are using game.StarterGui, since GUIs are only cloned and parented to Player.PlayerGui if and when the player’s character spawns, you also need to wait for the ScreenGui to be a child of Player.PlayerGui.

Cloning is a synchronous operation, meaning that it does not yield, so once the root of an instance is loaded, so will all of its static descendants. So you don’t, for example, need to wait for any static descendants of a ScreenGui cloned from game.StarterGui, only the root ScreenGui.

Personally I don’t use game.StarterGui at all, it just wastes visual space and I don’t like that GUIs are tied to character spawning. I just clone GUIs in from a folder, and avoid the waiting entirely, except on Player.PlayerGui.

30 Likes

I like this post a lot. I see many people that abuse :WaitForChild() way too often, and even call it on the same object more than once. Will definitely share your post in the future

11 Likes

Bump, this is an issue for me not only for GUI but also replicated storage, what I’ve come up with for GUI is just doing the UI purely using scripts which I’ve found tends to load faster.