Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice

Question Removing a UnityAction from a UnityEvent makes all subscribers unsubscribe from the UnityEvent

Discussion in 'Scripting' started by DebbyX3, Dec 7, 2022.

  1. DebbyX3

    DebbyX3

    Joined:
    Oct 22, 2020
    Posts:
    2
    Hi, I'm currently having an issue regarding the UnityEvents and UnityAction.

    I have the class Caretaker, which is a singleton, that has a public UnityEvent SaveGlobalStateEvent accessible from the outside.
    I add a UnityAction listener SaveGlobalStateAction in another class GObjController, and then Caretaker invokes the event at some point of my code. I'll show an example:

    Class Caretaker:
    Code (CSharp):
    1. public class CaretakerScene : MonoBehaviour
    2. {
    3.     public UnityEvent SaveGlobalStateEvent = new UnityEvent();
    4.    
    5.     public void FireEvent() // someone will call this
    6.     {
    7.         SaveGlobalStateEvent.Invoke();
    8.     }
    9.  
    10.     public void SaveGlobalState(GameObjController gObj)
    11.     {
    12.         // some code
    13.     }
    14. }
    Class GObjController:
    Code (CSharp):
    1. public class GObjController : MonoBehaviour
    2. {
    3.     private UnityAction SaveGlobalStateAction;
    4.  
    5.     private void SetGlobalUnityActions()
    6.     {
    7.         SaveGlobalStateAction += () => CaretakerScene.SaveGlobalState(this);
    8.         SaveGlobalStateAction += () => Print("something");
    9.     }
    10.  
    11.     private void Print (string s)
    12.     {
    13.         Debug.Log(s);
    14.     }
    15.    
    16.     public void SubscribeToGlobalScene()
    17.     {
    18.         CaretakerScene.SaveGlobalStateEvent.AddListener(SaveGlobalStateAction);
    19.     }
    20.    
    21.     public void UnsubscribeFromGlobalScene()
    22.     {
    23.         CaretakerScene.SaveGlobalStateEvent.RemoveListener(SaveGlobalStateAction);
    24.     }
    25. }
    Now: the class Caretaker is, as I said, a singleton.
    However, GObjController is a script attached to a lot of objects in the scene. So I have many instances of GObjController, and each of them adds their own SaveGlobalStateAction by calling SubscribeToGlobalScene(), at different times.

    Note that SaveGlobalStateAction is the same for every GObjController object, the ony difference is the "this" parameter passed

    My problem:
    When I issue UnsubscribeFromGlobalScene (by effectively doing RemoveListener, and passing SaveGlobalStateAction), the object unsubscribes from that event, but not only that object!
    The unsubscription affects ALL the objects that are subscribed!

    I am sure of that because I even tried with the debugger, and after running UnsubscribeFromGlobalScene for one object, the events are not fired for all the other objects previously subscribed.

    At this point, I was wondering that maybe I am using the UnityEvents wrong, but I don't think so.
    I also thought that maybe removing the Action from the event just removes all the actions because the methods in the actions have the same name, so Unity removes them by name, and not by reference.

    Am I doing something wrong? I just want to unsubscribe one object from the corresponding event, and not all of them
     
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,947
    This smells like it might be related to how delegates are sort of value types, not really reference types.

    IOW, if you pass SaveGlobalStateAction into someone, then you change the listeners on SaveGlobalStateAction, you just made a brand-new SaveGlobalStateAction unrelated to the previous one you handed in.

    But... I will confess, the above approach is not my go-to way of solving issues, but perhaps @Max-om could chime in, as they seem much more "on point" about how to make this work well.
     
  3. Max-om

    Max-om

    Joined:
    Aug 9, 2017
    Posts:
    486
    A delegate is a reference type. It's why you can do += and -= on c# events. I have never used unityevents because i don't like how they are implemented.
     
  4. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,947
    Is it? Try this codelet:

    Code (csharp):
    1.     System.Action alpha;
    2.  
    3.     void Start ()
    4.     {
    5.         alpha += () => { Debug.Log( "One."); };
    6.  
    7.         System.Action bravo = alpha;
    8.  
    9.         alpha += () => { Debug.Log( "Two."); };
    10.  
    11.         Debug.Log( "Here we go! First alpha()...");
    12.         alpha();
    13.         Debug.Log( "Here we go! And now bravo()...");
    14.         bravo();
    15.     }
    I get:

    Screen Shot 2022-12-07 at 10.44.31 AM.png

    This does not seem very reference-type-ey to me. :)

    If it was a reference type, I would expect the
    bravo()
    invocation to fire both One and Two.

    I seem to recall UnityEvents followed this pattern as well, but yeah, haven't looked deeply into them. Give it a shot!
     
  5. Max-om

    Max-om

    Joined:
    Aug 9, 2017
    Posts:
    486
    They are reference types

    https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/reference-types

    But multicast delegates behaves a bit odd that is true :)

    Edit: one reason why I stay away from them
     
    Kurt-Dekker likes this.
  6. DebbyX3

    DebbyX3

    Joined:
    Oct 22, 2020
    Posts:
    2
    Thanks for your replies!

    So, for what I understand, the situation described in my first post is the expected behaviour?
    And if so, how can I solve the problem?

    A solution I had in mind was to make all the all the objects re-subscribe to their correct event when a RemoveListener is issued (like: loop on a list of all the objects that satisfy a specific condition and run AddListener, passing the previously removed Action).

    Keep in mind that this operation would not be that frequent (RemoveListener is rarely called), but I sense that it's not the best solution
     
  7. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,947
    I normally wrap something like this in a small API with Add() and Remove() methods, and then have the delegate be private, along with a public Invoke() method.
     
    Last edited: Dec 12, 2022
    DebbyX3 likes this.