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. Dismiss Notice

[Solved]Custom UnityAction class

Discussion in 'Scripting' started by kabuto178, May 30, 2017.

  1. kabuto178

    kabuto178

    Joined:
    Mar 21, 2017
    Posts:
    59
    How can I extend a custom UnityAction to accept generic parameters?
     
  2. BlackPete

    BlackPete

    Joined:
    Nov 16, 2016
    Posts:
    970
    Can you show a bit of code to demonstrate what you're trying to do?

    UnityActions can accept up to 4 generic parameters by default, and if you needed more, you could roll your own.

    Or use a struct parameter.
     
  3. kabuto178

    kabuto178

    Joined:
    Mar 21, 2017
    Posts:
    59
    Code (csharp):
    1. private Dictionary<string, UnityEvent> eventDictionary;
    2.     private static EventsManager eventsManager;
    3.     public static EventsManager INSTANCE
    4.     {
    5.         get
    6.         {
    7.             if (!eventsManager)
    8.             {
    9.                 eventsManager = FindObjectOfType(typeof(EventsManager)) as EventsManager;
    10.                 if (!eventsManager)
    11.                 {
    12.                     Debug.LogError("EventsManager was not defined as well");
    13.                 }else
    14.                 {
    15.                     eventsManager.Init();
    16.                  
    17.                 }
    18.             }
    19.             return eventsManager;
    20.         }
    21.     }
    22.  
    23.     void Init()
    24.     {
    25.         if(eventDictionary == null)
    26.         {
    27.             eventDictionary = new Dictionary<string, UnityEvent>();
    28.         }
    29.     }
    30.  
    31.     public static void StartListening(string eventName, UnityAction<CommunityObject> listener)
    32.     {
    33.         UnityEvent thisEvent = null;
    34.         if(INSTANCE.eventDictionary.TryGetValue(eventName, out thisEvent))
    35.         {
    36.             thisEvent.AddListener(listener);
    37.         }else
    38.         {
    39.             thisEvent = new UnityEvent();
    40.             thisEvent.AddListener(listener);
    41.             INSTANCE.eventDictionary.Add(eventName, thisEvent);
    42.         }
    43.     }
    44.  
    45.     public static void StopListening(string eventName, UnityAction listener)
    46.     {
    47.         if (eventsManager == null)
    48.             return;
    49.  
    50.         UnityEvent thisEvent = null;
    51.         if(INSTANCE.eventDictionary.TryGetValue(eventName, out thisEvent))
    52.         {
    53.             thisEvent.RemoveListener(listener);
    54.         }
    55.     }
    56.  
    57.     public static void TriggerEvent(string eventName)
    58.     {
    59.         UnityEvent thisEvent = null;
    60.         if(INSTANCE.eventDictionary.TryGetValue(eventName, out thisEvent))
    61.         {
    62.             thisEvent.Invoke();
    63.         }
    64.  
    65.      
    66.     }
    This is my event manager class. I would use Trigger event to post a new event in another class file like below
    Code (csharp):
    1. UnityAction listener;
    2. ....
    3. listener = new UnityAction(someFunc);
    4.  
    5. ....
    6. EventsManager.StartListening("event", listener);
    7. void someFunc(){
    8. ....
    9. }
    10.  
    My issue is that I want to pass some data to someFunc but when I add parameters to the method, it throws an error.
     
    Last edited: May 31, 2017
  4. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,380
  5. kabuto178

    kabuto178

    Joined:
    Mar 21, 2017
    Posts:
    59
    Of course, I was looking for the option in the editor, didn't happen to see it at first glance but I do realized I have to go into an embedding item, this should be up front since this is a widely used formatting option.
     
  6. BlackPete

    BlackPete

    Joined:
    Nov 16, 2016
    Posts:
    970
    Here's an example on how to create an event with 2 parameters to help you get on the right track:

    https://docs.unity3d.com/ScriptReference/Events.UnityEvent_2.html

    Given that, there are a couple of approaches on how to convert your EventManager to support events with multiple parameters, although the level of difficulty is up to you -- whether you want it to be truly generic, or whether you want to restrict to known event types (such as a CommunityObject parameter in your example).

    Quick 'n simple solution:

    First thing you'll want to do is:

    Code (csharp):
    1.  
    2. [System.Serializable]
    3. public class CommunityObjectEvent : UnityEvent<CommunityObject>
    4. {
    5. }
    Then you can use CommunityObjectEvent instead of UnityEvent in your event manager.
     
  7. kabuto178

    kabuto178

    Joined:
    Mar 21, 2017
    Posts:
    59
    I really would prefer a truly generic approach, what modifications would i need to make?
     
  8. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,380
    Unity serializer does not support generics (with the exception of List<T>).

    So you'll always have to make a concrete type out of the base generic type.

    Otherwise... no serialization.
     
  9. kabuto178

    kabuto178

    Joined:
    Mar 21, 2017
    Posts:
    59
    Would I have to change my eventsmanager class though to accept this new type in the dictionary?
     
  10. KelsoMRK

    KelsoMRK

    Joined:
    Jul 18, 2010
    Posts:
    5,539
    At a certain point the event needs to know what to pass when it raises, so I mean you could maybe do something like this
    Code (csharp):
    1.  
    2. public abstract class GoofyEvent<T> : UnityEvent<Action<T>> { }
    3.  
    4. public class GoofyStringEvent : GoofyEvent<string> { }
    5.  
    But at the end of the day you still need to know you're passing a string to the listener (and I don't even know if the above would work)
     
  11. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,380
    What is it you're attempting to accomplish?

    Not, how to create a custom UnityAction... but rather, what is the end goal here? What do you need?
     
  12. kabuto178

    kabuto178

    Joined:
    Mar 21, 2017
    Posts:
    59
    Well my real aim is to create an event system where I can pass data around to trigger custom events occurring, maybe passing around an int, string or an object depending on what comes up. So maybe a player collects 5 items, I could just fire an event and that would be routed to the correct component or class without the need to hard cast or find components in the scene. Maybe trigger off the event in multiple components simultaneously as well.
     
  13. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,380
    And how do you expect to connect the listener to the dispatcher?

    Like what do you want the interface of this designed structure to look like?

    Do you connect them up in the inspector at design time? Do you want to be able to staticly access them globally at runtime? What's your programmer user experience desire for this?
     
  14. BlackPete

    BlackPete

    Joined:
    Nov 16, 2016
    Posts:
    970
    Since the dictionary is based on the UnityEvent (no args) type, then yes.

    However, you could try making the EventManager itself a generic class, although I haven't tried it for myself to see how well that'd work in practice. I don't see why that would be an issue, though.

    Something like:

    Code (csharp):
    1.  
    2.    public class EventManager<T>
    3.    {
    4.        [System.Serializable]
    5.        public class EventOneArg : UnityEvent<T> { }
    6.  
    7.        private Dictionary<string, EventOneArg> eventDictionary = new Dictionary<string, EventOneArg>();
    8.  
    9.        private static EventManager<T> eventManager;
    10.  
    11.        public static EventManager<T> Instance
    12.        {
    13.            get
    14.            {
    15.                if (eventManager == null)
    16.                    eventManager = new EventManager<T>();
    17.  
    18.                return eventManager;
    19.            }
    20.        }
    21.  
    22.        public static void StartListening(string eventName, UnityAction<T> listener)
    23.        {
    24.            EventOneArg thisEvent = null;
    25.            if (Instance.eventDictionary.TryGetValue(eventName, out thisEvent))
    26.            {
    27.                thisEvent.AddListener(listener);
    28.            }
    29.            else
    30.            {
    31.                thisEvent = new EventOneArg();
    32.                thisEvent.AddListener(listener);
    33.                Instance.eventDictionary.Add(eventName, thisEvent);
    34.            }
    35.        }
    36.  
    37.        //... etc...
    38.    }
    39.  
    Of course, you'll notice that this no longer derives from MonoBehaviour, so this would only be usable in code. I'm not sure I see a need for this to be serialized though, unless you can elaborate further on what your needs are.

    One downside is this quickly gets weird if you have a static instance for every single argument type. So I'd actually just strip out the static instance, and use an event manager factory class instead to manage multiple event managers depending on what arguments are needed.
     
    Last edited: May 31, 2017
  15. kabuto178

    kabuto178

    Joined:
    Mar 21, 2017
    Posts:
    59
    Oh I see, no need for MonoBehaviour really so that is fine. I do not intend to use this via inspector only through code. It would be sort of like a "Bus System" to trigger internal events within my game, not sure if this is the correct way to go about this or if there is an easier way, but I started to look into delegates and interfaces. I am used to having interfaces for a callback system, however I would need a hard reference for each interface type which I was avoiding. With a callback system being fairly generic I would be able to register to events and trigger events as needed without the statics. If doing static functions is a better approach then I am surely mistaken. Thanks for both of your posts, I will experiment a bit with this code and see.
     
  16. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,380
    So you just need a messaging system where you can just globally scream out "hey, X just happened", and every piece of code that cares about X can respond to it.

    Like, say the player dies, you want a global "HEY THE PLAYER JUST DIED" message to blast out to the entire program.

    And these exist... like this one here on the unify wiki:
    http://wiki.unity3d.com/index.php/Advanced_CSharp_Messenger

    Or this one from unity tutorials:
    https://unity3d.com/learn/tutorials/topics/scripting/events-creating-simple-messaging-system

    Or this outdated one I wrote a long while back that I never use anymore because I don't like messaging systems. But unlike the previous doesn't use strings for the message types because I find strings to be lacking, and it allows for typed parameters through an EventArgs setup similar to .net's Events:
    https://github.com/lordofduct/spacepuppy-unity-framework/blob/master/SpacepuppyBase/Notification.cs

    Although really... all of this can be simplified down into a type safe setup, so that each event has a static name associated with it:

    Code (csharp):
    1.  
    2. public class PlayerDeathNotification
    3. {
    4.    
    5.     public static event System.Action<Player> OnDeath;
    6.    
    7.     public static void Broadcast(Player player)
    8.     {
    9.         if(OnDeath != null) OnDeath(player);
    10.     }
    11.    
    12. }
    13.  
    14. public class GameOverNotification
    15. {
    16.    
    17.     public static event System.Action<float> OnGameOver;
    18.    
    19.     public static void Broadcast(float score)
    20.     {
    21.         if(OnGameOver != null) OnGameOver(score);
    22.     }
    23.    
    24. }
    25.  
    26. public class SomeScript : MonoBehaviour
    27. {
    28.    
    29.     void Start()
    30.     {
    31.         PlayerDeathNotification.OnDeath += this.OnPlayerDeath;
    32.     }
    33.    
    34.     void OnDestroy()
    35.     {
    36.         PlayerDeathNotification.OnDeath -= this.OnPlayerDeath;
    37.     }
    38.    
    39.     void OnPlayerDeath(Player player)
    40.     {
    41.         //do stuff
    42.     }
    43.    
    44. }
    45.  
    Though even then... I don't like global systems either (notice how my previous notification system isn't explicitly global... but yet it is still flawed).
     
  17. BlackPete

    BlackPete

    Joined:
    Nov 16, 2016
    Posts:
    970
    If you don't need serialization, you could probably just use Actions and/or Funcs (if you need a return value) as they're pretty simple to write.

    e.g.

    Code (csharp):
    1.  
    2. // Create an event handler
    3. public event Action<string> OnError;
    4.  
    5. // Add a listener
    6. OnError += error => Debug.LogError(error);
    7.  
    8. // Invoke the action
    9. if (OnError != null) OnError("I just died!");
    10.  
    .... or like what @lordofduct said while I was typing this ;)
     
  18. kabuto178

    kabuto178

    Joined:
    Mar 21, 2017
    Posts:
    59
    I followed the Unity tutorials one initially, however I got stuck at the generics aspect of it, but yes I wanted the script to yell "HEY I JUST HAPPENED" lol that made me laugh for a good bit.
     
  19. kabuto178

    kabuto178

    Joined:
    Mar 21, 2017
    Posts:
    59
    lol thanks again pete, good tips and nice examples. The little snippet you showed me worked as well so I might consider both.
     
    BlackPete likes this.