Quick answer: Scenes cannot see each other's nodes reliably, and connections are lost when a scene is freed. Put shared signals on an autoload (an event bus), emit them from anywhere with Events.player_died.emit(), and connect to them in each listener's _ready(). Godot removes the connection automatically when the listener is freed.
The UI needs to know when the player takes damage, but the HUD and the player live in different scenes. Wiring them with get_node("../../Level/Player") works until the tree changes. There are better patterns, and they also fix the "signal stopped working after changing level" bug.
Why direct connections break
A connection is between two specific objects. When change_scene_to_file() frees the old scene, every connection to its nodes goes with it. Paths like get_node("/root/Main/Level/Player") also break the moment someone renames or reparents a node, and fail outright when the target scene is not loaded yet.
Pattern 1: an event bus autoload
Create events.gd:
extends Node
signal player_damaged(amount: int, remaining: int)
signal coin_collected(total: int)
signal level_completed(level_id: String)
Register it under Project Settings > Globals > Autoload with the name Events. Autoloads live for the whole game, above any scene change.
Emit from anywhere:
# player.gd
func take_damage(amount: int):
health -= amount
Events.player_damaged.emit(amount, health)
Listen from anywhere:
# hud.gd
func _ready():
Events.player_damaged.connect(_on_player_damaged)
func _on_player_damaged(_amount: int, remaining: int):
$HealthBar.value = remaining
Neither scene references the other. When the HUD is freed, its connection is removed automatically.
Pattern 2: connect when you instance
When one scene creates another, it already has the reference, so connect at that moment:
var enemy := EnemyScene.instantiate()
enemy.died.connect(_on_enemy_died)
add_child(enemy)
Pattern 3: groups for broadcasts
To call a method on every member of a set without signals: get_tree().call_group("enemies", "alert", player_position). Good for one-off broadcasts; signals are better for ongoing events with typed arguments.
Common mistakes with an event bus
- Not registered as an autoload. The script exists but
Eventsis unknown, or it was added as a scene with a different name. - Connecting twice. Connecting in a function that runs more than once triggers "already connected" errors. Connect in
_ready(), or checkis_connected(). - Lambdas capturing nodes. A lambda connected to an autoload signal is not removed when the nodes it captured are freed. Connect methods instead (see freed objects and pending signals).
- Putting everything on the bus. Keep it for genuinely cross-scene events. Parent-child communication should stay as direct signals.