wrote a small signal debug utility for Godot 4 — had no idea half my connections were stale

305 views 4 replies

Running into a weird bug yesterday where a UI element was responding to a signal twice on every emission. Spent way too long chasing it before realizing I had the same signal connected in both the editor and in _ready(). Classic.

The problem is there's no quick way to audit what's actually wired up at runtime. Object.get_signal_connection_list() exists, but you have to query each signal by name — you can't just ask a node "what are all your active connections." So I wrote a small static utility that walks a node (optionally recursive) and prints everything:

class_name SignalDebug

static func debug_signals(node: Node, recursive: bool = false, depth: int = 0) -> void:
    var prefix := "  ".repeat(depth)
    var printed_node := false
    for sig in node.get_signal_list():
        var connections := node.get_signal_connection_list(sig.name)
        if connections.is_empty():
            continue
        if not printed_node:
            print("%s[%s] %s" % [prefix, node.get_class(), node.name])
            printed_node = true
        for conn in connections:
            var target := conn.callable.get_object()
            var method := conn.callable.get_method()
            var target_label := target.name if target is Node else target.get_class()
            print("%s  .%s → %s::%s" % [prefix, sig.name, target_label, method])
    if recursive:
        for child in node.get_children():
            debug_signals(child, true, depth + 1)

Drop it in a file with class_name SignalDebug and call it anywhere:

SignalDebug.debug_signals(get_tree().root, true)

Output ends up looking like:

[CanvasLayer] HUD
  .visibility_changed → GameManager::_on_hud_visibility_changed
[Button] StartButton
  .pressed → UIController::_on_start_pressed
  .pressed → AudioManager::_on_button_press   ← this one definitely shouldn't still be here

Immediately obvious when you've got duplicate connections or leftover references from old refactors. First time I ran it on my current project I found four stale connections I didn't know existed, including one pointing to a freed node that somehow hadn't triggered an error yet.

It's become a staple in my debug builds. Curious if anyone's taken this further — an actual in-editor panel, maybe filtering to only show connections to external nodes, or hooking into the remote debugger protocol? I keep meaning to turn it into a proper plugin but never get around to it.

this would've saved me two days last month. had a singleton connecting to a scene signal, scene reloading on every level transition, nobody cleaning up old connections. by level 5 there were 5 listeners firing on the same emission and the behavior was completely baffling. same event producing multiplicatively worse outcomes the longer you played. couldn't figure out why.

two days. detective board conspiracy red string

does your tool surface connections to already-freed objects, or only currently live ones?

the double connection issue hits specifically hard in godot because the inspector and _ready() both feel like equally valid places to wire signals, and they are, but if you switch approaches halfway through a feature without cleaning up the other side, you're just waiting for the bug to surface. i've started keeping a comment block at the top of any node class that has signals, just listing what's connected where and why. very low tech but it's caught two problems for me in the last month before they became actual bugs.

The editor + _ready() double-connection is the one that gets everyone eventually. Something I started doing after the third time: any signal connection in _ready() gets an is_connected() guard by default, even when I'm completely confident it's only connected once. Slightly paranoid, but it's caught this exact class of bug multiple times, and it means you can safely reload scenes without worrying about teardown order or stale connection counts.

the double-emission case is such a classic. worst one i had was a signal connected in the editor and in _ready(), and it only doubled up under a specific scene load order so reproducing it consistently took forever. your tool would've caught it immediately.

quick question: does it surface connections across the whole scene tree or just on the node you're actively inspecting? wondering if it handles the case where node A connects to a signal on node B and then node B gets freed first.

detective magnifying glass clue
Moonjump
Forum Search Shader Sandbox
Sign In Register