Search Unity

How to create persistent listener to an event?

Discussion in 'UGUI & TextMesh Pro' started by Aithoneku, Aug 24, 2014.

  1. Aithoneku

    Aithoneku

    Joined:
    Dec 16, 2013
    Posts:
    67
    How to create persistent listener to an event?

    When I call Button.onClick.addListener(MyMethod), it won't be persistent and it won't appear in inspector (of course event variable is serialized).

    ---

    Because I'm sure there will be people asking why I need this (and telling me I don't need this), I'm answering in advance: I want to fill some events using reflection and method attributes. For example menu: class MainMenu contains methods for each button, these methods have attribute "MenuItem" and my system just goes through all methods, pick ones with the attribute, instantiate prefab of a button and attach delegate. Everything is working, problem is, however, that, as stated above, it's not persistent.

    Reason why I need it to be persistent is simple - I want to execute whole process only in editor and cachce values in serialized field. Because I don't want to slow down startup of the game.
     
    Last edited: Aug 24, 2014
    tigerleapgorge and hopetolive like this.
  2. Tim-C

    Tim-C

    Unity Technologies

    Joined:
    Feb 6, 2010
    Posts:
    2,225
    You can use UnityEditor.Events.UnityEventTools to control persistent events. This class is in the scripting docs.
     
  3. Aithoneku

    Aithoneku

    Joined:
    Dec 16, 2013
    Posts:
    67
    I see, thank you very much!

    My searching ability are quite bad, so even thought I spent lot of time in the docs, I couldn't find it. (Funny is that I couldn't find it even though I knew the name of the class from you reply until I used file search :3.)
     
  4. Aithoneku

    Aithoneku

    Joined:
    Dec 16, 2013
    Posts:
    67
    I'm sorry, but I still cannot figure out how to use it, to be more precise, how to create correct UnityAction instance.

    For non-persistent listener, all I needed to do is create delegate and then create new UnityAction instance: new UnityEngine.Events.UnityAction(myDelegate); However when I use such UnityAction with AddPersistentListener method, an error appears: "ArgumentException: Could not register callback Invoke on MenuButtonBinder+ButtonClickedDelegate. The class MenuButtonBinder+ButtonClickedDelegate does not derive from UnityEngine.Object". (MenuButtonBinder.ButtonClickedDelegate is my delegate type returning void and taking no argument)

    What it means is quite clear, IMHO - constructor of UnityAction takes the delegate as object and method Invoke as method to be called instead of obtaining the object and MethodInfo from the delegate. I get it. However, how can create correct UnityAction? I know I can do it with new UnityAction(MyMethodName), but as stated above, I'm using reflection, so all I have is object and MethodInfo instance. (Btw. I'm creating the delegate with System.Delegate.CreateDelegate.)

    I cannot find any information about creating the UnityAction except what MonoDevelop auto-complete functionality gives me - and it claims that it takes two arguments, object and IntPtr. However even when I provide these data (I created IntPtr from MethodInfo instance), it won't compile with error that method name is required.
     
    auhfel likes this.
  5. skalev

    skalev

    Joined:
    Feb 16, 2012
    Posts:
    264
    I've managed to create what it is you were after.

    Code (csharp):
    1. UnityAction methodDelegate = System.Delegate.CreateDelegate(typeof(UnityAction), yourComponentInstance, targetInfo) as UnityAction;
    2. UnityEventTools.AddPersistentListener(ActionTarget, methodDelegate);
    Where targetInfo is the MethodInfo object, and yourCopmonentInstance is self explanatory.

    I should point out that I get the method info using:

    Code (csharp):
    1. targetInfo = UnityEvent.GetValidMethodInfo(yourComponentInstance ,MethodName , MethodParam);
     
    konurhan, Calvares, F-N and 4 others like this.
  6. Ben-BearFish

    Ben-BearFish

    Joined:
    Sep 6, 2011
    Posts:
    1,204
    @skalev Do you know how to do this for void parameters? There wasn't much documentation on what to do in that case.
     
  7. skalev

    skalev

    Joined:
    Feb 16, 2012
    Posts:
    264
  8. MaDDoX

    MaDDoX

    Joined:
    Nov 10, 2009
    Posts:
    764
    Thank you very much skalev, this worked beautifully. I'll paste a full code snippet here in case anyone needs it:

    Code (CSharp):
    1. using System;
    2. using UnityEngine.Events;
    3. using UnityEditor.Events;
    4. using UnityEngine;
    5.  
    6. public class EventTest : MonoBehaviour {
    7.     public UnityEvent BigExplosionEvent;
    8.  
    9.     void Start()
    10.     {
    11.         if (BigExplosionEvent == null)
    12.             BigExplosionEvent = new UnityEvent();
    13.  
    14.         var targetInfo = UnityEvent.GetValidMethodInfo(this, nameof(ExplodeMe), new Type[0]);
    15.         UnityAction methodDelegate = Delegate.CreateDelegate(typeof(UnityAction), this, targetInfo) as UnityAction;
    16.         UnityEventTools.AddPersistentListener(BigExplosionEvent, methodDelegate);
    17.     }
    18.  
    19.     public void ExplodeMe()
    20.     {
    21.         Debug.Log("I just blew up!");
    22.     }
    23. }
    24.  
     
  9. atalantus

    atalantus

    Joined:
    Jan 17, 2016
    Posts:
    16
    How would I be able to set the execution property of the registered Persistent Listener from the default "Runtime Only" to "Editor and Runtime" by script to actually be able to invoke the registered events in editor mode?




    Code (CSharp):
    1.  
    2. public void RegisterEvents()
    3. {
    4.     // registers persistent listener as "Runtime only"
    5.     UnityEventTools.AddVoidPersistentListener(modulesManager.OnModuleVariantsShowEvent, OnShowModuleVariants);
    6. }
    7.  
    8. public void OnShowModuleVariants()
    9. {
    10.     Debug.Log("OnShowModuleVariants");
    11.  
    12.     // ...
    13. }
    14.  
     
    SolidAlloy and tigerleapgorge like this.
  10. atalantus

    atalantus

    Joined:
    Jan 17, 2016
    Posts:
    16
    Oh well... going through the UnityEvent class code I found the SetPersistentListenerState method.
    With this I'm able to change the Call States of my Persistent Listeners:

    Code (CSharp):
    1. public void RegisterEvents()
    2. {
    3.     // registers persistent listener as "Runtime only"
    4.     UnityEventTools.AddVoidPersistentListener(modulesManager.OnModuleVariantsShowEvent, OnShowModuleVariants);
    5.  
    6.     for (var i = 0; i < modulesManager.OnModuleVariantsShowEvent.GetPersistentEventCount(); i++)
    7.     {
    8.         modulesManager.OnModuleVariantsShowEvent.SetPersistentListenerState(i, UnityEventCallState.EditorAndRuntime);
    9.     }
    10. }
    11. public void OnShowModuleVariants()
    12. {
    13.     Debug.Log("OnShowModuleVariants");
    14.     // ...
    15. }
    Think it would still be quite a nice addition to be able to set the Call State right away when adding it through
    UnityEventTools.AddPersistentListener
    .
     
    siliconkaiser and SolidAlloy like this.
  11. SolidAlloy

    SolidAlloy

    Joined:
    Oct 21, 2019
    Posts:
    58
    Yes, it would be great. I don't understand why Unity hides persistent listener methods in the UnityEventTools class when UnityEvent has an internal
    void AddPersistentListener(UnityAction call, UnityEventCallState callState)
    that does basically the same and accepts callState as a parameter.
     
  12. d_grass

    d_grass

    Joined:
    Mar 6, 2018
    Posts:
    45
    For my future self and others who will search, if you want to add a dynamic listener (in my case to set strings on TMP from an event):

    Code (CSharp):
    1. TextMeshProUGUI target = gameObject.GetComponent<TextMeshProUGUI>();
    2. //Register Serialized Event
    3. var setStringMethod = target.GetType().GetProperty("text").GetSetMethod();
    4. var methodDelegate = Delegate.CreateDelegate(typeof(UnityAction<string>), target, setStringMethod) as UnityAction<string>;
    5. UnityEventTools.AddPersistentListener(eventObject.OnUpdateString, methodDelegate);
     
  13. Reimirno7

    Reimirno7

    Joined:
    Nov 25, 2021
    Posts:
    51
    You literally just saved my afternoon. Thank you so much!
     
  14. alsharefeeee

    alsharefeeee

    Joined:
    Jul 6, 2013
    Posts:
    80
    Thanks. It works.
    Do you have any idea how to make it stick?
    I am trying to do the same thing you did here but through Editor. This way if I save the scene the Events remain there.
     
  15. AleBraFinGroup

    AleBraFinGroup

    Joined:
    May 17, 2023
    Posts:
    9
    A year late, but I'll add the answer: you need to create an Editor extension class that checks the Persistent Listeners in the base class and adds it if there's no method with its name in it, all by reflection.

    I'll paste part of the code I made.
    Code (CSharp):
    1. using HurricaneVR.Framework.Core;
    2. using HurricaneVR.Framework.Core.Grabbers;
    3. using System;
    4. using UnityEditor;
    5. using UnityEditor.Events;
    6. using UnityEngine.Events;
    7.  
    8.  
    9. [CustomEditor(typeof(NetworkedGrabbable))]
    10. public class NetworkedGrabbableInspector : Editor
    11. {
    12.     private NetworkedGrabbable targetReference;
    13.  
    14.     private void OnEnable() {
    15.         //Retrieve references from the base class and the Grabbable class
    16.         targetReference = target as NetworkedGrabbable;
    17.         var grabbable = targetReference.GetComponent<HVRGrabbable>();
    18.  
    19.         //Check if the Grab event in the NetworkedGrabbable class is added as a persistent listener
    20.         bool hasGrabbedEvent = false;
    21.         for (int i = 0; i < grabbable.Grabbed.GetPersistentEventCount() && !hasGrabbedEvent ; i++) {
    22.             if (grabbable.Grabbed.GetPersistentMethodName(i) == nameof(targetReference.Grab))
    23.                 hasGrabbedEvent = true;
    24.         }
    25.  
    26.         //Check if the Release event in the NetworkedGrabbable class is added as a persistent listener
    27.         bool hasReleasedEvent = false;
    28.         for (int i = 0; i < grabbable.Released.GetPersistentEventCount() && !hasReleasedEvent; i++) {
    29.             if (grabbable.Released.GetPersistentMethodName(i) == nameof(targetReference.Release))
    30.                 hasReleasedEvent = true;
    31.         }
    32.  
    33.         //If there is no persistent listener named "Grab" from "NetworkGrabbable", add it.
    34.         if (!hasGrabbedEvent) {
    35.             var methodInfo = UnityEvent.GetValidMethodInfo(targetReference, nameof(targetReference.Grab), new Type[0]);
    36.             var method = Delegate.CreateDelegate(typeof(UnityAction<HVRGrabberBase, HVRGrabbable>), targetReference, nameof(targetReference.Grab)) as UnityAction<HVRGrabberBase, HVRGrabbable>;
    37.             UnityEventTools.AddPersistentListener<HVRGrabberBase, HVRGrabbable>(grabbable.Grabbed, method);
    38.         }
    39.  
    40.         //If there is no persistent listener named "Release" from "NetworkGrabbable", add it.
    41.         if (!hasReleasedEvent) {
    42.             var methodInfo = UnityEvent.GetValidMethodInfo(targetReference, nameof(targetReference.Release), new Type[0]);
    43.             var method = Delegate.CreateDelegate(typeof(UnityAction<HVRGrabberBase, HVRGrabbable>), targetReference, nameof(targetReference.Release)) as UnityAction<HVRGrabberBase, HVRGrabbable>;
    44.             UnityEventTools.AddPersistentListener<HVRGrabberBase, HVRGrabbable>(grabbable.Released, method);
    45.         }
    46.  
    47.     }
    48. }
    49.  
    50.  

    So, to explain: I have a "Grabbable" class, which has a "Grabbed" event. I want to add a persistent event to it whenever I add the class I made "NetworkedGrabbable".
    To do this, I create an Editor class "NetworkedGrabbableInspector", in which I look for the base class method name (e.g. "Grab" in "NetworkedGrabbable") in the "Grabbed" event of the "Grabbable" class.
    If it's not there, then I'll just create a delegate (with Delegate.CreateDelegate) with the method, and use "AddPersistentListener".

    Pretty straightforward.
     
    alsharefeeee likes this.