Managing game state and node hierarchies with the SceneTree
The SceneTree is one of Godot’s most important classes. It manages the hierarchy of nodes in your game, controls the game loop, handles scene switching, and provides powerful tools for organizing and querying nodes through groups.
The SceneTree manages the active tree of nodes in your game. It’s automatically created when Godot starts and serves as the default MainLoop implementation, making it responsible for:
func _ready(): # Get the scene tree from any node var tree = get_tree() # Check if node is in the tree if is_inside_tree(): print("Node is in the scene tree") # Access the root node (main Window) var root = get_tree().root print("Root node: ", root.name)
Calling get_tree() on a node that isn’t in the scene tree will generate an error and return null. Always check with is_inside_tree() first if you’re unsure.
The root node is the topmost node in the scene tree, always of type Window:
func _ready(): var root = get_tree().root # The root contains your main scene and autoload nodes print("Root has ", root.get_child_count(), " children") # Access the current scene var current = get_tree().current_scene print("Current scene: ", current.name)
func _on_start_button_pressed(): # Load and change to a new scene var error = get_tree().change_scene_to_file("res://levels/level_1.tscn") if error != OK: print("Failed to load scene")
2
Change Scene from PackedScene
Use a preloaded scene for faster switching:
var next_level = preload("res://levels/level_2.tscn")func advance_level(): get_tree().change_scene_to_packed(next_level)
3
Change Scene to Node Instance
Switch to an already instantiated scene:
func load_custom_scene(): # Create a scene programmatically var new_scene = Node2D.new() new_scene.name = "CustomScene" # Add some children var sprite = Sprite2D.new() new_scene.add_child(sprite) # Switch to this scene get_tree().change_scene_to_node(new_scene)
When changing scenes, the current scene is removed from the tree immediately, then freed at the end of the frame. The new scene is added after the current frame completes.
Understanding the order of operations during scene changes is crucial:
func change_to_next_level(): print("1. About to change scene") print("Current scene: ", get_tree().current_scene) get_tree().change_scene_to_file("res://level_2.tscn") print("2. After change call") print("Current scene: ", get_tree().current_scene) # null! # Wait for the scene to actually change await get_tree().scene_changed print("3. Scene changed") print("Current scene: ", get_tree().current_scene) # New scene!
func restart_level(): # Reload the current scene get_tree().reload_current_scene()func game_over(): # Wait before reloading await get_tree().create_timer(3.0).timeout get_tree().reload_current_scene()
func return_to_menu(): # Remove current scene without loading a new one get_tree().unload_current_scene() # Now manually load what you want var menu = preload("res://menu.tscn").instantiate() get_tree().root.add_child(menu) get_tree().current_scene = menu
The SceneTree controls whether the game is paused:
func _ready(): # Pause the game get_tree().paused = true # Unpause the game get_tree().paused = falsefunc toggle_pause(): get_tree().paused = !get_tree().paused
When the game is paused:
Physics processing stops
_process() and _physics_process() may not be called (depends on process_mode)
Nodes can control how they behave when the game is paused:
func _ready(): # This node processes only when NOT paused (default) process_mode = Node.PROCESS_MODE_PAUSABLE # This node processes only WHEN paused (e.g., pause menu) process_mode = Node.PROCESS_MODE_WHEN_PAUSED # This node ALWAYS processes process_mode = Node.PROCESS_MODE_ALWAYS # This node NEVER processes process_mode = Node.PROCESS_MODE_DISABLED
Set your pause menu’s process_mode to PROCESS_MODE_WHEN_PAUSED so it can respond to input while the game is paused.
func _ready(): # Add this node to a group add_to_group("enemies") add_to_group("damageable") # Add to persistent group (saved with scene) add_to_group("save_data", true)func _exit_tree(): # Remove from group (usually not needed - automatic on free) remove_from_group("enemies")
Nodes are automatically removed from all groups when freed. You rarely need to manually remove nodes from groups.
func _ready(): var tree = get_tree() # Check if group exists if tree.has_group("enemies"): print("Enemies exist in the scene") # Get all nodes in a group var enemies = tree.get_nodes_in_group("enemies") print("There are ", enemies.size(), " enemies") # Get first node in group var first_enemy = tree.get_first_node_in_group("enemies") if first_enemy: print("First enemy: ", first_enemy.name) # Count nodes in group var enemy_count = tree.get_node_count_in_group("enemies")
One of the most powerful features is calling methods on all nodes in a group:
# Call a method on all nodes in a groupfunc defeat_all_enemies(): get_tree().call_group("enemies", "take_damage", 9999)# Set a property on all nodes in a group func freeze_all_enemies(): get_tree().set_group("enemies", "frozen", true)# Send notification to all nodes in a groupfunc alert_all_enemies(): get_tree().notify_group("enemies", NOTIFICATION_ALERT)
By default, call_group() acts immediately on all nodes, which may cause performance issues with large groups. Use call_group_flags() with GROUP_CALL_DEFERRED for better performance.
func _ready(): var tree = get_tree() # Call at end of frame (deferred) tree.call_group_flags( SceneTree.GROUP_CALL_DEFERRED, "enemies", "take_damage", 10 ) # Call in reverse order (children before parents) tree.call_group_flags( SceneTree.GROUP_CALL_REVERSE, "ui_elements", "update_display" ) # Call only once, even if called multiple times in same frame tree.call_group_flags( SceneTree.GROUP_CALL_DEFERRED | SceneTree.GROUP_CALL_UNIQUE, "particles", "emit" )
Node paths are used to reference nodes within the scene tree:
func _ready(): # Get node by path var player = get_node("/root/Main/Player") # Relative paths var weapon = get_node("Player/Weapon") var sibling = get_node("../OtherNode") # Using NodePath type var path: NodePath = ^"/root/Main/Player" var node = get_node(path) # Get path to a node var my_path = get_path() print("This node's path: ", my_path) # Get relative path between nodes var relative = get_path_to(player) print("Relative path to player: ", relative)
Absolute paths start with /root/ and work from the root node. Relative paths work from the current node.
func delayed_action(): # Create a timer var timer = get_tree().create_timer(3.0) # Wait for it to complete await timer.timeout print("3 seconds have passed")func respawn_player(): print("Player died") # Wait 5 seconds that ignore time scale await get_tree().create_timer(5.0, true, false, true).timeout print("Respawning...") player.respawn()
func _ready(): var tree = get_tree() # When any node enters the tree tree.node_added.connect(_on_node_added) # When any node leaves the tree tree.node_removed.connect(_on_node_removed) # When a node is renamed tree.node_renamed.connect(_on_node_renamed) # When scene changes tree.scene_changed.connect(_on_scene_changed) # When tree hierarchy changes tree.tree_changed.connect(_on_tree_changed) # Before process frame tree.process_frame.connect(_on_process_frame) # Before physics frame tree.physics_frame.connect(_on_physics_frame)func _on_node_added(node: Node): print("Node added: ", node.name)func _on_scene_changed(): print("Scene changed to: ", get_tree().current_scene.name)