Skip to content

Commit

Permalink
Add C# signal automatic disconnection info
Browse files Browse the repository at this point in the history
  • Loading branch information
31 committed Jan 23, 2024
1 parent 553e3f6 commit f18861f
Showing 1 changed file with 156 additions and 6 deletions.
162 changes: 156 additions & 6 deletions tutorials/scripting/c_sharp/c_sharp_signals.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,164 @@ In addition, you can always access signal names associated with a node type thro
await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame);
.. warning::
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.

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 ``+=``).
The following sections explain these cases in more detail and include
suggestions for how to disconnect manually.

An alternative to manually disconnecting using ``-=`` is to
:ref:`use Connect <using_connect_and_disconnect>` rather than ``+=``.
.. 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;
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 myClass;
public override void _EnterTree()
{
myClass.MySignal += OnMySignal;
}
public override void _ExitTree()
{
myClass.MySignal -= OnMySignal;
}
Another solution is to use ``Connect``, which does disconnect automatically with
custom signals:

.. code-block:: csharp
[Export] public MyClass myClass;
public override void _EnterTree()
{
myClass.Connect(MyClass.SignalName.MySignal, Callable.From(OnMySignal));
}
Custom signals as C# events
---------------------------
Expand Down

0 comments on commit f18861f

Please sign in to comment.