Skip to content

Commit 52414e8

Browse files
committed
Add C# signal automatic disconnection info
1 parent 67b1795 commit 52414e8

File tree

1 file changed

+168
-9
lines changed

1 file changed

+168
-9
lines changed

tutorials/scripting/c_sharp/c_sharp_signals.rst

Lines changed: 168 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ In some cases it's necessary to use the older
1515
:ref:`Disconnect()<class_object_method_disconnect>` APIs.
1616
See :ref:`using_connect_and_disconnect` for more details.
1717

18+
If you encounter a ``System.ObjectDisposedException`` while handling a signal,
19+
you might be missing a signal disconnection. See
20+
:ref:`disconnecting_automatically_when_the_receiver_is_freed` for more details.
21+
1822
Signals as C# events
1923
--------------------
2024

@@ -33,15 +37,6 @@ In addition, you can always access signal names associated with a node type thro
3337
3438
await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame);
3539
36-
.. warning::
37-
38-
While all engine signals connected as events are automatically disconnected when nodes are freed, custom
39-
signals connected using ``+=`` aren't. This means you will need to manually disconnect (using ``-=``)
40-
all the custom signals you connected as C# events (using ``+=``).
41-
42-
An alternative to manually disconnecting using ``-=`` is to
43-
:ref:`use Connect <using_connect_and_disconnect>` rather than ``+=``.
44-
4540
Custom signals as C# events
4641
---------------------------
4742

@@ -184,3 +179,167 @@ does nothing.
184179
{
185180
GD.Print("Greetings!");
186181
}
182+
183+
.. _disconnecting_automatically_when_the_receiver_is_freed:
184+
185+
Disconnecting automatically when the receiver is freed
186+
------------------------------------------------------
187+
188+
Normally, when any ``GodotObject`` is freed (such as any ``Node``), Godot
189+
automatically disconnects all connections associated with that object. This
190+
happens for both signal emitters and signal receivers.
191+
192+
For example, a node with this code will print "Hello!" when the button is
193+
pressed, then free itself. Freeing the node disconnects the signal, so pressing
194+
the button again doesn't do anything:
195+
196+
.. code-block:: csharp
197+
198+
public override void _Ready()
199+
{
200+
Button myButton = GetNode<Button>("../MyButton");
201+
myButton.Pressed += SayHello;
202+
}
203+
204+
private void SayHello()
205+
{
206+
GD.Print("Hello!");
207+
Free();
208+
}
209+
210+
When a signal receiver is freed while the signal emitter is still alive, in some
211+
cases automatic disconnection won't happen:
212+
213+
- The signal is connected to a lambda expression that captures a variable.
214+
- The signal is a custom signal.
215+
216+
The following sections explain these cases in more detail and include
217+
suggestions for how to disconnect manually.
218+
219+
.. note::
220+
221+
Automatic disconnection is totally reliable if a signal emitter is freed
222+
before any of its receivers are freed. With a project style that prefers
223+
this pattern, the above limits may not be a concern.
224+
225+
No automatic disconnection: a lambda expression that captures a variable
226+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
227+
228+
If you connect to a lambda expression that captures variables, Godot can't tell
229+
that the lambda is associated with the instance that created it. This causes
230+
this example to have potentially unexpected behavior:
231+
232+
.. code-block:: csharp
233+
234+
Timer myTimer = GetNode<Timer>("../Timer");
235+
int x = 0;
236+
myTimer.Timeout += () =>
237+
{
238+
x++; // This lambda expression captures x.
239+
GD.Print($"Tick {x} my name is {Name}");
240+
if (x == 3)
241+
{
242+
GD.Print("Time's up!");
243+
Free();
244+
}
245+
};
246+
247+
.. code-block:: text
248+
249+
Tick 1, my name is ExampleNode
250+
Tick 2, my name is ExampleNode
251+
Tick 3, my name is ExampleNode
252+
Time's up!
253+
[...] System.ObjectDisposedException: Cannot access a disposed object.
254+
255+
On tick 4, the lambda expression tries to access the ``Name`` property of the
256+
node, but the node has already been freed. This causes the exception.
257+
258+
To disconnect, keep a reference to the delegate created by the lambda expression
259+
and pass that to ``-=``. For example, this node connects and disconnects using
260+
the ``_EnterTree`` and ``_ExitTree`` lifecycle methods:
261+
262+
.. code-block:: csharp
263+
264+
[Export]
265+
public Timer MyTimer { get; set; }
266+
267+
private Action _tick;
268+
269+
public override void _EnterTree()
270+
{
271+
int x = 0;
272+
_tick = () =>
273+
{
274+
x++;
275+
GD.Print($"Tick {x} my name is {Name}");
276+
if (x == 3)
277+
{
278+
GD.Print("Time's up!");
279+
Free();
280+
}
281+
};
282+
MyTimer.Timeout += _tick;
283+
}
284+
285+
public override void _ExitTree()
286+
{
287+
MyTimer.Timeout -= _tick;
288+
}
289+
290+
In this example, ``Free`` causes the node to leave the tree, which calls
291+
``_ExitTree``. ``_ExitTree`` disconnects the signal, so ``_tick`` is never
292+
called again.
293+
294+
The lifecycle methods to use depend on what the node does. Another option is to
295+
connect to signals in ``_Ready`` and disconnect in ``Dispose``.
296+
297+
.. note::
298+
299+
Godot uses `Delegate.Target <https://learn.microsoft.com/en-us/dotnet/api/system.delegate.target>`_
300+
to determine what instance a delegate is associated with. When a lambda
301+
expression doesn't capture a variable, the generated delegate's ``Target``
302+
is the instance that created the delegate. When a variable is captured, the
303+
``Target`` instead points at a generated type that stores the captured
304+
variable. This is what breaks the association. If you want to see if a
305+
delegate will be automatically cleaned up, try checking its ``Target``.
306+
307+
``Callable.From`` doesn't affect the ``Delegate.Target``, so connecting a
308+
lambda that captures variables using ``Connect`` doesn't work any better
309+
than ``+=``.
310+
311+
No automatic disconnection: a custom signal
312+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
313+
314+
Connecting to a custom signal using ``+=`` doesn't disconnect automatically when
315+
the receiving node is freed.
316+
317+
To disconnect, use ``-=`` at an appropriate time. For example:
318+
319+
.. code-block:: csharp
320+
321+
[Export]
322+
public MyClass Target { get; set; }
323+
324+
public override void _EnterTree()
325+
{
326+
Target.MySignal += OnMySignal;
327+
}
328+
329+
public override void _ExitTree()
330+
{
331+
Target.MySignal -= OnMySignal;
332+
}
333+
334+
Another solution is to use ``Connect``, which does disconnect automatically with
335+
custom signals:
336+
337+
.. code-block:: csharp
338+
339+
[Export]
340+
public MyClass Target { get; set; }
341+
342+
public override void _EnterTree()
343+
{
344+
Target.Connect(MyClass.SignalName.MySignal, Callable.From(OnMySignal));
345+
}

0 commit comments

Comments
 (0)