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.