@@ -15,6 +15,10 @@ In some cases it's necessary to use the older
1515:ref: `Disconnect()<class_object_method_disconnect> ` APIs.
1616See :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+
1822Signals 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