How to create persistent listener to an event?

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.

2 Likes

You can use UnityEditor.Events.UnityEventTools to control persistent events. This class is in the scripting docs.

6 Likes

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.)

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.

1 Like

I’ve managed to create what it is you were after.

UnityAction methodDelegate = System.Delegate.CreateDelegate(typeof(UnityAction), yourComponentInstance, targetInfo) as UnityAction;
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:

targetInfo = UnityEvent.GetValidMethodInfo(yourComponentInstance ,MethodName , MethodParam);
7 Likes

@skalev Do you know how to do this for void parameters? There wasn’t much documentation on what to do in that case.

@Ben-BearFish , I did this a while ago, but looking at the source code, looks like:

GetValidMethodInfo(listener, name, new Type[0]);

should work.

Got it from looking at the Unity C# code, check this out here:

Line 732.

Thank you very much skalev, this worked beautifully. I’ll paste a full code snippet here in case anyone needs it:

using System;
using UnityEngine.Events;
using UnityEditor.Events;
using UnityEngine;

public class EventTest : MonoBehaviour {
    public UnityEvent BigExplosionEvent;
  
    void Start()
    {
        if (BigExplosionEvent == null)
            BigExplosionEvent = new UnityEvent();

        var targetInfo = UnityEvent.GetValidMethodInfo(this, nameof(ExplodeMe), new Type[0]);
        UnityAction methodDelegate = Delegate.CreateDelegate(typeof(UnityAction), this, targetInfo) as UnityAction;
        UnityEventTools.AddPersistentListener(BigExplosionEvent, methodDelegate);
    }

    public void ExplodeMe()
    {
        Debug.Log("I just blew up!");
    }
}
6 Likes

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?


public void RegisterEvents()
{
    // registers persistent listener as "Runtime only"
    UnityEventTools.AddVoidPersistentListener(modulesManager.OnModuleVariantsShowEvent, OnShowModuleVariants);
}

public void OnShowModuleVariants()
{
    Debug.Log("OnShowModuleVariants");

    // ...
}
2 Likes

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:

public void RegisterEvents()
{
    // registers persistent listener as "Runtime only"
    UnityEventTools.AddVoidPersistentListener(modulesManager.OnModuleVariantsShowEvent, OnShowModuleVariants);

    for (var i = 0; i < modulesManager.OnModuleVariantsShowEvent.GetPersistentEventCount(); i++)
    {
        modulesManager.OnModuleVariantsShowEvent.SetPersistentListenerState(i, UnityEventCallState.EditorAndRuntime);
    }
}
public void OnShowModuleVariants()
{
    Debug.Log("OnShowModuleVariants");
    // ...
}

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.

3 Likes

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.

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):

TextMeshProUGUI target = gameObject.GetComponent<TextMeshProUGUI>();
//Register Serialized Event
var setStringMethod = target.GetType().GetProperty("text").GetSetMethod();
var methodDelegate = Delegate.CreateDelegate(typeof(UnityAction<string>), target, setStringMethod) as UnityAction<string>;
UnityEventTools.AddPersistentListener(eventObject.OnUpdateString, methodDelegate);
10 Likes

You literally just saved my afternoon. Thank you so much!

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.

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.

using HurricaneVR.Framework.Core;
using HurricaneVR.Framework.Core.Grabbers;
using System;
using UnityEditor;
using UnityEditor.Events;
using UnityEngine.Events;


[CustomEditor(typeof(NetworkedGrabbable))]
public class NetworkedGrabbableInspector : Editor
{
    private NetworkedGrabbable targetReference;

    private void OnEnable() {
        //Retrieve references from the base class and the Grabbable class
        targetReference = target as NetworkedGrabbable;
        var grabbable = targetReference.GetComponent<HVRGrabbable>();

        //Check if the Grab event in the NetworkedGrabbable class is added as a persistent listener
        bool hasGrabbedEvent = false;
        for (int i = 0; i < grabbable.Grabbed.GetPersistentEventCount() && !hasGrabbedEvent ; i++) {
            if (grabbable.Grabbed.GetPersistentMethodName(i) == nameof(targetReference.Grab))
                hasGrabbedEvent = true;
        }

        //Check if the Release event in the NetworkedGrabbable class is added as a persistent listener
        bool hasReleasedEvent = false;
        for (int i = 0; i < grabbable.Released.GetPersistentEventCount() && !hasReleasedEvent; i++) {
            if (grabbable.Released.GetPersistentMethodName(i) == nameof(targetReference.Release))
                hasReleasedEvent = true;
        }

        //If there is no persistent listener named "Grab" from "NetworkGrabbable", add it.
        if (!hasGrabbedEvent) {
            var methodInfo = UnityEvent.GetValidMethodInfo(targetReference, nameof(targetReference.Grab), new Type[0]);
            var method = Delegate.CreateDelegate(typeof(UnityAction<HVRGrabberBase, HVRGrabbable>), targetReference, nameof(targetReference.Grab)) as UnityAction<HVRGrabberBase, HVRGrabbable>;
            UnityEventTools.AddPersistentListener<HVRGrabberBase, HVRGrabbable>(grabbable.Grabbed, method);
        }

        //If there is no persistent listener named "Release" from "NetworkGrabbable", add it.
        if (!hasReleasedEvent) {
            var methodInfo = UnityEvent.GetValidMethodInfo(targetReference, nameof(targetReference.Release), new Type[0]);
            var method = Delegate.CreateDelegate(typeof(UnityAction<HVRGrabberBase, HVRGrabbable>), targetReference, nameof(targetReference.Release)) as UnityAction<HVRGrabberBase, HVRGrabbable>;
            UnityEventTools.AddPersistentListener<HVRGrabberBase, HVRGrabbable>(grabbable.Released, method);
        }

    }
}

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.

1 Like

Thank you, you are a life saver!