Imagine you hit "Play" in the engine to test your latest scene.
Behind the scenes, you have two managers working hard. First, there is the RoomManager, responsible for loading all the cozy furniture into the scene—the easel, the rugs, and most importantly, the artist's desk.
Then, you have the CatManager, responsible for spawning your cat, Pixel. As soon as the game starts, CatManager wants to give Pixel his very first task of the day: jumping up onto the artist's desk to knock over a cup of pens.
In a perfect world, the logic flows like this:
But game engines are incredibly literal, and they don't wait for anyone unless you force them to. Both managers wake up at the exact same time and start sprinting through their _ready() functions. This is what we call a race condition.
Because of a tiny, unpredictable hiccup in the processor, CatManager finishes its setup one millisecond before RoomManager has a chance to place the furniture. CatManager happily fires its command: "Okay Pixel, jump on the desk!"
Here is what that free-for-all looks like in the code:
//RoomManager.gd (Autoload)
//GD Script example
extends Node
var desk_position = null
func _ready():
# Pretend this takes a tiny, unpredictable amount of time
# to instance the desk scene and place it in the room.
setup_room()
func setup_room():
desk_position = Vector2(150, 200)//CatManager.gd (Autoload)
//GD Script example
extends Node
func _ready():
# THE RACE CONDITION:
# If CatManager wins the race and runs first, desk_position is still null!
var target_pos = RoomManager.desk_position
# Pixel tries to jump to a null position. The game crashes.
tell_pixel_to_jump(target_pos)Pixel looks for the desk. But to the game engine, the desk doesn't exist yet. It's a null instance.
Best case scenario? Pixel spawns in mid-air, the physics engine takes over, and he plummets through the floor into the infinite, dark void beneath your cozy room. Worst case scenario? The entire game instantly freezes and crashes to the desktop with a giant red Null Reference Exception error.
Neither manager did anything wrong. They both executed their code perfectly. But because the timing was chaotic, the game broke.
To fix this, we have to stop making assumptions. We need to tell the CatManager to sit tight and do absolutely nothing until the RoomManager explicitly announces that the room is fully loaded and safe to use.
In Godot, we do this using a signal and the await keyword.
//RoomManager.gd (Autoload)
//GD Script example
extends Node
# custom signal to broadcast our status
signal room_is_ready
var desk_position = null
func _ready():
setup_room()
func setup_room():
desk_position = Vector2(150, 200)
# The desk is placed! fire the signal to let everyone know.
room_is_ready.emit()//CatManager.gd (Autoload)
//GD Script example
extends Node
func _ready():
# We pause this script right here. It will not move to the next
# line until it hears the 'room_is_ready' signal from RoomManager.
await RoomManager.room_is_ready
# Crisis averted. We are now 100% guaranteed the desk exists.
var target_pos = RoomManager.desk_position
tell_pixel_to_jump(target_pos)This tiny change transforms a game-breaking race into a clean, predictable line of events. By relying on an event-driven approach, your managers stay decoupled, your initialization order is guaranteed, and most importantly, your cat will never fall through the floor again.