GDScript, lifecycle, and signals
How Godot calls your code — _ready, _process, _physics_process, signals, await.
A script on a node
A GDScript file (.gd) usually starts with extends, specifying which node it inherits from:
extends Node3D
# Optional: a global class name (visible in the editor and other scripts)
class_name Enemy
# Export to the Inspector — an analog of [SerializeField]
@export var max_hp: int = 100
@export_range(1.0, 10.0) var speed: float = 3.0
@export var target: Node3D # a reference to another node
# @onready — assigned in _ready, handy for references to children
@onready var sprite: MeshInstance3D = $Sprite
@onready var anim: AnimationPlayer = $AnimationPlayer
var hp: int
func _ready() -> void:
hp = max_hp
print("Enemy ready: %s" % name)
func _process(delta: float) -> void:
# every frame
rotation_degrees.y += 30.0 * delta
A React component: useState for fields, useEffect(() => {}, []) for initialization, the render function
each time. props are the @export fields.
[SerializeField] private float speed ↔ @export var speed. private Animator anim +
anim = GetComponent<Animator>() in Awake ↔ a one-line @onready var anim: AnimationPlayer = $AnimationPlayer.
More concise.
The node lifecycle
The main methods Godot calls itself:
_init()— the object’s constructor, even before the node is added to the tree._enter_tree()— the node was added to the scene tree. Parent → children (top-down)._ready()— all children are also ready. Children → parent (bottom-up). This is where you do initialization that depends on the children._input(event)/_unhandled_input(event)— every input event._physics_process(delta)— a fixed tick. By default 60 Hz (Project Settings → Physics → Common → Physics Ticks Per Second)._process(delta)— every frame (variable timestep, like Unity’s Update)._exit_tree()— the node was removed from the tree (including when leaving the scene)._notification(what)— a low-level callback for NOTIFICATION_* events.
_ready fires once, when the node first enters the scene. If the node leaves the
tree and returns, _ready will not fire again. _enter_tree will fire again.
_process vs _physics_process
Similar to Unity:
| Method | Frequency | What goes there |
|---|---|---|
_physics_process(delta) | Fixed (60 Hz by default) | Physics: move_and_slide, apply_central_impulse |
_process(delta) | Every frame | Animation, UI updates, non-physical logic |
delta is the time since the last call. In _process it is variable, in _physics_process it is
constant (1.0 / physics_ticks_per_second).
extends CharacterBody3D
@export var speed: float = 5.0
func _physics_process(delta: float) -> void:
var input := Input.get_vector("move_left", "move_right", "move_forward", "move_back")
velocity.x = input.x * speed
velocity.z = input.y * speed
if not is_on_floor():
velocity.y += get_gravity().y * delta # gravity from project settings
move_and_slide()
Signals — a built-in pub-sub
A signal is declared in a node’s script. Any other node can connect to it.
# Declaration
signal damaged(amount: int, attacker: Node)
signal died
# Emit (new syntax in 4.x — properties-style)
damaged.emit(10, attacker)
died.emit()
# Connecting from another script
enemy.damaged.connect(_on_enemy_damaged)
enemy.died.connect(_on_enemy_died, CONNECT_ONE_SHOT) # once and disconnect
func _on_enemy_damaged(amount: int, attacker: Node) -> void:
print("Enemy took ", amount, " from ", attacker.name)
In the Inspector → Node tab → Signals you can connect signals visually, without code. Godot will generate the handler method itself.
An EventEmitter or an RxJS Subject. One object emits, many are subscribed.
A mix of UnityEvent (Inspector bindings) and C# events. In Godot this is one thing — a signal.
await — coroutines through signals
Instead of StartCoroutine, Godot has await:
# Wait one frame
await get_tree().process_frame
# Wait one second (via a tree timer)
await get_tree().create_timer(1.0).timeout
# Wait for a signal
await player.died
# Wait for an animation to finish
$AnimationPlayer.play("attack")
await $AnimationPlayer.animation_finished
print("Attack done!")
Any await returns control flow to the engine and continues when the signal is emitted. It works
on the main thread, like Unity coroutines — it does not create parallel threads.
get_node, the $ shortcut, and unique names
Ways to reach other nodes:
# Full paths
var sprite = get_node("Sprite3D")
var sprite_alt = $Sprite3D # shortcut
var deep = $"Body/Head/Sprite3D"
var parent = get_parent()
var first_child = get_child(0)
# Unique names — the % prefix
# A node marked "Access as Unique Name" in the scene → accessible from anywhere in the scene via %Name
var label = %ScoreLabel # equivalent to $"path/to/ScoreLabel" but independent of location
If you move a node in the tree, an ordinary $Path/To/Child will break. Unique names (%Name)
are searched within the scene and are resilient to renaming containers. Use them for frequently
referenced nodes: the main player, HUD elements.
Controlling activity
set_process(false)— turn off_process(the node stays in the scene).set_physics_process(false)— turn off_physics_process.set_process_input(false)— turn off input handling.visible = false— hide (for CanvasItem/Node3D).queue_free()— deferred deletion at the end of the frame (safer thanfree()).
# Destroy after one second
await get_tree().create_timer(1.0).timeout
queue_free()
The next chapter covers input through the Input Map.