Search Unity

UnityEvent, where have you been all my life?

Discussion in 'Scripting' started by JoeStrout, Apr 24, 2015.

?

Did you know about UnityEvent before this?

  1. No! Thank you! This is so cool!

    35.5%
  2. No, but I still don't see what's the big deal.

    3.0%
  3. Yes, but I didn't realize how cool they were.

    25.3%
  4. Yes, but I still don't see what's the big deal.

    11.3%
  5. Yes, and I use them all the time. Where have you been?!?

    24.8%
  1. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    8,131
    To some extent, this is the inherent price in decoupling. If code A calls code B directly, then sure, it's easy to see what code A is calling... but it's also coupled to code B, with all the maintenance/refactoring problems that implies. If you decouple them — by whatever means you prefer — then maintenance, refactoring, and reuse are easier. But of course it means that looking at code A, you don't know what it might actually be invoking in any given situation.

    However, @Flipbookee says that the next version of Script Inspector 3 will be able to find (and replace!) all UnityEvent references to a method in the scene. That makes them not any harder to trace and modify than public methods called directly.

    Of course I do agree there's potential for misuse, as with anything else. But in my projects, it has certainly made things more maintainable, not less.
     
    angrypenguin, Flipbookee and Kiwasi like this.
  2. Flipbookee

    Flipbookee

    Joined:
    Jun 2, 2012
    Posts:
    2,379
    To be honest, debugging of events and logic which depends on their order knows to be complicated quite often. But Unity is an incredibly extensible engine/editor and that makes it so beautifully magical :cool: you guys just gave me an idea how to improve tracking and debugging of events and issues related with that! :) Imagine if there was a button you can turn on for all the events to get recorded with time, order of firing, and full call stacks - wouldn't that help solving issues? :) I think it will and I think I can make that button :D just hang on, let me test a few things first and I'll let you know...
     
    Kiwasi and JoeStrout like this.
  3. JoshuaMcKenzie

    JoshuaMcKenzie

    Joined:
    Jun 20, 2015
    Posts:
    817
    I actually have an abstract Command class (derived from scriptableObject) that can hook commands up via the inspector. one such command is my LogMessageCommand. so if I ever need to figure out an event chain I can just plop that into the chain and when the log prints out I can follow its stack trace.

    Also nearly all of My UnityEvents (specifically ones that are meant to be "global" events) are also scriptable objects so I can drag and drop which event each component binds to or invokes for. For Example, I can have a BoolEvent scriptable object named OnPause that my Pause controller invokes and my Pause Menu binds to. I don't need to worry so much about race conditions since OnPause is a scriptableObject asset and should always exist (and run OnEnable) before anything in any scene can use it. so my pause menu doesn't need to wait on the PauseController to initalize (much less even reference it directly) to bind to the OnPause event. UnityEvents as SO's actually makes things super decoupled and makes it super easy to swap out which component is using which event. I can even go so far and extend the class to add logging (which components are listening, which are invoked) in one place if I ever need to
     
    BTStone, Flipbookee and angrypenguin like this.
  4. Flipbookee

    Flipbookee

    Joined:
    Jun 2, 2012
    Posts:
    2,379
    Okay folks, my UnityEvents Tracer seems to work nicely! :) It currently supports UnityEvents in MonoBehaviours only, but it could be expanded with support for ScriptableObjects as well in a similar way. If you wish to give it a try, copy this code and paste it in a C# script inside an Editor folder:
    Code (csharp):
    1. using UnityEngine;
    2. using UnityEngine.Events;
    3. using UnityEditor;
    4. using System.Collections;
    5. using System.Collections.Generic;
    6. using System.Reflection;
    7.  
    8. class UnityEventsTracer
    9. {
    10.     static MethodInfo addMethodInfo = typeof(UnityEventBase).GetMethod("AddListener", BindingFlags.NonPublic | BindingFlags.Instance);
    11.     static MethodInfo removeMethodInfo = typeof(UnityEventBase).GetMethod("RemoveListener", BindingFlags.NonPublic | BindingFlags.Instance);
    12.  
    13.     abstract class LoggerBase
    14.     {
    15.         public UnityEventBase unityEvent;
    16.         public MonoBehaviour source;
    17.         public string propertyPath;
    18.      
    19.         protected abstract MethodInfo LogMethodInfo { get; }
    20.      
    21.         public void Register()
    22.         {
    23.             addMethodInfo.Invoke(unityEvent, new object[] { this, LogMethodInfo });
    24.         }
    25.      
    26.         public void Unregister()
    27.         {
    28.             removeMethodInfo.Invoke(unityEvent, new object[] { this, LogMethodInfo });
    29.         }
    30.     }
    31.  
    32.     class Logger : LoggerBase
    33.     {
    34.         static readonly MethodInfo logMethodInfo = typeof(Logger).GetMethod("Log");
    35.         protected override MethodInfo LogMethodInfo { get { return logMethodInfo; } }
    36.      
    37.         public void Log()
    38.         {
    39.             Debug.LogFormat(source, "{0} => {1} ()", source, propertyPath);
    40.         }
    41.     }
    42.  
    43.     class Logger<T0> : LoggerBase
    44.     {
    45.         static readonly MethodInfo logMethodInfo = typeof(Logger<T0>).GetMethod("Log");
    46.         protected override MethodInfo LogMethodInfo { get { return logMethodInfo; } }
    47.      
    48.         public void Log(T0 arg0)
    49.         {
    50.             Debug.LogFormat(source, "{0} => {1} ( {2} )", source, propertyPath, arg0);
    51.         }
    52.     }
    53.  
    54.     class Logger<T0, T1> : LoggerBase
    55.     {
    56.         static readonly MethodInfo logMethodInfo = typeof(Logger<T0, T1>).GetMethod("Log");
    57.         protected override MethodInfo LogMethodInfo { get { return logMethodInfo; } }
    58.      
    59.         public void Log(T0 arg0, T1 arg1)
    60.         {
    61.             Debug.LogFormat(source, "{0} => {1} ( {2}, {3} )", source, propertyPath, arg0, arg1);
    62.         }
    63.     }
    64.  
    65.     class Logger<T0, T1, T2> : LoggerBase
    66.     {
    67.         static readonly MethodInfo logMethodInfo = typeof(Logger<T0, T1, T2>).GetMethod("Log");
    68.         protected override MethodInfo LogMethodInfo { get { return logMethodInfo; } }
    69.      
    70.         public void Log(T0 arg0, T1 arg1, T2 arg2)
    71.         {
    72.             Debug.LogFormat(source, "{0} => {1} ( {2}, {3}, {4} )", source, propertyPath, arg0, arg1, arg2);
    73.         }
    74.     }
    75.  
    76.     class Logger<T0, T1, T2, T3> : LoggerBase
    77.     {
    78.         static readonly MethodInfo logMethodInfo = typeof(Logger<T0, T1, T2, T3>).GetMethod("Log");
    79.         protected override MethodInfo LogMethodInfo { get { return logMethodInfo; } }
    80.      
    81.         public void Log(T0 arg0, T1 arg1, T2 arg2, T3 arg3)
    82.         {
    83.             Debug.LogFormat(source, "{0} => {1} ( {2}, {3}, {4}, {5} )", source, propertyPath, arg0, arg1, arg2, arg3);
    84.         }
    85.     }
    86.  
    87.     static readonly char[] propertyPathSeparators = { '.', '[', ']' };
    88.     const BindingFlags instanceFlags = BindingFlags.Instance | BindingFlags.Public
    89.     | BindingFlags.NonPublic | BindingFlags.DeclaredOnly;
    90.  
    91.     static Dictionary<int, Dictionary<string, LoggerBase>> loggers = new Dictionary<int, Dictionary<string, LoggerBase>>();
    92.  
    93.     [MenuItem("Tools/UnityEvents Tracer/Disable", false, 2)]
    94.     static void DisableTracing()
    95.     {
    96.         foreach (var kv in loggers)
    97.         {
    98.             foreach (var kvLogger in kv.Value)
    99.                 kvLogger.Value.Unregister();
    100.             kv.Value.Clear();
    101.         }
    102.     }
    103.  
    104.     [MenuItem("Tools/UnityEvents Tracer/Enable", false, 1)]
    105.     static void EnableTracing()
    106.     {
    107.         DisableTracing();
    108.      
    109.         var allBehaviours = Resources.FindObjectsOfTypeAll<MonoBehaviour>();
    110.         foreach (var item in allBehaviours)
    111.         {
    112.             var so = new SerializedObject(item);
    113.             for (var sp = so.GetIterator(); sp.NextVisible(true); )
    114.             {
    115.                 if (sp.propertyType != SerializedPropertyType.Generic)
    116.                     continue;
    117.              
    118.                 var unityEventBase = AsUnityEventBase(sp);
    119.                 if (unityEventBase == null)
    120.                     continue;
    121.              
    122.                 if (unityEventBase is UnityEvent)
    123.                 {
    124.                     Dictionary<string, LoggerBase> loggersByPath;
    125.                     if (!loggers.TryGetValue(item.GetInstanceID(), out loggersByPath))
    126.                         loggers.Add(item.GetInstanceID(), loggersByPath = new Dictionary<string, LoggerBase>());
    127.                  
    128.                     var propertyPath = sp.propertyPath.Replace(".Array.data[", "[");
    129.                  
    130.                     LoggerBase logger;
    131.                     if (!loggersByPath.TryGetValue(propertyPath, out logger))
    132.                     {
    133.                         logger = new Logger {
    134.                             unityEvent = unityEventBase,
    135.                             source = item,
    136.                             propertyPath = propertyPath
    137.                         };
    138.                         loggersByPath.Add(propertyPath, logger);
    139.                     }
    140.                  
    141.                     logger.Register();
    142.                     continue;
    143.                 }
    144.              
    145.                 var type = unityEventBase.GetType();
    146.                 while (type.BaseType != typeof(UnityEventBase))
    147.                     type = type.BaseType;
    148.              
    149.                 var genericArgs = type.GetGenericArguments();
    150.                 if (genericArgs.Length >= 1 && genericArgs.Length <= 4)
    151.                 {
    152.                     Dictionary<string, LoggerBase> loggersByPath;
    153.                     if (!loggers.TryGetValue(item.GetInstanceID(), out loggersByPath))
    154.                         loggers.Add(item.GetInstanceID(), loggersByPath = new Dictionary<string, LoggerBase>());
    155.                  
    156.                     var propertyPath = sp.propertyPath.Replace(".Array.data[", "[");
    157.                  
    158.                     LoggerBase logger;
    159.                     if (!loggersByPath.TryGetValue(propertyPath, out logger))
    160.                     {
    161.                         System.Type loggerType = null;
    162.                         switch (genericArgs.Length)
    163.                         {
    164.                         case 1:
    165.                             loggerType = typeof(Logger<>).MakeGenericType(genericArgs);
    166.                             break;
    167.                         case 2:
    168.                             loggerType = typeof(Logger<,>).MakeGenericType(genericArgs);
    169.                             break;
    170.                         case 3:
    171.                             loggerType = typeof(Logger<,,>).MakeGenericType(genericArgs);
    172.                             break;
    173.                         case 4:
    174.                             loggerType = typeof(Logger<,,,>).MakeGenericType(genericArgs);
    175.                             break;
    176.                         }
    177.                         logger = System.Activator.CreateInstance(loggerType) as LoggerBase;
    178.                         logger.unityEvent = unityEventBase;
    179.                         logger.source = item;
    180.                         logger.propertyPath = propertyPath;
    181.                      
    182.                         loggersByPath.Add(propertyPath, logger);
    183.                     }
    184.                  
    185.                     logger.Register();
    186.                     continue;
    187.                 }
    188.             }
    189.         }
    190.     }
    191.  
    192.     static UnityEventBase AsUnityEventBase(SerializedProperty property)
    193.     {
    194.         FieldInfo targetField = null;
    195.         object targetObject = property.serializedObject.targetObject;
    196.         var fieldNames = property.propertyPath.Split(propertyPathSeparators, System.StringSplitOptions.RemoveEmptyEntries);
    197.         for (var i = 0; targetObject != null && i < fieldNames.Length; ++i)
    198.         {
    199.             if (fieldNames[i] == "Array")
    200.             {
    201.                 targetField = null;
    202.              
    203.                 var array = targetObject as IList;
    204.                 if (array == null)
    205.                     break;
    206.                 var index = int.Parse(fieldNames[i += 2]);
    207.                 if (index >= array.Count)
    208.                     break;
    209.                 targetObject = array[index];
    210.             }
    211.             else
    212.             {
    213.                 for (var type = targetObject.GetType(); type != typeof(object); type = type.BaseType)
    214.                 {
    215.                     targetField = type.GetField(fieldNames[i], instanceFlags);
    216.                     if (targetField != null)
    217.                         break;
    218.                 }
    219.                 if (targetField == null)
    220.                 {
    221.                     targetObject = null;
    222.                     break;
    223.                 }
    224.                 targetObject = targetField.GetValue(targetObject);
    225.             }
    226.         }
    227.         return targetObject as UnityEventBase;
    228.     }
    229. }
    230.  
    It's a bit longish, maybe non-fully-optimal, and maybe logging could be improved a little bit, but it works for all UnityEvent types - the non-generic one and all of the 4 generic variations. :cool:

    To enable or disable tracing, select Enable or Disable from the main menu under Tools -> UnityEvents Tracer. Note that this will only work for game objects that exist at the time of enabling, so it won't work for any dynamically spawned game objects after that. :p This is because on enabling the script finds all MonoBehaviour instances, scans them for serialized UnityEvent fields, and then adds a non-persistent callback to each of them that logs the UnityEvent with the parameters it was invoked.

    Let me know how this works for you... :)
     
    khaled24 likes this.
  5. Flipbookee

    Flipbookee

    Joined:
    Jun 2, 2012
    Posts:
    2,379
    So, did anyone check this script out?
     
  6. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    8,131
    Sorry, somehow I didn't get a notification. Looks neat! I haven't tried it yet, but I'm curious: will it work for custom (concrete) event types? (I frequently use my own StringEvent, IntEvent, FloatEvent, BoolEvent, and ObjectEvent concrete event classes.)
     
  7. Flipbookee

    Flipbookee

    Joined:
    Jun 2, 2012
    Posts:
    2,379
    Yes! :cool: It works with everything, even with custom concrete event classes derived from 2, 3, and 4 generic type arguments.

    I forgot to mention that you can enable tracing again after spawning some game objects dynamically to get those objects traced. If that's a common practice then I can extend this script with a let's say RegisterForEventsTracing method to be called after instantiating such game objects.

    And one more thing, this script is only tracing events triggered in the Editor game, so not on standalone builds. ;)
     
    JoeStrout likes this.
  8. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    8,131
    That's super cool.

    I personally haven't ever felt the need for this — that is, I've never found myself in a situation where I was confused about what/how my public methods were getting called via events. But I can certainly imagine that happening, so I'm going to remember this is here.

    Or, if this functionality were to someday appear in my favorite code editor... perhaps under the Script Inspector 3 menu... why, then I'd always have it if I needed it! :D
     
    Flipbookee likes this.
  9. rpgw

    rpgw

    Joined:
    Jul 3, 2012
    Posts:
    41
    Superpig pointed me at this thread today at lunch. I thought I might be alone in setting up projects like this.
    I have a lot of interesting looking stuff to read about here!

    This approach certainly has some flaws but I really like using it a lot of the time as adding new functionality and testing out new logic is so easy. For what it is worth I have attached a screengrab of one of my prefabs that has a bunch of inspector based logic. I have ended up adding a lot of scripts onto separate gameobjects as children. I'm sure this isn't ideal for performance, but it certainly makes it easier to understand what is going on in an 'Entity'. It also makes it easy to copy some new behaviour into another prefab. Just copy/paste the gameobjects and re-assign the references.

    Capture.JPG

    As a rule I -never- have events linking to other gameobjects outside of the prefab (or entity as I define it). The event references are always internal. External communication is always via messages. I'm using some scripts I wrote that utilise the excellent Event Dispatcher asset for all inter-entity and game-wide communication. https://www.assetstore.unity3d.com/en/#!/content/12715 - A Unity event can send a new message as a parameter which is defined as a scriptableObject that contains the message information. Other scripts listen for those messages (referenced using the same scriptableObject) as defined in a list.

    One downside to this setup? If I have made a bunch of separate prefabs that all contain similar logic but I need to make a change , then I have to go through them all and alter them one by one. I know from experience that this can be pretty bad so I am trying to figure out ways of avoiding this.
     
  10. ashley

    ashley

    Joined:
    Nov 5, 2011
    Posts:
    84
    Just looking at this again for a project I'm working on and got a few queries, which should hopefully be checking my understanding rather than 'hey this guy hasn't read this thread at all!'. Excuse my relative lack of intelligence when it comes to the programming side of things, I'm still fairly new to it all.

    Am I correct in believing you can't pass in more than one parameter? I have tried with a simple debug method asking for two strings, but it's not appearing as a valid method to call.

    How does it work with custom classes? Tried with some random ones I already have and I can't figure out why it is working with some but not others. I have one that is a simple class with a few parameters and a constructor and that works fine, but if I create another class (in the same file as my debugging testing if that makes a difference) it isn't offering the associated method (with that class as a parameter) I want to call.

    Also, from what I can see it wants an existing created instance of that class? I can't just use it to get around the one parameter limitation mentioned above by creating an instance on the fly, can I?

    This may be a more general question, but is there a preferred method for indicating that the event that is being invoked has finished? Is there some kind of way I can fire back an 'on complete' kind of message so the think invoking the event knows the event has been invoked and completed successfully?

    Let me know if any of this doesn't make sense!
     
  11. LeftyRighty

    LeftyRighty

    Joined:
    Nov 2, 2012
    Posts:
    5,148
    ashley likes this.
  12. ashley

    ashley

    Joined:
    Nov 5, 2011
    Posts:
    84
    Thanks for that. I was thinking of it at the 'receiving' end rather than the 'invoking' end.

    Quick check - if I wanted one event to require a string and an int and another that required two strings I'd need to create two different event types that overwrite the default, right?
     
  13. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    8,131
    That's right. One parameter only, and only certain basic types (most notably missing Enum params), alas.

    I'm not sure. I didn't think classes as parameters ever worked, through the Inspector at least (you can do considerably more for events you hook up in code, as @LeftyRighty pointed out, but there's also not as much point to that — you could use an ordinary delegate instead).

    No, there's no work-around there as far as I can see. But you may have stumbled onto something I wasn't aware of!

    No need — .Invoke is an ordinary method call; when that call returns, all the events being invoked have returned. There is no sense in which the could be "not finished" unless they explicitly start a coroutine or some other extended process, which it would be up to you to define.

    So, for example, I have a bunch of helpers now that do things like fade in/out a CanvasGroup, or move a transform from one place to another. These are public methods and so can be invoked from events. But they also declare their own events, which they invoke when they start or finish doing their thing. So, you can hook them up as event senders or receivers however you might need.

    Best,
    - Joe
     
    lermy3d and ashley like this.
  14. JoshuaMcKenzie

    JoshuaMcKenzie

    Joined:
    Jun 20, 2015
    Posts:
    817
    Correct its a limitation of of how the BaseUnityEvent class and its inspector class were implemented. You can invoke UnityEvents that take upto 4 parameters, but you can't serialize a handler for more than two in the inspector. The Inspector script only stores one copy of a Int serialized property, one copy of a Boolean SP, one copy of a String, one float, and one UnityObject type (also some target assembly and invocation types).

    If you start on pointing to a static call of GameObject.SetActive the bool SP will pop up and save what value you give it. Then you change the handler to call some function that takes an int it'll default to 0. and finally you change it again to some monobehaviour.enabled and you notice that it'll show the bool value, the same boolean value you used for that GameObject.SetActive. theres only One Bool Serialized property per handler and its just recycled around, so how would the inspector draw a function that takes two or more booleans? simple fact is that it doesn't (and not just for the inspector reason, but also how UnityBaseEvent was implemented)

    as said before it has a serialized property for a UnityObject, any custom classes you want to drop into a field will need to derive from a UnityObject, and that most likely means your custom classes will derive from a Monobehaviour or a ScriptableObject.

    More accurately, Unity actually stores a GUID to represent object types so basically any non-primitive datatype (as in not a bool, float, int, or string) must have meta file that represents it (if its an asset) or be a UnityObject instance in a scene for you to be able to drag and drop it into a UnityEvent inspector. it needs a GUID and only meta files (i.e. assets) and in-scene instances have valid GUIDs this is also why to see that every ScriptableObject and Monobehaviour class needs its own file with the same name as the class so that unity can generate an appropriate meta file for it.

    Yes the Instance needs to pre-exist at editor time for you to use a Static UnityEvent call. Unity needs to serialize the data that it plans to send and it can't do that if that data doesn't exist at editor time. Dynamic UnityEvent calls however can use data thats generated at runtime to be passed. The means that the responsibility for defining that data is moved from the level designer (who would fill that data in the inspector) to the programmer (to prepare that data and invoke it through the function).

    Another way around the parameter limit is to write a "container" class that holds all the data as a packet and passes it as one parameter. These container classes will be ScriptableObjects and I typically call them payloads (their entire purpose is to transfer data from one place to another) or mementos (transfer data, gather results, and provide results to caller). similar to a pass by reference, you pass the mementos out and let all the listeners use the data, the listeners then can accumulate their results back into the memento and after the invoke call the caller still has a reference to the memento object and can see the results from all the listeners without knowing what is actually listening to the event.

    Using a Payload/Memento object also allows for extensibility. say you got 40 classes that are listening for a payload and work beautifully. but now a new class comes up and wants to use that event... but needs slightly more information. what you can do is extend the Payload class to implement a new interface that supports that info. Then caller would then prepare the extra info and then pass as normal. the 40 classes work as before, completely ignoring the new data it doesn't need, while the new listener looks for the extra interface (otherwise acts as if it wasn't called). its a pretty SOLID workflow.

    UnityEvents are primarily run on the Main Thread so the moment you call Invoke() the very next line won't run until all the listeners are done.and since its run on the main thread all the listeners will run in the order they are added. If there are listeners that weren't set up properly (such as you have a an event's inspector list a target object but no function to call, or the target object was destroyed but the handler wasn't cleaned up) the event will skip those as they are invalid invocations.

    you can be sure if you run Invoke(), the "invoke" part will run correctly and the calls will be sent to the valid listeners, , On the other hand,its not invokers responsibility to check that the listeners are working correctly (thats the point of using events in the first place) instead the invoker must continue to work perfectly regardless if a listener failed because its meant to be decoupled from its listeners. A call will be sent out to the listeners, but by default you can't tell if all the listeners completed successfully. However if you use that Memento trick I mentioned before so the listeners can report that they failed.
     
    Last edited: Mar 20, 2017
    MGGDev, lermy3d, ashley and 1 other person like this.
  15. ashley

    ashley

    Joined:
    Nov 5, 2011
    Posts:
    84
    Thanks for the very detailed responses above.

    I've got the overall gist but will probably have to come back and re-read a few more times to fully grasp all of it. There's something about all the technical semantics that I can't seem to keep in my brain (why isn't there a Duolingo for programming? :p)

    I did understand the bit about knowing if an invoke is finished though and that all sounds fine. I wasn't looking to do anything particularly complicated anyway so it sounds like it will all be fine.

    Thanks again :)
     
    JoeStrout likes this.
  16. richardboegli

    richardboegli

    Joined:
    Dec 28, 2016
    Posts:
    13
    @JoeStrout I was having some problems with getting events working and I stumbled upon this thread via Google.
    After reading part way thru your post, something clicked as to what I was doing wrong and I was able to fix my code.

    THANKS!
     
    angrypenguin, Cromfeli and JoeStrout like this.
  17. lermy3d

    lermy3d

    Joined:
    Mar 16, 2014
    Posts:
    101
    You can use as many parameters you want actually!!

    You guys might probably want to check out this thread: Custom UI Event System revamped

    Enjoy!
     
  18. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    12,023
    I thought it was up to 4?
     
  19. lermy3d

    lermy3d

    Joined:
    Mar 16, 2014
    Posts:
    101
    Noup, did you tested the addon from the link?

    Check out the examples in the screenshots, you will love it! :):cool:
     
  20. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    12,023
    No, because this thread isn't about plain old UnityEvent, not your addon.
     
    Cromfeli likes this.
  21. lermy3d

    lermy3d

    Joined:
    Mar 16, 2014
    Posts:
    101
    I never said it was using UnityEvent.
     
  22. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    8,131
    Right, but I think what @angrypenguin is trying to say is, this thread is about UnityEvent. Discussion of other systems that are not UnityEvent should probably stay in other threads, since this one has become a bit of a reference for Unity's UnityEvent system.
     
    Cromfeli and angrypenguin like this.
  23. lermy3d

    lermy3d

    Joined:
    Mar 16, 2014
    Posts:
    101
    For sure. I totally agree. Just wanted to help out ashley with the UnityEvent's current limitations.
     
  24. samKeene

    samKeene

    Joined:
    Jan 20, 2013
    Posts:
    8
    Hey everyone, I've been trying to figure out a way of creating an event manager that uses UnityEvents and takes a param, but the param can be of any type. So sometimes I want to pass in an int as an argument, and sometimes I want to pass in a Vector3, or whatever. What I've built so far is based on the "Events: Creating a Simple Messaging System" tutorial on the Unity website, but I've added a HashTable as an argument. The thing I like about using a HashTable is that it allows for values that aren't strictly typed, so I can insert any value. I'd prefer to be able to pass through a loosely typed argument directly but this seems like a pretty good workaround to me.

    What do you guys think of this pattern? Is there another way of doing this that doesn't require wrapping arguments in a HashTable value?
    I don't want to hook anything up in the Unity editor, all of my code and objects are being created dynamically. I also know this can be done with delegates, but I don't want to use delegates. I want to use UnityEvents. All of the examples I've seen so far only allow parameters of a single fixed type, this doesn't seem very useful to me for building a robust eventSystem that may need to iterate through dozens of different events with different types and different needs. This is what I have:

    Code (CSharp):
    1. using UnityEngine.Events;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4.  
    5. /*
    6. Unity C# Event manager using UnityEvents and a Hashtable for loosely typed params with event.
    7. This EventManager expands the usefulness of UnityEvents by allowing values of any type to be passed as a
    8. parameter in the Event eg: int, string, Vector3 etc.
    9.  
    10. Usage:
    11.  
    12. // Add Listener for Event
    13. MyEventManager.StartListening ("MY_EVENT", MyEventHandlerMethodName);
    14.  
    15. // Trigger Event:
    16. MyEventManager.TriggerEvent ("MY_EVENT", new Hashtable(){{"MY_EVENT_KEY", "valueOfAnyType"}});
    17.  
    18. // Pass null instead of a Hashtable if no params
    19. MyEventManager.TriggerEvent ("MY_EVENT", null);
    20.  
    21. // Handler
    22. private void HandleTeleportEvent (Hashtable eventParams){
    23.     if (eventParams.ContainsKey("MY_EVENT")){
    24.         // DO SOMETHING
    25.     }
    26. }
    27.  
    28. */
    29.  
    30. public class MyEvent : UnityEvent <Hashtable> {}
    31.  
    32. public class MyEventManager : MonoBehaviour {
    33.  
    34.     private Dictionary <string, MyEvent> eventDictionary;
    35.  
    36.     private static MyEventManager eventManager;
    37.  
    38.     //    SINGLETON
    39.     public static MyEventManager instance
    40.     {
    41.         get
    42.         {
    43.             if (!eventManager)
    44.             {
    45.                 eventManager = FindObjectOfType (typeof (MyEventManager)) as MyEventManager;
    46.  
    47.                 if (!eventManager)
    48.                 {
    49.                     Debug.LogError ("There needs to be one active EventManger script on a GameObject in your scene.");
    50.                 }
    51.                 else
    52.                 {
    53.                     eventManager.Init ();
    54.                 }
    55.             }
    56.  
    57.             return eventManager;
    58.         }
    59.     }
    60.  
    61.     void Init ()
    62.     {
    63.         if (eventDictionary == null)
    64.         {
    65.             eventDictionary = new Dictionary<string, MyEvent>();
    66.         }
    67.     }
    68.  
    69.     public static void StartListening (string eventName, UnityAction<Hashtable> listener)
    70.     {
    71.         MyEvent thisEvent = null;
    72.         if (instance.eventDictionary.TryGetValue (eventName, out thisEvent))
    73.         {
    74.             thisEvent.AddListener (listener);
    75.         }
    76.         else
    77.         {
    78.             thisEvent = new MyEvent ();
    79.             thisEvent.AddListener (listener);
    80.             instance.eventDictionary.Add (eventName, thisEvent);
    81.         }
    82.     }
    83.  
    84.     public static void StopListening (string eventName, UnityAction<Hashtable> listener)
    85.     {
    86.         if (eventManager == null) return;
    87.         MyEvent thisEvent = null;
    88.         if (instance.eventDictionary.TryGetValue (eventName, out thisEvent))
    89.         {
    90.             thisEvent.RemoveListener (listener);
    91.         }
    92.     }
    93.  
    94.     public static void TriggerEvent (string eventName, Hashtable eventParams = default(Hashtable))
    95.     {
    96.         MyEvent thisEvent = null;
    97.         if (instance.eventDictionary.TryGetValue (eventName, out thisEvent))
    98.         {
    99.             thisEvent.Invoke (eventParams);
    100.         }
    101.     }
    102.  
    103.     public static void TriggerEvent (string eventName)
    104.     {
    105.         TriggerEvent (eventName, null);
    106.     }
    107. }
     
  25. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    12,023
    Yes, just use the C# base type 'object'. Every type in C# derives from object, so if you want a "typeless" parameter this is the way to do it.

    Keep in mind that for value types this will cause boxing, and therefore allocation.

    Using a Hashtable just for this seems like overkill to me. First, it's lack of type safety comes from the above - it internally stores things as 'object' references, so if that's all you want then you can do it yourself trivially. Secondly, you're creating a whole collection and forcing people to use a collection even if they just want to pass a single value. I would only use a collection if you explicitly want to pass more than one piece of data. Finally, as per the name Hashtables apply hashing to speed up certain operations, which comes with an overhead that's likely not needed just for a parameter passing solution. If you want to pass a collection then I suspect others will better suit this use case.

    Using UnityEvent<T1...,T4> you can pass up to four strongly typed parameters as a part of an event.

    So far I haven't needed more than that. If you look at .NET event standards the first parameter attached to all events is the sender object. This is a really useful convention, because anything that is publicly accessible on the sender doesn't have to be included in the event - the receiver can look it up at will. I generally use the other three available parameters to either share any otherwise private data relevant to the event, or to better describe the change that took place (eg: an "OnDamaged" event will have a reference to the "Health" component that recorded the damage, and maybe old health, new health and amount of damage. The new health value could be looked up easily, but the other bits don't get stored and thus couldn't be looked up, but might be useful to anything that cares about when health values change).

    Can you give an example of where this is a problem for you? I can't think of general purpose examples of situations where I'd recommend throwing away type safety.

    At the end of the day, there's some finite number of ways in which your program uses the data sent with your events. Planning that out in advance is pretty helpful, and representing it in your event types means your IDE and the compiler can help you with it (eg: you don't have to roll your own type safety on a per-receiver basis...).

    Also keep in mind that where you do need to have case-specific additional data for some events you can easily use something like the EventArgs pattern that .NET uses. Instead of having loads of different event types with different parameters (or collections of who knows what data that you need to inspect on the fly), you could have a base ThingEventArgs class/struct with the common stuff, and ThingSpecificEventArgs derived types for the special cases where extra data is needed... and again the built-in tools help you.
     
  26. samKeene

    samKeene

    Joined:
    Jan 20, 2013
    Posts:
    8
    Hey AngryPenguin thanks for the reply.
    I totally agree it doesn't feel right, hence why I asked the question. I come from an iOS dev background, I'm not an expert in C# paradigms (yet ;) ), so I think perhaps I'm missing something. In Apple/iOS dev we have the native NSNotificationCenter. When you post a notif you can send an optional dictionary in the userInfo field. It can contain anything you want and it's incumbent on the developer to do the type checking at the other end. I was borrowing from this pattern because it's super simple and I couldn't find any other clear solutions in Unity.
    I only need one param, but with my EventManager I want the param's type to be agnostic.
    I want my event manager to handle potentially dozens of different types of events per game/app: on a teleport event I want to pass through a Vector3 position, on a hit event I might want to include a health value as an int, AND I want the EventManager to be reusable enough that I can drag and drop it into any new project I have. I'm happy to do the type checking manually. The EventManager shouldn't need to know about the value type of the parameter that is being passed through it, and I'm not sure how to get around this with any of the examples I've seen. For instance, in my code example from above line 69:
    Code (CSharp):
    1. public static void StartListening (string eventName, UnityAction<Hashtable> listener)
    2.     {
    How can I write this code so that it is agnostic to the type of param that is being passed through via the UnityAction without the <HashTable>? Should I replace this with Object? In the example you suggested I could have up to 4 strongly typed params, would those types have to be hard coded into my EventManager? This would mean having a separate EventManager for every different event.

    Does it make sense what I'm asking? I'd love to see some examples of a Unity event manager / system similar to the one in this tutorial on the Unity web site but sends parameters with the event and is reusable.

    Again I feel like I might be missing something and it's probably because I come from an iOS Objective C background, so I'm probably going about this the wrong way. Thanks for your feedback. I'm going to try using "object", although I seem to remember trying that a while back and having issues type casting from, say, Vector3 to Object and then back again to Vector3 at the other end, but I'll let you know how it goes.
     
  27. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    4,221
    Why are you using UnityEvents for this?

    UnityEvent allows you to serialize events in the scene. That's what they're good for. But the syntax is worse than delegates, and they execute a lot slower than delegates.

    Note: the "creating a messaging system" tutorial is a horribly misguided mess. I have no idea what Unity were thinking when they made it.
     
  28. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    6,646
    I've seen a few different 'notification system' out there.

    Like this one (js, pretty simple):
    http://wiki.unity3d.com/index.php?title=NotificationCenter

    Or this one (cs, has stronger typing):
    http://wiki.unity3d.com/index.php/Advanced_CSharp_Messenger

    I even wrote my own several years back (cs, more complicated):
    https://github.com/lordofduct/spacepuppy-unity-framework/blob/master/SpacepuppyBase/Notification.cs

    I wrote it in response to the fact I saw several notification systems (probably from ios devs coming to unity or something), and thought, "hey, that sounds useful, that's implement one for ourselves".

    Thing is... I stopped using it years back, almost immediately after creating it.

    The very loose coupling felt dirty, and it was hard to track code across a scene with it easily. A couple things in my framework rely on it still as I haven't updated those parts... but when I say a couple... I literally mean like 2 places (i think my SpawnPool and CharacterMovementController, which aren't even public).

    See... my beef with these notification systems is that there's very few 'arbitrary' messages I would listen for. Usually I would be listening for very specific messages from very specific objects... or from a global point. And well delegate/events are far better for that. I have an 'OnGameStart' event? Well a static event on my Game class works far better. Is faster, more expressive, and I know all the places that access it just by searching for who touches that event. Events also worked better on individual objects as well... I have a more direct reference to it anyways.

    The only place that I didn't like events was that you couldn't easily reference them in the inspector (this was also before UnityEvent as well). And my partner really wanted a way to hook into events/messages like this.

    I created components that ran code on these notifications, which could be easy since we could use 'Type' or 'string' for them... but knowing what objects were dispatching what events was cumbersome... and my partner refused to use it.

    Which is why I then created my Trigger class:
    https://github.com/lordofduct/space...lob/master/SpacepuppyBase/Scenario/Trigger.cs
    And the rest of the T's & I's:
    https://github.com/lordofduct/spacepuppy-unity-framework/tree/master/SpacepuppyBase/Scenario

    Which was essentially UnityEvent, but before UnityEvent existed.

    And wiring scene events together became easier.

    The notification system went the way of the dodo for me...

    And I don't really see why anyone would want to use a notification system over UnityEvent (or Trigger) and standard C# delegates/events.

    I personally prefer my Trigger system over UnityEvent though... I've added a lot more robust features to it than UnityEvent even still offers.
     
    Last edited: Mar 31, 2017
  29. samKeene

    samKeene

    Joined:
    Jan 20, 2013
    Posts:
    8
    Thanks for the responses peeps, I agree and disagree.
    I guess I incorrectly assumed that a class in Unity, called UnityEvents, that was implemented to solve all the issues around SendMessage, could possibly be used to build a robust and performant eventSystem ;)
    True, looks like I'll build it using delegates. I really wanted UnityEvents to work for me, the name has a ring to it.
    I didn't watch the tut, just appropriated the code. Looks like a pretty standard implementation of the Observer Pattern to me.
    I agree in part, I'll roll my own with delegates, or use one that's already out there. I disagree with you RE the usefulness of a notification system. It definitely doesn't solve all problems but correctly used the observer pattern is a powerful weapon in any developers arsenal. It's perfect for broadcasting loosely coupled global messages across a system.

    Thanks again for the responses.
    -S
     
  30. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    6,646
    The observer pattern is extremely powerful. I didn't say it wasn't.

    I just meant that events/delegates, as well as UnityEvent/Trigger, are a much better implementation of the observer pattern than a string tethered global notification system.
     
    angrypenguin likes this.
  31. gamesjust007

    gamesjust007

    Joined:
    Jan 3, 2017
    Posts:
    8
    Excellent stuff and awesomely helpful. I think Unity has decided not to properly introduce and let people know about some good things like this "Unity Events" & Asset Bundles.
    Just like unity events, there is no proper tutorial for "Asset Bundles..." :(
     
  32. Gditoro

    Gditoro

    Joined:
    May 21, 2017
    Posts:
    1
    Guys, if I want to make a "singleton" pattern persistent "Global messaging system" should I stick to delegates and events or should I use UnityEvent and addListeners()?
     
  33. ghost99

    ghost99

    Joined:
    Jul 12, 2013
    Posts:
    6
    Anyone have luck with UnityEvent and get, set accessors? Trying to use UnityEvent and Interface to loosely couple monobehaviour classes. Can't just put UnityEvent in an Interface. It works in Interface with get, set accessors. The implentation compiles using accessors but blows-up in runtime with a stack overflow. I think on just the set.

    Doesn't seem to be any online documentation about UnityEvent accessors, UnityEvent in an interface.
     
  34. JoshuaMcKenzie

    JoshuaMcKenzie

    Joined:
    Jun 20, 2015
    Posts:
    817
    UnityEvents work just fine with interfaces and within properties.

    My guess is that your getter is trying to access itself rather than its backing variable. A relatively common mistake with getters and StackOverflowExceptions. Keeping your properites written in Pascal Case and your fields (including backing fields) in camel case, as is the typical convention, should help to make this obvious.
     
    JoeStrout likes this.
  35. teknic

    teknic

    Joined:
    Oct 18, 2012
    Posts:
    29
    Here's a more surgical tracing option using a pass-through method.

    Code (CSharp):
    1.     [System.Serializable]
    2.     public class Send : UnityEvent
    3.     {
    4.         public new void Invoke()
    5.         {
    6.             base.Invoke();
    7. #if TRACE_EVENTS
    8.             for (int i=0; i< GetPersistentEventCount(); i++)
    9.             {
    10.                 Debug.Log(GetType() + " <i>called</i> <color=cyan><b>" + GetPersistentMethodName(i) + "()</b></color> <i>in:</i> " + GetPersistentTarget(i) + "\n", GetPersistentTarget(i));
    11.             }
    12. #endif
    13.         }
    14.     }
    then in a MonoBehaviour:

    Code (CSharp):
    1. public Send Message;
    2.  
    3. void Start()
    4. {
    5.   Message.Invoke();
    6. }
    This also works with the other UnityEvent shapes (UnityEvent<T0>, UnityEvent<To, T1>, etc.)
    be sure to define TRACE_EVENTS in each script or in the Scripting Define Symbols in Player Settings. This could also be wrapped in #if UNITY_EDITOR to prevent the logging overhead in a build.
     
    Last edited: Feb 2, 2018
  36. teknic

    teknic

    Joined:
    Oct 18, 2012
    Posts:
    29
    And here's an example of a zero arg callback which accepts an optional delay parameter (this was discussed previously in the thread):

    Code (CSharp):
    1.     [System.Serializable]
    2.     public class Send : UnityEvent
    3.     {
    4.         bool trace = true;
    5.         MonoBehaviour mono;
    6.  
    7.         public void Register(MonoBehaviour m, bool trace = true)
    8.         {
    9.             this.trace = trace;
    10.             mono = m;
    11.         }
    12.  
    13.         public new void Invoke()
    14.         {
    15.             Execute();
    16.         }
    17.  
    18.         public void Invoke(float delay)
    19.         {
    20.             mono.StartCoroutine(DelayedInvoke(delay));
    21.         }
    22.  
    23.         void Execute()
    24.         {
    25.             base.Invoke();
    26.  
    27. #if TRACE_EVENTS
    28.             if (trace)
    29.             {
    30.                 for (int i = 0; i < GetPersistentEventCount(); i++)
    31.                 {
    32.                     Debug.Log("<color=cyan><b>" + GetPersistentMethodName(i) + "()</b></color> <i>called in:</i> " + GetPersistentTarget(i) + "\n", GetPersistentTarget(i));
    33.                 }
    34.             }
    35. #endif
    36.         }
    37.  
    38.         IEnumerator DelayedInvoke(float delay)
    39.         {
    40.             yield return new WaitForSeconds(delay);
    41.             Execute();
    42.         }
    43.     }
    In a MonoBehaviour:

    Code (CSharp):
    1. public Send Message;
    2. void Start()
    3. {
    4.   Message.Register(this); // register event
    5.   Message.Invoke(3); // invoke in 3 seconds
    6. }
    This also supports an optional trace parameter, in case tracing is not desired.
     
    Last edited: Feb 2, 2018
    khaled24 likes this.
  37. JoshuaMcKenzie

    JoshuaMcKenzie

    Joined:
    Jun 20, 2015
    Posts:
    817
    The "GetPersistent" calls only focus on the Listeners that were serialized (shown via the inspector). They won't show anything about the listeners which were added dynamically at runtime. The "GetPersistent" methods you have access to are there primarily to allow the inspector to show what object/methods you have linked up. In other words, this tracing code isn't going to show any information that the inspector is not already showing.

    In order to trace runtime listeners you would have to use reflection and access "UnityEventBase.m_Calls" to reach that UnityEvent's InvokableCallList. and from there use reflection to access its List<BaseInvokeableCall> m_RuntimeCalls (for ALL listeners use m_ExecutingCalls instead). THEN for each executing call you'd need to access its private event UnityAction Delegate again with reflection just so you can log out that Delegate's MethodName and calling Object (which for runtime listeners may or may not be a UnityObject).

    or you can do what I did and rebuild UnityEvents from the ground up and add a feature that would show information on the runtime listeners in the inspector.

    ...or a much easier way is just checking the inspector for the serialized listeners and asking your IDE to search for all code referencing your UnityEvent for the runtime listeners....
     
  38. ghost99

    ghost99

    Joined:
    Jul 12, 2013
    Posts:
    6
    OK. I just copied interface definition into the class implementation. It worked: compiled, ran w/o overflow, event invoked, listener handled event. I assume accessor auto-gen works in the built-in c# editor,

    Thanks but...I've got several questions about UnityEvents : concepts, fundamentals, advantages/disadvantages, coupling, testing and details. Is there a holistic reference specific to Unity?
     
  39. ghost99

    ghost99

    Joined:
    Jul 12, 2013
    Posts:
    6
    UnityEvents are better than System.events but can UnityEvents be tested for memory leakage?
     
  40. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    8,131
    I dunno about holistic reference. This thread is as good a place to discuss them (though we do have a couple of people who occasionally use it to discuss something else). What are your questions?

    I don't know what that means. Can you elaborate?
     
  41. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    6,646
    System.events aren't a thing.

    What you might mean is C# events, where 'event' is an access modifier for delegates.

    And I wouldn't say they're necessarily better... I'd say they're different, because they have very different uses. I'd say they're better in the context of Unity Components and editor time development.

    And what do you mean by tested for "memory leakage"? Unregister your events, and you should avoid any potential references getting stuck and not collected. Otherwise... if you mean *actual* memory leakage... well that's up to Unity to develop them appropriately.
     
  42. teknic

    teknic

    Joined:
    Oct 18, 2012
    Posts:
    29
    Good catch. I suppose one could create some wrapper code for subscribing/unsubscribing to UnityEvents, which would allow tracing of both "persistent" and dynamically assigned targets. Or just roll your own, as you have.
     
  43. phobos2077

    phobos2077

    Joined:
    Feb 10, 2018
    Posts:
    54
    Can anyone help me understand this? Somebody on this thread said a while back that
    But at the same time the official documentation states this:
    If I understand correctly this states that UnityEvents contain strong references to the invocation targets and thus should prevent them from being garbage collected. What am I missing here?

    Related question. Does UnityAction provide any additional features or is it just the same old C# delegate?
     
    Last edited: Jul 22, 2018
  44. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    8,131
    What you're missing is the two different ways of hooking up a UnityEvent. If you hook it up in the Inspector, it is a weak reference, and will all get cleaned up properly when the object is destroyed, with no action on your part. But if you hook it up in code, then it's a strong reference, and you must remember to unhook it in code. Typically people add the event handler in Activate, and remove it in Deactivate.

    (But this is one reason I most often hook things up in the Inspector whenever possible.)
     
    Kiwasi likes this.
  45. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    4,221
    Is it really a weak reference, in the proper meaning of the term? In that the C# side of the referenced object can get garbage collected even in you have a UnityEvent targeting it?
     
  46. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    8,131
    Under the hood? I'm not sure. But the point is, when the GameObject is destroyed and all the Components are torn down, those event handler references (that were made in the IDE) get cleaned up automatically for you. But if you assigned the handlers in code, then they don't.
     
  47. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,464
    No. Its not a true weak reference. Rather its a magical self aware reference.
     
  48. JoshuaMcKenzie

    JoshuaMcKenzie

    Joined:
    Jun 20, 2015
    Posts:
    817
    Actually they don't get cleaned up, For UI flow reasons, the invalid serialized versions never get cleaned up, just skipped. If it did clean up the fields for a handler, that handler would reset every-time you'd try to hook it up (since setting up a serialized listener takes multiple UI actions, and the invocation can't be valid until all those actions are done). I found that out when I was making my own type of Serialized Event class.

    The runtime listeners have the same deal, they don't get cleaned up unless you specifically remove the listener.

    Targets are serialized via a (guid+fileID) group so serialization-wise it is a weak reference. But when a UnityEvent.Invoke() is called and it is dirty it rebuilds up the needed invocation list (e.g. the valid delegates to call) from its Persistent Listeners and Runtime Listeners lists. So the Target field on these delegates will be a strong reference. A UnityEvent is considered dirty after it is deserialized or when any Add/Remove Listener is called.

    So if the Target is destroyed, UnityEvent will still have a reference to it regardless if it is persistent or not (destroying the target doesn't set UnityEvent dirty) the handlers is simply skipped to avoid MissingReferenceExceptions, but it still holds onto it.

    This means that the target can't be cleaned up by the garbage collector until the unity event itself is destroyed/reserialized (for persistent) or the offending listener is removed (for runtime). Do note that UnityObjects are just proxys, the actual data is on C++ and that does get cleaned up. Its just the proxy that can't get cleaned up.

    The biggest feature with UnityEvents is that its a serialized way to set up which components can listen to the event. this allows you to take the context of how your classes interact with each other out of the code itself and into the inspector, allowing you to keep your code more generalized and decoupled.
     
    Last edited: Jul 23, 2018
    phobos2077, Flipbookee and JoeStrout like this.
  49. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    6,646
    I believe they were asking about 'UnityAction':
    https://docs.unity3d.com/ScriptReference/Events.UnityAction.html

    Which is a plane old C# delegate.
     
    phobos2077 and JoshuaMcKenzie like this.
  50. JoshuaMcKenzie

    JoshuaMcKenzie

    Joined:
    Jun 20, 2015
    Posts:
    817
    Oh right I read that differently, yeah UnityAction is just a delegate declaration. In fact UnityAction is no different than System.Action, just that some unity functions, mostly UnityEvent, expect UnityAction.

    but its nothing special and you can cast UnityAction back and forth with System.Action just fine.
     
    phobos2077 likes this.