~2 min read

GDScript, lifecycle, and signals

How Godot calls your code — _ready, _process, _physics_process, signals, await.

Иллюстрация: GDScript, lifecycle, and signals

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
Web

A React component: useState for fields, useEffect(() => {}, []) for initialization, the render function each time. props are the @export fields.

Unity

[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:

  1. _init() — the object’s constructor, even before the node is added to the tree.
  2. _enter_tree() — the node was added to the scene tree. Parent → children (top-down).
  3. _ready() — all children are also ready. Children → parent (bottom-up). This is where you do initialization that depends on the children.
  4. _input(event) / _unhandled_input(event) — every input event.
  5. _physics_process(delta) — a fixed tick. By default 60 Hz (Project Settings → Physics → Common → Physics Ticks Per Second).
  6. _process(delta) — every frame (variable timestep, like Unity’s Update).
  7. _exit_tree() — the node was removed from the tree (including when leaving the scene).
  8. _notification(what) — a low-level callback for NOTIFICATION_* events.
_ready vs Unity Start

_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:

MethodFrequencyWhat goes there
_physics_process(delta)Fixed (60 Hz by default)Physics: move_and_slide, apply_central_impulse
_process(delta)Every frameAnimation, 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.

Web

An EventEmitter or an RxJS Subject. One object emits, many are subscribed.

Unity

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
Unique names save you from fragility

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 than free()).
# Destroy after one second
await get_tree().create_timer(1.0).timeout
queue_free()

The next chapter covers input through the Input Map.