Can you write this from memory?
Write the first line of a GDScript that extends Node2D for your Arena Survivor player.
Every GDScript file starts the same way: extends a node type, declare variables, reference children with @onready, define _ready. Once you nail the foundations, you can move on to movement, signals, or exports tuning.
These exercises lock in the script skeleton so you can write game logic instead of fighting boilerplate.
What does extends do? It declares which Godot class your script inherits from. Without it, your script extends RefCounted (not Node!), meaning no _ready(), no scene tree, no $Child.
When does @onready run? Just before _ready(). It waits until the node and all children are in the scene tree, so $Child references work.
_init vs _ready? _init runs when the object is created in memory (no scene tree). _ready runs once the node and its children have entered the tree. Rule of thumb: _init is for data, _ready is for nodes.
Think of it this way: A script is a class. A node is an instance. A scene is a packed tree of nodes.
When you write extends CharacterBody2D, you're creating a class that inherits from Godot's CharacterBody2D. When you drag that script onto a node in the editor, that node becomes an instance of your class.
PackedScene (.tscn file)
│
▼ instantiate()
Node Tree (runtime)
│
▼ add_child()
Live in the game
The Script Skeleton
Every gameplay script follows this pattern:
extends CharacterBody2D # What class we inherit from
signal health_changed(new_value: int) # Custom events
@export var speed: float = 200.0 # Inspector-editable
@onready var sprite: Sprite2D = $Sprite2D # Child reference
func _ready() -> void:
# Runs once when node enters tree
pass
func _physics_process(delta: float) -> void:
# Runs every physics frame (60 Hz)
pass
Lifecycle Timeline (the part people mess up)
Rule of thumb: _init is for data. _ready is for nodes.
- _init() — Runs when the object is created in memory. Scene tree not available yet.
- _enter_tree() — Node enters the tree. Still don't assume children are ready.
- @onready variables resolve just before _ready().
- _ready() — Runs once the node and all its children are in the tree.
If you need $ChildNode, it must be @onready or inside _ready().
func _init() -> void:
# Object exists, but NOT in tree yet
# $Child will be null here!
max_hp = 100 # Data-only initialization is OK
func _ready() -> void:
# Node + children are in the tree
sprite = $Sprite2D # This works now
hp_bar.max_value = max_hp
Why extends Matters
If you omit extends, your script defaults to extending RefCounted—not Node. This means:
- No
_ready(),_process(), or other lifecycle methods - No
$Childreferences - No scene tree access
Always write extends explicitly, even for simple scripts.
@onready: The Safe Way to Reference Children
# WRONG: Child doesn't exist yet at _init time
var sprite = $Sprite2D # Returns null!
# RIGHT: Defers until just before _ready
@onready var sprite: Sprite2D = $Sprite2D
The @onready annotation waits until the node and all its children are in the scene tree before evaluating the expression. This is when $Child paths actually work.
Type Everything
Godot 4 GDScript supports gradual typing. Use it:
# Weak: No type info, no autocomplete
var speed = 200
# Strong: Type hints, editor support, error checking
var speed: float = 200.0
var player: CharacterBody2D
var items: Array[String] = []
# Inferred typing with := (type from right-hand side)
var direction := Vector2.UP # Inferred as Vector2
var count := 0 # Inferred as int
Types help the editor catch mistakes before you run the game.
Constants
Use const for values that never change:
const MAX_SPEED: float = 400.0
const GRAVITY: float = 980.0
const DIRECTIONS := [Vector2.UP, Vector2.DOWN, Vector2.LEFT, Vector2.RIGHT]
Constants must be assigned at declaration and cannot be modified. Use them for configuration values, magic numbers, and preloaded resources.
Node References
$Child is shorthand for get_node("Child"). It breaks when you rename or move nodes.
@onready var sprite: Sprite2D = $Sprite2D # Relative path
@onready var player: Node2D = $"../Player" # Parent's sibling
For refactor-safe references, Godot 4 introduced Unique Names (%NodeName). See scene instancing for the full pattern.
Common Errors and Fixes
| Error Message | What It Means | Fix |
|---|---|---|
| Script inherits from native type 'RefCounted'... | You forgot extends Node2D (or similar) | Add extends at the top of your script |
| Invalid get index 'Sprite2D' (on base: 'Nil') | $Sprite2D is null—accessed too early | Use @onready or move to _ready() |
| Node not found: "NodeName" | The node path is wrong or doesn't exist | Check spelling, path structure, and unique name setup |
| Identifier "x" not declared in current scope | Variable referenced before declaration | Declare with var x or check for typos |
| Cannot call method 'X' on null instance | The object was freed or never initialized | Add null checks or verify node exists |
Next Steps
Once you have the foundations locked in:
- Movement — Input handling, velocity, move_and_slide
- Exports & Tuning — @export_range, enums, Inspector organization
- Timers & Signals — Event-driven architecture, await, cooldowns
- Arrays & Loops — Collections, iteration, dictionary patterns
When to Use GDScript Foundations
- Starting any new GDScript file—extends, var, and _ready are the skeleton of every script
- Defining node behavior with exported properties and signal connections in _ready
- Setting up @onready references to child nodes to avoid repeated get_node calls
Check Your Understanding: GDScript Foundations
Check Your Understanding: What is the difference between _ready and _init in GDScript, and when does @onready resolve?
_init is called when the object is created in memory, before it enters the scene tree. _ready is called once the node and all its children have entered the tree. @onready variables resolve just before _ready runs, making them safe for referencing child nodes. Use _init for data-only initialization and _ready for anything that depends on the scene tree.
What You'll Practice: GDScript Foundations
Common GDScript Foundations Pitfalls
- Accessing child nodes in _init instead of _ready—the scene tree is not built yet, so $Child returns null
- Forgetting that @export vars set in the Inspector override values assigned in code—this causes confusion when defaults seem to be ignored
- Omitting extends and wondering why _ready never runs (script defaults to RefCounted, not Node)
- Using var when you meant const—if a value should never change, declare it const so the editor catches accidental reassignment
GDScript Foundations FAQ
What does extends do in GDScript?
extends declares which Godot class or script your node inherits from. For example, extends CharacterBody2D gives your script access to all CharacterBody2D methods like move_and_slide(). Every GDScript file must start with extends or it defaults to extending RefCounted (not Node!).
What is the difference between _init and _ready in Godot 4?
_init() runs when the object is created in memory—the scene tree is not available yet, so $Child returns null. _ready() runs after the node and all its children have entered the tree. Use _init for data-only initialization (setting default values). Use _ready for anything that touches the scene tree (child references, signal connections).
How does @onready work in Godot 4 GDScript?
@onready is an annotation that defers variable assignment until just before _ready() runs. This guarantees child nodes exist in the scene tree. For example: @onready var sprite = $Sprite2D. Without @onready, the assignment happens at _init time when child nodes are not yet available.
What is the signal keyword in GDScript?
The signal keyword declares a custom signal on a node. In Godot 4 the syntax is: signal health_changed(new_value: int). Other nodes connect to it via health_changed.connect(callable). Signals decouple nodes so they communicate without direct references. See timers & signals for full patterns.
What does class_name actually do?
class_name registers your script as a global type. Other scripts can reference it by name without preload(), and it appears in the "Add Node" dialog. Use it for scripts you'll reference frequently or want to instantiate by type.
What's the difference between @export and setting a variable in code?
@export exposes the variable to the Inspector, letting you set different values per scene instance. The Inspector value overrides the code default. Use @export for values you want to tweak without editing code. See exports tuning for advanced patterns.
When should I use Node vs Node2D?
Use Node for non-visual logic (game managers, state machines). Use Node2D when you need position, rotation, and scale in 2D space. Node2D is the base for all 2D game objects.
What is the difference between $NodeName and %NodeName?
$NodeName is shorthand for get_node() with a relative path—it breaks if you move nodes. %NodeName uses Godot 4 Unique Names for refactor-safe references. See the scene instancing guide for the full pattern and gotchas.
GDScript Foundations Syntax Quick Reference
extends CharacterBody2D
signal health_changed(new_value: int)
@export var speed: float = 200.0
@onready var sprite: Sprite2D = $Sprite2D
func _ready() -> void:
print("Node ready: ", name)extends Node
const MAX_HP: int = 100
var current_hp: int = MAX_HP
var player_name: String = "Hero"
var items: Array[String] = []
func take_damage(amount: int) -> void:
current_hp = max(current_hp - amount, 0)# No extends = extends RefCounted
# This is a data class, not a Node
class_name PlayerStats
var max_hp: int = 100
var attack: int = 10
var defense: int = 5GDScript Foundations Sample Exercises
Write the first line of a GDScript that extends CharacterBody2D for a player that uses physics movement.
extends CharacterBody2DDeclare a variable max_enemies with type int and value 10 for your arena's spawn cap.
var max_enemies: int = 10Declare a variable move_speed with type float and value 150.0 for your player's base speed.
var move_speed: float = 150.0+ 29 more exercises