1
\$\begingroup\$

The godot documentation provides examples of connecting Signals via code in the _Ready lifecycle method here (I'm using C#).

However, these examples do not show the handlers being disconnected when the node is leaving the scene tree or being destroyed.

If I have a node that will enter and leave the scene tree multiple times during the life of the game, can this not lead to "event leaks" as the event publisher still holds a reference to the subscriber and therefore prevents garbage collection?

Is there a typical pattern for unsubscribing Signals in Godot when a node is destroyed or leaves the scene tree?

\$\endgroup\$

2 Answers 2

2
\$\begingroup\$

Edit: Per the docs, the following only applies to custom signals. Built-in signals like Timer.timeout will clean themselves up.

If you're using the example to connect to the signal as a C# event like so:

public override void _Ready() // You could also use _EnterTree()
    {
        var timer = GetNode<Timer>("Timer");

        // This is where you connect by adding a subscription to the event
        timer.Timeout += OnTimerTimeout;
    }

Then per the docs you must unsubscribe from the event:

Warning

While all engine signals connected as events are automatically disconnected when nodes are freed, custom signals connected using += aren't. Meaning that: you will need to manually disconnect (using -=) all the custom signals you connected as C# events (using +=).

An alternative to manually disconnecting using -= is to use Connect rather than +=.

See Godot issue #70414.

You can use the same method you would in other C# applications to disconnect the signal by removing the method from the event in the something like the _ExitTree() method.

public override void _ExitTree() 
{
    timer.Timeout -= OnTimerTimeout;
}

Note: For this specific example, you do need to raise the variable storing the reference to the Timer node outside of the _Ready() method so that it is accessible by the _ExitTree() method. So the entire script from the tutorial would look like this:

using Godot;

public partial class MySprite2D : Sprite2D
{
    private float _speed = 400;
    private float _angularSpeed = Mathf.Pi;
    
    // 1. Lift the timer variable to a higher scope.
    private Timer timer; 

    public override void _Ready()
    {
        // 2. Get the timer node and store it in our class field.
        timer = GetNode<Timer>("Timer");
        
        // 3. Connect to the Timeout signal by subscribing to the event.
        timer.Timeout += OnTimerTimeout;
    }

    public override void _Process(double delta)
    {
        Rotation += _angularSpeed * (float)delta;
        var velocity = Vector2.Up.Rotated(Rotation) * _speed;
        Position += velocity * (float)delta;
    }

    public override void _ExitTree()
    {
        // 4. Unsubscribe from the event when the MySprite2D leaves the tree
        timer.Timeout -= OnTimerTimeout;
    }

    private void OnButtonPressed()
    {
        SetProcess(!IsProcessing());
    }

    private void OnTimerTimeout()
    {
        Visible = !Visible;
    }
}

References:

\$\endgroup\$
1
\$\begingroup\$

Disclamers:

  • I couldn't find good documentation on this - my answer is based on creating some test code and verifying actual results.
  • I only use GDScript - its likely C# is similar, but you will have to repeat my testing.

Godot uses a combination of explicit reference counting (RefCounted and WeakRef), manual allocation (new(), free() and queue_free()) and some built in reference counting inside the engine.

My testing "appears" to show that:

  • The connection between a signal and a callable is tracked by the engine.
  • There are two objects associated with the connection:
    • The source of the event.
    • The target object (self) containing the callable code (may be different to the event source)
  • Either object can be destroyed.
    • Which results in the connection being destroyed automatically by the engine - i.e.: the connection is removed from the get_connections() array.

In other words it "appears" that the signal connection behaves like a weak reference - it is not sufficient (by itself) to prevent an object being released.


The source of events will typically be a child of Node - hence will need to be removed explicitly with either free() or queue_free() - simply removing them from the tree with remove_child() is not sufficient to destroy them. Note: queue_free() propagates through a tree so freeing the root of a tree, frees the whole tree.

The target object (containing the callback method) is typically either:

  • A child of Node - in which case the previous statement is true for the target.
  • Or a custom class - which the engine assumes to be a child of RefCounted - in which case it will be destroyed by the engine as soon as the last "Strong" reference is broken (again the signal connection appears to be weak).

Per request in the comments, here is the minimal viable test case showing that the connections are removed automatically by the engine.

extends Control

var button: Button
var custom: CustomClass
var target: TargetControl

class CustomClass:
    func custom_callback() -> void:
        print(str("Custom: ", self))

class TargetControl:
    extends Control
    func target_on_pressed() -> void:
        print(str("TargetControl on_pressed: ", self))


func _ready() -> void:
    button = Button.new()
    custom = CustomClass.new()
    target = TargetControl.new()
    add_child(button)
    add_child(target)

    button.pressed.connect(custom.custom_callback)
    button.pressed.connect(target.target_on_pressed)

    print(button.pressed.get_connections())

    # Clear the previously registered custom class
    custom = null

    # If queue_free() is used the connection will still be present as it is removed during idle.
    target.free()
    #target.queue_free()

    # Check connections again
    print(button.pressed.get_connections())

Steps to reproduce:

  • Create a new Godot Project.
  • Create a new UI Scene.
  • Add the script (above) to the scene.
  • Run the scene.

Output:

[{ "signal": Button::[signal]pressed, "callable": RefCounted::custom_callback, "flags": 0 }, { "signal": Button::[signal]pressed, "callable": Control::target_on_pressed, "flags": 0 }]
[]

The first array has two callbacks, in the second the array is empty.

\$\endgroup\$
3
  • \$\begingroup\$ Can you show how you determined that it "appears" to behave this way? I suspect you're right, but showing the evidence that supports this would give me and other readers more confidence in clicking the "upvote" button, and could be an opportunity for your readers to learn about techniques they can use to investigate topics like this themselves. As an example, I wrote a similar answer about Unity, and shared code a reader could run to test it out. \$\endgroup\$
    – DMGregory
    Commented Aug 13 at 18:09
  • \$\begingroup\$ Thanks for providing the code! That's an upvote from me. 👍 \$\endgroup\$
    – DMGregory
    Commented Aug 13 at 19:05
  • \$\begingroup\$ The connection is not a weak reference, instead the destructor disconnects the instances. I mention it on What is happening during queue_free() process?. And yes, this is not well documentend. \$\endgroup\$
    – Theraot
    Commented Aug 14 at 11:52

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .