Merge pull request #8815 from 31/dev/31/signal-connect

Add C# signal automatic disconnection info
This commit is contained in:
Max Hilbrunner
2024-01-30 10:49:07 +01:00
committed by GitHub

View File

@@ -15,6 +15,10 @@ In some cases it's necessary to use the older
:ref:`Disconnect()<class_object_method_disconnect>` APIs.
See :ref:`using_connect_and_disconnect` for more details.
If you encounter a ``System.ObjectDisposedException`` while handling a signal,
you might be missing a signal disconnection. See
:ref:`disconnecting_automatically_when_the_receiver_is_freed` for more details.
Signals as C# events
--------------------
@@ -33,15 +37,6 @@ In addition, you can always access signal names associated with a node type thro
await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame);
.. warning::
While all engine signals connected as events are automatically disconnected when nodes are freed, custom
signals connected using ``+=`` aren't. This means 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
:ref:`use Connect <using_connect_and_disconnect>` rather than ``+=``.
Custom signals as C# events
---------------------------
@@ -184,3 +179,167 @@ does nothing.
{
GD.Print("Greetings!");
}
.. _disconnecting_automatically_when_the_receiver_is_freed:
Disconnecting automatically when the receiver is freed
------------------------------------------------------
Normally, when any ``GodotObject`` is freed (such as any ``Node``), Godot
automatically disconnects all connections associated with that object. This
happens for both signal emitters and signal receivers.
For example, a node with this code will print "Hello!" when the button is
pressed, then free itself. Freeing the node disconnects the signal, so pressing
the button again doesn't do anything:
.. code-block:: csharp
public override void _Ready()
{
Button myButton = GetNode<Button>("../MyButton");
myButton.Pressed += SayHello;
}
private void SayHello()
{
GD.Print("Hello!");
Free();
}
When a signal receiver is freed while the signal emitter is still alive, in some
cases automatic disconnection won't happen:
- The signal is connected to a lambda expression that captures a variable.
- The signal is a custom signal.
The following sections explain these cases in more detail and include
suggestions for how to disconnect manually.
.. note::
Automatic disconnection is totally reliable if a signal emitter is freed
before any of its receivers are freed. With a project style that prefers
this pattern, the above limits may not be a concern.
No automatic disconnection: a lambda expression that captures a variable
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you connect to a lambda expression that captures variables, Godot can't tell
that the lambda is associated with the instance that created it. This causes
this example to have potentially unexpected behavior:
.. code-block:: csharp
Timer myTimer = GetNode<Timer>("../Timer");
int x = 0;
myTimer.Timeout += () =>
{
x++; // This lambda expression captures x.
GD.Print($"Tick {x} my name is {Name}");
if (x == 3)
{
GD.Print("Time's up!");
Free();
}
};
.. code-block:: text
Tick 1, my name is ExampleNode
Tick 2, my name is ExampleNode
Tick 3, my name is ExampleNode
Time's up!
[...] System.ObjectDisposedException: Cannot access a disposed object.
On tick 4, the lambda expression tries to access the ``Name`` property of the
node, but the node has already been freed. This causes the exception.
To disconnect, keep a reference to the delegate created by the lambda expression
and pass that to ``-=``. For example, this node connects and disconnects using
the ``_EnterTree`` and ``_ExitTree`` lifecycle methods:
.. code-block:: csharp
[Export]
public Timer MyTimer { get; set; }
private Action _tick;
public override void _EnterTree()
{
int x = 0;
_tick = () =>
{
x++;
GD.Print($"Tick {x} my name is {Name}");
if (x == 3)
{
GD.Print("Time's up!");
Free();
}
};
MyTimer.Timeout += _tick;
}
public override void _ExitTree()
{
MyTimer.Timeout -= _tick;
}
In this example, ``Free`` causes the node to leave the tree, which calls
``_ExitTree``. ``_ExitTree`` disconnects the signal, so ``_tick`` is never
called again.
The lifecycle methods to use depend on what the node does. Another option is to
connect to signals in ``_Ready`` and disconnect in ``Dispose``.
.. note::
Godot uses `Delegate.Target <https://learn.microsoft.com/en-us/dotnet/api/system.delegate.target>`_
to determine what instance a delegate is associated with. When a lambda
expression doesn't capture a variable, the generated delegate's ``Target``
is the instance that created the delegate. When a variable is captured, the
``Target`` instead points at a generated type that stores the captured
variable. This is what breaks the association. If you want to see if a
delegate will be automatically cleaned up, try checking its ``Target``.
``Callable.From`` doesn't affect the ``Delegate.Target``, so connecting a
lambda that captures variables using ``Connect`` doesn't work any better
than ``+=``.
No automatic disconnection: a custom signal
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Connecting to a custom signal using ``+=`` doesn't disconnect automatically when
the receiving node is freed.
To disconnect, use ``-=`` at an appropriate time. For example:
.. code-block:: csharp
[Export]
public MyClass Target { get; set; }
public override void _EnterTree()
{
Target.MySignal += OnMySignal;
}
public override void _ExitTree()
{
Target.MySignal -= OnMySignal;
}
Another solution is to use ``Connect``, which does disconnect automatically with
custom signals:
.. code-block:: csharp
[Export]
public MyClass Target { get; set; }
public override void _EnterTree()
{
Target.Connect(MyClass.SignalName.MySignal, Callable.From(OnMySignal));
}