Understanding Godot’s fundamental building blocks - nodes and scenes
Nodes are Godot’s fundamental building blocks. Everything in your game is built from nodes organized into scene hierarchies. Understanding how nodes and scenes work is essential to mastering Godot.
A Node is the base class for all scene objects in Godot. Nodes can be assigned as children of other nodes, creating a tree structure. Each node can contain any number of child nodes, with the requirement that all siblings (direct children of a node) have unique names.
Nodes inherit from the Object class, giving them access to signals, properties, and methods that form the foundation of Godot’s object system.
Nodes are organized in a parent-child relationship:
# Creating a simple node hierarchyvar parent = Node2D.new()var child = Sprite2D.new()var grandchild = Area2D.new()# Build the hierarchychild.add_child(grandchild)parent.add_child(child)# Access nodes in the hierarchyvar my_child = parent.get_child(0) # Returns childvar child_count = parent.get_child_count() # Returns 1
Every child must have a unique name among its siblings. If you add a node with a duplicate name, Godot will automatically rename it by appending a number.
A scene is a tree of nodes saved together. Scenes can be saved to disk and then instantiated into other scenes, allowing for highly flexible and modular game architecture.
# Create a player scene programmaticallyvar player = CharacterBody2D.new()var sprite = Sprite2D.new()var collision = CollisionShape2D.new()# Build the scene hierarchyplayer.add_child(sprite)player.add_child(collision)# Set ownership for saving (important!)sprite.owner = playercollision.owner = player# Pack and save the scenevar scene = PackedScene.new()var result = scene.pack(player)if result == OK: ResourceSaver.save(scene, "res://player.tscn")
The owner property determines which nodes get saved when packing a scene. Only nodes with their owner set will be included in the saved scene file.
One of the most powerful features of scenes is the ability to instance them multiple times:
func _ready(): # Load a scene var enemy_scene = preload("res://enemy.tscn") # Create multiple instances for i in range(5): var enemy = enemy_scene.instantiate() enemy.position = Vector2(i * 100, 0) add_child(enemy)
Nodes go through a specific lifecycle when added to or removed from the scene tree.
1
Entering the Tree
When a node is added to the scene tree:
The parent’s _enter_tree() is called first
Then children’s _enter_tree() methods are called in order
The node receives NOTIFICATION_ENTER_TREE
func _enter_tree(): print("Node entering the scene tree") # The node is now part of the tree but not fully initialized
2
Ready Notification
After all nodes have entered the tree:
Children’s _ready() methods are called first (bottom-up)
Then the parent’s _ready() is called
The node receives NOTIFICATION_READY
func _ready(): print("Node is ready and fully initialized") # All children are guaranteed to be ready # This is the best place for initialization
3
Exiting the Tree
When a node is removed:
The parent’s _exit_tree() is called last
Children’s _exit_tree() methods are called first
The node receives NOTIFICATION_EXIT_TREE
func _exit_tree(): print("Node leaving the scene tree") # Clean up resources here
The _ready() method is called only once for each node. If you remove a node and add it back to the tree, _ready() will not be called again unless you use request_ready().
The _process() callback runs on every frame and is ideal for game logic, animations, and visual updates:
func _process(delta): # delta is the time elapsed since last frame (in seconds) # Rotate the node smoothly rotation += 1.0 * delta # Move based on input var velocity = Vector2.ZERO if Input.is_action_pressed("ui_right"): velocity.x += 1 if Input.is_action_pressed("ui_left"): velocity.x -= 1 position += velocity * 200 * delta
The delta parameter represents elapsed time in seconds. Multiply movement and animations by delta to make them framerate-independent.
The _physics_process() callback runs at a fixed rate (60 Hz by default) and should be used for physics-related code:
func _physics_process(delta): # Called at a fixed rate (default: 60 times per second) # Perfect for physics calculations # Apply physics-based movement var velocity = Vector2(100, 0) position += velocity * delta # Check collisions if is_on_floor(): apply_jump()
By default, _physics_process() is called 60 times per second, regardless of the visual framerate. This ensures consistent physics behavior.
# Add a child nodevar child = Node2D.new()add_child(child)# Add a child at a specific position in the sibling listvar sibling = Node2D.new()child.add_sibling(sibling)# Remove a childremove_child(child)# Free a node (deletes it and all children)child.queue_free() # Safe - waits until end of frame# child.free() # Immediate - use with caution
When you free a node with free() or queue_free(), all its children are automatically freed as well. Use queue_free() instead of free() to avoid errors when freeing nodes during processing.
# Get a child by indexvar first_child = get_child(0)var last_child = get_child(-1)# Get a node by pathvar player = get_node("Player")var weapon = get_node("Player/Weapon")var root = get_node("/root") # Absolute path# Using the $ shorthand (only works with literal strings)var sprite = $Sprite2Dvar health_bar = $UI/HealthBar# Find child by patternvar enemy = find_child("Enemy*", true, false) # Recursive search# Find all children of a specific typevar all_sprites = find_children("*", "Sprite2D", true, false)
# Access parentvar parent = get_parent()# Check if node is ancestorif player.is_ancestor_of(weapon): print("Player is an ancestor of weapon")# Get all childrenvar children = get_children()for child in children: print(child.name)# Get sibling indexvar index = get_index()print("This node is child number ", index)
Here’s a complete example of creating a player character scene:
extends CharacterBody2Dconst SPEED = 200.0const JUMP_VELOCITY = -400.0# References to child nodes@onready var sprite = $Sprite2D@onready var animation = $AnimationPlayer@onready var collision = $CollisionShape2Dfunc _ready(): # Initialize when all children are ready print("Player ready with ", get_child_count(), " children")func _physics_process(delta): # Add gravity if not is_on_floor(): velocity.y += get_gravity().y * delta # Handle jump if Input.is_action_just_pressed("jump") and is_on_floor(): velocity.y = JUMP_VELOCITY # Handle movement var direction = Input.get_axis("move_left", "move_right") if direction: velocity.x = direction * SPEED else: velocity.x = move_toward(velocity.x, 0, SPEED) move_and_slide()func _process(delta): # Update sprite based on movement if velocity.x != 0: sprite.flip_h = velocity.x < 0
Understanding @onready
The @onready annotation defers variable initialization until _ready() is called. This ensures that child nodes are available when you try to access them:
# This will fail - children aren't ready yetvar sprite = $Sprite2D# This works - children are ready when _ready() is called@onready var sprite = $Sprite2D