Can you write this from memory?
Connect the $SpawnTimer node's timeout signal to a method called _on_spawn_timer_timeout.
Signals are Godot's event system. Timers are just a common event source (timeout).
This page is a cheat-sheet + practice hub for Godot 4 GDScript: connect(), bind(), await create_timer(), cooldowns, and the mistakes that cause double-fires.
Connect
timer.timeout.connect(_on_timeout)
button.pressed.connect(_on_pressed)
One-shot connect
timer.timeout.connect(_on_timeout, CONNECT_ONE_SHOT)
Bind extra args
button.pressed.connect(_on_pick_upgrade.bind(upgrade_id))
Delay (fast, one-shot)
await get_tree().create_timer(0.25).timeout
Cancelable delay / cooldown (Timer node)
@onready var cooldown: Timer = $Cooldown
cooldown.start()
cooldown.stop()
1) Cooldown Gate (Timer node)
@onready var cd: Timer = $Cooldown
var ready := true
func _ready() -> void:
cd.timeout.connect(func(): ready = true)
func try_attack() -> void:
if not ready:
return
ready = false
do_attack()
cd.start()
2) Repeating Spawner (Timer node)
@onready var spawn_timer: Timer = $SpawnTimer
func _ready() -> void:
spawn_timer.timeout.connect(_spawn)
spawn_timer.start()
func _spawn() -> void:
spawn_enemy()
3) Debounce UI / Input (Timer node)
Prevents double-taps: every press "resets" the timer, and only the last press wins.
@onready var debounce: Timer = $Debounce
var pending := false
func _ready() -> void:
debounce.one_shot = true
debounce.timeout.connect(func():
if pending:
pending = false
commit_action()
)
func request_action() -> void:
pending = true
debounce.start(0.2) # restart window
4) Quick Delay (SceneTreeTimer)
Use for short sequences where you don't need to cancel.
func flash() -> void:
modulate = Color.RED
await get_tree().create_timer(0.1).timeout
modulate = Color.WHITE
Note: SceneTreeTimer is a one-shot timer with no built-in stop/cancel API. If you need cancellation, use a Timer node or a guard flag.
Godot signals can only connect once to the same Callable unless you use a reference-counting flag.
func _ready() -> void:
if not $Timer.timeout.is_connected(_on_timeout):
$Timer.timeout.connect(_on_timeout)
Bound Callables: store them if you plan to disconnect
var _cb: Callable
func _ready() -> void:
_cb = _on_pick_upgrade.bind(123)
$Button.pressed.connect(_cb)
func _exit_tree() -> void:
if $Button.pressed.is_connected(_cb):
$Button.pressed.disconnect(_cb)
| Use-case | Best tool |
|---|---|
| "wait 0.2s then do X" | create_timer + await |
| cooldowns / cancelable waits | Timer node |
| repeating ticks/spawns | Timer node |
| debounce/throttle | Timer node |
When you press a button, the button emits pressed. When a timer finishes, it emits timeout. When your player takes damage, your script emits health_changed. Same pattern every time.
# Godot 4 connection syntax:
timer.timeout.connect(_on_timer_timeout)
button.pressed.connect(_on_button_pressed)
player.health_changed.connect(_on_health_changed)
The signal is an object. .connect() takes a Callable (a function reference). This is the observer pattern at work—the emitter doesn't know or care who's listening. Under the hood, Godot stores connections as a list—emission iterates all connected Callables, so overhead is proportional to the number of connections (negligible for typical game use with a few connections per signal).
Passing Extra Arguments with bind()
Use .bind() to attach extra data:
button.pressed.connect(_on_upgrade_selected.bind(upgrade_data))
func _on_upgrade_selected(data: UpgradeData) -> void:
apply_upgrade(data)
Guard Pattern for "Cancelable" Await
If you need to abort an await sequence, use a guard variable:
var ability_active := false
func start_ability() -> void:
ability_active = true
await get_tree().create_timer(1.0).timeout
if not ability_active:
return # Was cancelled
finish_ability()
func cancel_ability() -> void:
ability_active = false
- Signal fires twice → you connected twice (common when reconnecting in _ready during re-entering tree).
- "Already connected" error → guard with is_connected(), or redesign so connect happens once.
- Tried to cancel create_timer() → use Timer node or a guard boolean.
- Disconnect didn't work → you didn't keep the exact bound Callable reference.
When to Use GDScript Timers & Signals
- Adding cooldowns to attacks, abilities, or spawning intervals without blocking game logic
- Decoupling game systems with custom signals so nodes communicate without direct references
- Creating delayed one-shot events like screen shake duration or invincibility frames
Check Your Understanding: GDScript Timers & Signals
Check Your Understanding: How do you create and connect custom signals in Godot 4 GDScript, and how does this differ from Godot 3?
In Godot 4, declare a signal with typed parameters: signal damage_taken(amount: int). Emit with damage_taken.emit(25). Connect using the signal object directly: node.damage_taken.connect(_on_damage_taken). Godot 3 used string-based connect("signal_name", target, "method") which was error-prone and not type-safe. Godot 4's Callable-based approach catches errors at parse time.
What You'll Practice: GDScript Timers & Signals
Common GDScript Timers & Signals Pitfalls
- Connecting signals in a loop without checking for existing connections—causes duplicate calls; use is_connected() or connect with CONNECT_ONE_SHOT flag
- Using get_tree().create_timer() and expecting to cancel it later—SceneTreeTimers have no stop/cancel API; use a Timer node if you need cancellation
- Forgetting that a freed node's signal connections are silently broken—if the receiver is queue_free'd, the signal emits with no error but nothing happens
- Using bind() but not storing the resulting Callable—disconnect() won't work unless you keep the exact bound Callable you connected
GDScript Timers & Signals FAQ
How do I use a Timer node in Godot 4 GDScript?
Add a Timer node as a child, configure wait_time and one_shot in the Inspector or code, then connect its timeout signal: $Timer.timeout.connect(_on_timer_timeout). Call $Timer.start() to begin. For one-shot timers, set one_shot = true.
What is the difference between get_tree().create_timer() and a Timer node in Godot 4?
get_tree().create_timer(seconds) returns a SceneTreeTimer for quick one-shot delays without adding a node. It has no stop/cancel API. A Timer node is a persistent child you can start, stop, pause, and configure as repeating. Use create_timer for simple delays and Timer nodes for recurring events or timers you need to control.
How do I pass arguments with signals in Godot 4?
Declare the signal with parameters: signal item_collected(item_name: String, value: int). Emit with item_collected.emit("gem", 50). The connected function must accept matching parameters. You can also use .connect(_on_func.bind(extra_arg)) to append additional arguments.
How do I connect a signal only once?
Use the CONNECT_ONE_SHOT flag: signal.connect(callback, CONNECT_ONE_SHOT). The connection auto-disconnects after the first emit. This is useful for one-time events like "level_complete".
How do I pass extra args to a signal callback?
Use Callable.bind(): button.pressed.connect(_on_button.bind(item_id, quantity)). The bound arguments are appended after any signal arguments.
Why is my signal firing twice?
You're probably connecting the signal multiple times (e.g., in _ready when the scene re-enters the tree). Use is_connected() to check first, or connect in the editor, or use CONNECT_ONE_SHOT for one-time events.
Timer node vs create_timer()—which should I use?
Use Timer node when you need to stop, pause, or restart the timer, or when it repeats. Use create_timer() for simple one-shot delays where you don't need control. Remember: create_timer() has no cancel API.
Can I cancel await get_tree().create_timer()?
No. SceneTreeTimers have no built-in stop/cancel API. Use a Timer node if you need cancellation, or use a guard variable to ignore the result when the await completes.
How do I disconnect a signal in Godot 4?
Use signal.disconnect(callable). If you used bind(), store that Callable so you can disconnect the exact same one later. Example: var _cb = func.bind(arg); signal.connect(_cb); later signal.disconnect(_cb).
Why do I get "signal is already connected" in Godot 4?
A signal can only be connected once to the same Callable. If you connect in _ready and the node re-enters the tree, you may reconnect. Guard with is_connected() or connect once in a place that only runs once (like _init or a setup function called externally).
GDScript Timers & Signals Syntax Quick Reference
extends CharacterBody2D
@onready var attack_timer: Timer = $AttackCooldown
var can_attack: bool = true
func _ready() -> void:
attack_timer.timeout.connect(_on_cooldown_finished)
func attack() -> void:
if not can_attack:
return
can_attack = false
attack_timer.start()
print("Attack!")
func _on_cooldown_finished() -> void:
can_attack = trueextends Node
signal health_changed(new_hp: int, max_hp: int)
signal died
var hp: int = 100
var max_hp: int = 100
func take_damage(amount: int) -> void:
hp = max(hp - amount, 0)
health_changed.emit(hp, max_hp)
if hp == 0:
died.emit()extends Node2D
func start_invincibility() -> void:
set_collision_layer_value(1, false)
modulate = Color(1, 1, 1, 0.5)
await get_tree().create_timer(1.5).timeout
set_collision_layer_value(1, true)
modulate = Color(1, 1, 1, 1)extends Node
var _bound_cb: Callable
func _ready() -> void:
_bound_cb = _on_item_selected.bind(item_id)
if not inventory.item_selected.is_connected(_bound_cb):
inventory.item_selected.connect(_bound_cb)
func _exit_tree() -> void:
if inventory.item_selected.is_connected(_bound_cb):
inventory.item_selected.disconnect(_bound_cb)GDScript Timers & Signals Sample Exercises
Fill in the blank to connect the signal to the callback.
connectWhat does this code print?
50What does this code print?
A
B+ 12 more exercises