Adding Persistent Listeners Through Code

I am working on a project in Unity 2018.4 that has to be exported as an Asset Bundle so I’m fairly restricted in what can be done. I need to add a whole lot of function callback to a whole lot of UnityEvents, and I’d rather not do it by hand, so I’m wondering if it can be done using code.

I want events to set the text property of a Text UI component, so the result should be like this after running the script in Edit Mode:

Using another function as the callback function that sets the property is not an option due to the restrictions.

So far I have come up with the following code:

using System.Collections;
using System.Collections.Generic;
using UnityEngine.UI;
using UnityEngine;
using Klak.Wiring;
using UnityEditor.Events;
using UnityEngine.Events;
using UnityEditor.Profiling;
using System;
using System.Reflection;

public class PrintAdder : MonoBehaviour
{
    public GameObject[] NotePatches = new GameObject[18];
    public GameObject[] Modes = new GameObject[4];
    public bool execute = false;

    static string[] modeStepsArr = { "I (1)", "II (1)", "III (1)", "IV (1)", "V (1)", "VI (1)", "VII (1)"};
    static string[] stepsArr = { "I", "II", "III", "IV", "V", "VI", "VII"};

    [ExecuteInEditMode]
    public void AddListeners()
    {
        if (execute)
        {
            foreach (var note in NotePatches)
            {
                int stepCount = 0;
                foreach (var step in stepsArr)
                {
                    GameObject stepNode = note.transform.FindChild(step).gameObject;
                    EventOut eventOut = stepNode.GetComponent<EventOut>();

                    foreach (var mode in Modes)
                    {
                        GameObject modeStep = mode.transform.FindChild(modeStepsArr[stepCount]).gameObject;
                        EventOut stepNodeEventOut = stepNode.GetComponent<EventOut>();
                        Text modeStepText = modeStep.GetComponent<Text>();
                      
                        eventOut._event.AddListener(() => modeStepText.text = note.name);
                       
                        // DOES NOT WORK
                        //UnityEventTools.AddVoidPersistentListener(eventOut._event, () => modeStepText.text = note.name);
                    }
                    stepCount++;
                }
            }
            execute = false;
        }
    }
}

This has the desired result in terms of functionality after running it during runtime, but I need the listeners to be persistent. I tried the commented out line but unfortunatly that doesn’t work.

To add the PersistentListener I assume I need to create a UnityAction, but I have no idea how to pass a value to the text property’s setter nor how to link it to a VoidEvent.
Any help is greatly appreciated.

For completeness, this is the EventOut component.

namespace Klak.Wiring
{
    [AddComponentMenu("Klak/Wiring/Output/Generic/Event Out")]
    public class EventOut : NodeBase
    {
        #region Editable properties

        [SerializeField]
        public VoidEvent _event;

        #endregion

        #region Node I/O

        [Inlet]
        public void Bang()
        {
            _event.Invoke();
        }

        #endregion
    }
}

And this is the VoidEvent:

[Serializable]
          public class VoidEvent : UnityEvent {}

I’m not a big fan of hooking stuff up like this, especially when things are split between the main project and an asset bundle.

The reason is because it is fragile and dependent on Unity’s serialization. Once you built your asset bundle(s), the rest of the project can silently change in subtle ways and your connections can silently break, leaving you no way to really reason about why things just stopped working.

And then to fix it you have to update all these out-of-project asset bundles. Yuck.

If I understand your use case correctly, for me a preferred pattern is a resource locator (kind of a central register) where the buttons can announce their presence with unique identifiers, then others can find those buttons and add themselves as listeners.

By doing the register / unregister in OnEnable() / OnDisable() then looking for the buttons much later, such as in Start() (or even lazily in Update() until you find what you want), you will end up with a system that is robust and resistant to order-of-operation changes.

I use this pattern a great deal with additive scene loading: load the common scenes, then stream in the content scene, which might be really large, while all the common scenes (UI, player, enemy manager) quietly sit in a busy idle waiting for the content scene to appear and register its spawn points and capabilities. Making Start() an IEnumerator and then asserting a boolean ready = true; once everybody gets found really works nicely.

I understand your point, but that is all beyond my control. I’m creating something using GitHub - teenageengineering/videolab for a synthesizer companion app that allows users to insert their own scenes, primarily for MIDI-reactive visuals.
It might be quite unorthodox but that’s what I’m working with for this project.

I found the solution:

MethodInfo targetMethod = modeStepText.GetType().GetProperty("text").GetSetMethod();
Type actionType = typeof(UnityAction<string>);
var targetAction = Delegate.CreateDelegate(actionType, modeStepText, targetMethod);
UnityEventTools.AddStringPersistentListener(eventOut._event, (UnityAction<string>)targetAction, note.name);
5 Likes