Search Unity

UnityAction / UnityEvent, parameters and serialization

Discussion in 'Scripting' started by HoloGuy, May 3, 2017.

  1. HoloGuy

    HoloGuy

    Joined:
    Oct 4, 2016
    Posts:
    11
    Hi folks!

    I've got some issues/questions regarding the Unity Event System. What I'm trying to archive, is to be able to bind method calls in the editor on the buttons I created. I made different kinds of toolboxes with buttons consisting of simple cubes. When one of these buttons is pressed, the method defined in the editor window should be called. So far so good. This is easily possible with UnityAction or UnityEvent. But I need to add a sender object to be able to access it later.

    What I tried so far:

    Using UnityAction's Invoke() without any parameters -> is serialized in the editor, but I can't provide the sending object.

    Using UnityAction's Invoke with a parameter of type of the class that is calling the function -> works, but I have to manually drag the object itself in the parameter slot, which is quite ugly.

    Using UnityAction<T>'s Invoke with the object type specified in the template -> should work, but this isn't serialized in the editor.

    Using class definition MyClassEvent : UnityEvent<T> (with serialized attribute, of course) -> should also work, but in the class the Invoke points to, I have to define the calling class as parameter. Looks good. But: This parameter is serialized in the editor again and is not filtered. So its basically the same as No. 2.

    So: What is the right way to do it? I just need to call a function with the sender as object but the function itself should be displayed configured parameterlessly in the editor. Like I "secretly" pass another parameter to it.

    Anyway: Where is the difference between UnityAction and UnityEvent since I can Invoke both of them and add listeners to both of them?
     
    Last edited: May 5, 2017
    a436t4ataf likes this.
  2. HoloGuy

    HoloGuy

    Joined:
    Oct 4, 2016
    Posts:
    11
    Update: I'm now able to invoke the action selected in the inspector window without having to provide it there:

    Code (CSharp):
    1. public UnityEvent Event;
    Code (CSharp):
    1. try
    2. {
    3.     // Invoke target action using reflection
    4.     // Get target object and define arguments
    5.     Object targetObject = Event.GetPersistentTarget(0);
    6.     object[] args = { this };
    7.  
    8.     // Get method and invoke method on object using arguments defined above
    9.     MethodInfo method = targetObject.GetType().GetMethod(Event.GetPersistentMethodName(0));
    10.     method.Invoke(targetObject, args);
    11. }
    12. catch (System.Exception exception)
    13. {
    14.     Debug.LogWarning("Couldn't invoke action. Error:");
    15.     Debug.LogWarning(exception.Message);
    16. }
    17.  
    But there is still the (now unused) field for the selection of the object, which is still quite ugly:
    UnityEvent.PNG

    Any suggestions on this?

    // edit: code formatting
     
  3. laxbrookes

    laxbrookes

    Joined:
    Jan 9, 2015
    Posts:
    235
    I think I understand what you are trying to achieve. I'll share with you my method of implementing callbacks in this way and hopefully it may help.

    Code (csharp):
    1.  
    2. public Button confirmButton;
    3. public InputField inputField;
    4.  
    5. //...some code...
    6.  
    7. public void getUserInput (string question, Action<string> response) {
    8.     confirmButton.onClick.RemoveAllListeners();
    9.     confirmButton.onClick.AddListener ( () => {
    10.         // Callback response
    11.         response (inputField.text);
    12.     }
    13. }
    14.  
    Then in another class...
    Code (csharp):
    1.  
    2. public void promptUserInput () {
    3.     OtherClass.Instance().getUserInput ("Please Enter Your Name", (response) => {
    4.         //Assuming I have a SetName method...
    5.         SetName(response)
    6.     }
    7. }
    8.  
    If that is of no use, I apologise.
     
  4. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    I typically wrap whatever parameters I need in a UnityEngine.Object, ie a MonoBehaviour or ScriptableObject. That way you can simply drag and drop the parameter in the inspector.

    The other option is to simply create another field on the same class that can take the parameters. Its a little hacky, but it works for simple use cases.

    Oh, and remember that the inspector can't display generics. So make sure you inherit from your generic with a specific type.

    Code (CSharp):
    1. // This won't show up in the inspector
    2. public GenericType<T> {
    3.     public T value;
    4. }
    5.  
    6. // But this will
    7. public GenericTypeInt : GenericType<int> {}
     
  5. HoloGuy

    HoloGuy

    Joined:
    Oct 4, 2016
    Posts:
    11
    Thank you for your answers!

    Maybe I couldn't express my issue easily understandable - sorry for that!

    The problem is, that I want to use the serialized UnityEvent or UnityAction type because I think this is a handy way to assign actions to buttons. I also want to pass a parameter, the sender of the action (my button), to the called method. This enables me to implement a callback (e.g. button is highlighted when something is active).

    But as soon as I define the parameter for the callback in the method I want to call, it appears in the inspector as field that wants to be set (see image in my post above). By using reflection, I'm able to bypass this in a way, but this is not very transparent to the user.

    @BoredMormon Yep, I tried that. The problem is still that I have this parameter selection mentioned above because the inspector just looks at my target method with it's parameters and doesn't filter it when this is already defined by the template. Say: When I have a serialized class inheriting from UnityEvent<int> and im calling a method target(int value), why the heck shall I provide this value in the inspector? In my opinion the type used in the template should be filtered or better defined as an additional parameter, not listend in the inspector.

    // edit: Sorry, sorry, sorry! I may have used the term callback wrong: I don't want to pass another function, I just want to pass the object that invoked the function! Maybe this clarifies everything a little bit...
     
    Last edited: May 4, 2017
  6. methos5k

    methos5k

    Joined:
    Aug 3, 2015
    Posts:
    8,712
    You could break it up into 2 parts, as an example.
    Have your Event call a method that then calls the same method with the extra parameter (your button).
     
    Kiwasi likes this.
  7. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,338
    What you're asking for is a serialized method that's assignable in the editor, and that's typed. That would be a Serialized Delegate, I guess? It could look like this:

    Code (csharp):
    1. public class MyButton : MonoBehaviour, IPointerClickHandler  {
    2.  
    3.    [Serializable]
    4.    public class ButtonEvent : SerializedDelegate<MyButton> { }
    5.  
    6.    public ButtonEvent OnClick;
    7.  
    8.    public void OnPointerClick(PointerEventData eventData)
    9.    {
    10.       OnClick.Invoke(this);
    11.    }
    12. }
    I was going to say that writing a propertyDrawer for that SerializedDelegate would require the user to subclass an editor, but I did some hacking around and found a way to create a custom property drawer for generic classes:

    I've always believe that if you have a class Foo<T>, you can't create a PropertyDrawer for Foo that will work for subclasses of Foo, since you can't define that property drawer:

    Code (csharp):
    1. [CustomPropertyDrawer(typeof(Foo<T>))] //Won't compile!
    2. public class FooDrawer : PropertyDrawer {
    3.  
    4. }
    The problem here is that Foo<T> doesn't really exist as it's own thing. If you have IntFoo : Foo<int> and StringFoo : Foo<string>, the common base class of IntFoo and StringFoo is object.

    The way I've seen this problem handled is that you create a FooDrawer that does all the drawing, and then create a IntFooDrawer that inherits from FooDrawer and is the [CustomPropertyDrawer] for IntFoo. That's annoying - especially since you have to do this every time, and the drawer has to go into the editor folder, and so on.


    The trick/hack is to define a dummy, non-generic base class for Foo, and create a property drawer for that:

    Code (csharp):
    1. public abstract class FooBase {}
    2.  
    3. public abstract class Foo<T> { public T t; }
    4.  
    5. public class IntFoo : Foo<int> { }
    6. public class StringFoo : Foo<string { }
    7.  
    8. [CustomPropertyDrawer(typeof(FooBase), true) //true means "also for subclasses"
    9. public class FooDrawer : PropertyDrawer {
    10.     //This is the property drawer for IntFoo and StringFoo!
    11. }
    I might be re-inventing the wheel here, but all instances I've seen of eg. serializable dictionaries requires the user to both subclass the dictionary class, and the dictionary drawer class. This setup allows for skipping subclassing the drawer. It's a bit ugly, and there's a FooBase class around that's kind of a blemish, but it will allow for making more powerful drawers for code plugins.


    I have been writing a UnityEvent replacement (faster, allows for any number of arguments), and this is interesting to me. I could make an attempt at creating the SerializedDelegate as a part of that package, if you're interested. Should not be too hard.
     
    HoloGuy likes this.
  8. HoloGuy

    HoloGuy

    Joined:
    Oct 4, 2016
    Posts:
    11
    @methos5k Yes, this was also one of my first ideas: To overload the function so I have a parameterless function to select in the inspector and which is extracted by reflection and the function I call finally with my stealthy parameter. But then again, I have both functions in the selection menu of the UnityEvent field. Maybe I'm a little bit too much into perfectionism here, but this bothers me...

    @Baste Great, this looks really promising! I'll have a deeper look into it tomorrow. To be able to provide more than one parameter is great! If you don't mind: I'd love to used your enhanced class with a parameter hidden in the inspector (e.g. as a generic)!
     
    Last edited: May 4, 2017
  9. JoshuaMcKenzie

    JoshuaMcKenzie

    Joined:
    Jun 20, 2015
    Posts:
    916
    There are Static versions of the call and then there are Dynamic versions of the same call. you are describing the static version. The static version allows any type of UnityEvent (not just UnityEvent<int>) to call a function that takes a int, string, bool, etc. The dynamic version does what I believe you are expecting (from the quoted sentence, not your initial issue). Look closer that the functions listed in the dropdown and you should see two methods listed "target(int)". One of them static, the other dynamic

    As for your specific question this is something I went over in my post in the UnityEvents thread, create either a payload or memento object and send that. the payload can have a reference to the invoker.
     
  10. HoloGuy

    HoloGuy

    Joined:
    Oct 4, 2016
    Posts:
    11
    Oh, damn. It was there all the time. You are absolutely correct: I meant the dynamic version of the event invocation! I just overlooked the option for the dynamic version because it is above the other items. Used the inherited UnityEvent<T>, waited to see the change between the static versions below and didn't notice the added dynamic version above.
     
  11. edmondkeroro

    edmondkeroro

    Joined:
    Mar 21, 2020
    Posts:
    4
    For Those who want to create a custom event Script:

    I made a collision detection script. The events set in the inspector are called when a collision is detected. Just like a button.



    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using System.Reflection;
    4. using UnityEngine;
    5. using UnityEngine.Events;
    6.  
    7. public class CollisionCallFunc : MonoBehaviour {
    8.    [System.Serializable]
    9.    public class CollisionEvent : UnityEvent<object> {
    10.        public object value;
    11.    }
    12.  
    13.    [SerializeField]
    14.    private CollisionEvent collisionEvents = new();
    15.  
    16.    private void OnCollisionEnter(Collision collision) {
    17.        try {
    18.            collisionEvents.Invoke(collisionEvents.value);
    19.        } catch(System.Exception exception) {
    20.            Debug.LogWarning("Couldn't invoke action. Error:");
    21.            Debug.LogWarning(exception.Message);
    22.        }
    23.    }
    24.    private void OnCollisionEnter2D(Collision2D collision) {
    25.        try {
    26.            collisionEvents.Invoke(collisionEvents.value);
    27.        } catch(System.Exception exception) {
    28.            Debug.LogWarning("Couldn't invoke action. Error:");
    29.            Debug.LogWarning(exception.Message);
    30.        }
    31.    }
    32. }
     
  12. ina

    ina

    Joined:
    Nov 15, 2010
    Posts:
    1,085
    can you pass a string to this