UnityEvents in a Prefab (via Script?)

Right now, I’m instantiating a UI Button, and when it’s clicked, it needs to call a function in another script. Usually I would just set the callback in the inspector, but I can’t do that since the Button is a prefab. How in the world would I do this?

So I don’t use UnityEvent, but instead my own Trigger system. But it’s essentially the same thing as a UnityEvent.

When we need such things… we create what we call ‘ProxyScripts’. Ours are done with reflection, but you can just adhoc it.

Basically the Proxy has the same methods as the script you’re attempting to access, but just forwards them along.

So say you have a script like this:

public class Foo : MonoBehaviour
{
   
    public void DoStuff()
    {
       
    }
   
}

You create a Proxy like this:

public class FooProxy : MonoBehaviour
{
   
    public void DoStuff()
    {
        GameObject.Find("FooContainer").GetComponent<Foo>().DoStuff();
    }
   
}

Note the forwarding hinges on how ‘Foo’ is implemented. If for instance it’s a singleton, you access it as such. In this example I pretend it’s got a specific name. You could also find it by tag, or any other method you use.

Just attach a FooProxy to your prefab somewhere, and have the UnityEvent target that.

Thanks, I thought about using Find(), and I might just do that. But to be sure, is there no way to do this with UnityEvents?

Well UnityEvent needs to have a reference to something.

Since the prefab exists outside of the scene, it can’t have a reference to it.

So no… it’s just like how you couldn’t have a ‘GameObject’ field on a script in a prefab, and reference something in the scene. What would it be referencing?

1 Like

Personally, I would use an “event” ScriptableObject Asset. A scriptableObject that has a private event with functions that let you pass Add/RemoveListeners and Invoke. since this event is a project Asset the button prefab can simply reference it and call invoke. then inScene gameobjects have some monobehaviour that add/removes listeners to that event asset in OnEnable/OnDisable.

Spawn the button and click it. and it’ll invoke the event on the Asset. which some other object is listening to and that object then does it’s actions.

this makes it easy to simply drag and drop which scripts listen to which events

AbstractEventData

    using System.Collections.Generic;
    using System.Linq;

    using UnityEngine;
    using UnityEngine.Events;

    public abstract class AbstractEventData: ScriptableObject
    {
        /// <summary>
        /// Enables Event Calling. If false it prevents any Invokes. does not prevent Add/Remove Listener calls
        /// </summary>
        [SerializeField]protected bool m_invokable = true; //m_Enable is already in use by the scriptableObject
        /// <summary>
        /// Should this Event Log to console when its Used?
        /// </summary>
        [SerializeField]protected bool m_Log = false;

        public bool Invokeable { get { return m_invokable; } set { m_invokable = value; } }
        public bool Log        { get { return m_Log;       } set { m_Log       = value; } }
    }

ActionEventData

    using System.Collections.Generic;
    using System.Linq;

    using UnityEngine;
    using UnityEngine.Events;

    [CreateAssetMenu(menuName = "ScriptableObject Asset/Events/Action")]
    public class ActionEventData : AbstractEventData
    {
        protected UnityEvent actionEvent = new UnityEvent();
  
        virtual protected void OnDisable()
        {
            actionEvent.RemoveAllListeners();

            if (localEvents != null)
                localEvents.Clear();
        }

        #region Global Events
        virtual public void AddListener(UnityAction call)
        {
            #if UNITY_EDITOR
            if(Log) Debug.LogFormat(this, "{2}.AddListener({0}.{1})",call.Target.ToString(), call.Method.Name,name);
            #endif

            actionEvent.AddListener(call);
        }
        virtual public void RemoveListener(UnityAction call)
        {
            #if UNITY_EDITOR
            if(Log) Debug.LogFormat(this, "{2}.RemoveListener({0}.{1})",call.Target.ToString(), call.Method.Name,name);
            #endif

            actionEvent.RemoveListener(call);
        }
        virtual public void Invoke()
        {
            #if UNITY_EDITOR
            if(Log) Debug.LogFormat(this, "{1}.Invoke()\n Enabled:{0}", Invokeable,name);
            #endif


            if(!Invokeable) return;

            actionEvent.Invoke();
        }
        #endregion
   
        #region LocalEvents
        protected Dictionary<GameObject,UnityEvent> localEvents;//lazy initialized

        virtual public void AddListener(GameObject target, UnityAction call)
        {
            if (localEvents == null)
                localEvents = new Dictionary<GameObject, UnityEvent>();

            if(!target)
            {
                //assume target was destroyed, clean local event incase it has any null refs
                Clean();
                #if UNITY_EDITOR
                //target invalid, don't add listener
                if(Log) Debug.LogWarningFormat(this,"{0}.AddListener(): Target passed was null or Destroyed",name);
                #endif
                return;
            }

            UnityEvent localEvent;
            if(!localEvents.TryGetValue(target,out localEvent))
            {
                localEvent = new ActionEvent();
                localEvents[target] = localEvent;
            }

            #if UNITY_EDITOR
            if(Log) Debug.LogFormat(this,"{0}.AddListener(<{1}>{2}.{3})",name,target.name,call.Target.ToString(),call.Method.Name);
            #endif
            localEvent.AddListener(call);
        }

        virtual public void RemoveListener(GameObject target,UnityAction call)
        {

            if (localEvents == null) return;// local events wasn't initalized meaning nothing was added yet, do nothing

            if(!target)
            {
                //assume target was destroyed, clean local event incase it has any null refs
                Clean();
                //Clean should have removed any of Targets limbo bindings
                return;
            }

            UnityEvent localEvent;
            if(localEvents.TryGetValue(target,out localEvent))
            {
                #if UNITY_EDITOR
                if(Log) Debug.LogFormat(this,"{0}.RemoveListener(<{1}>{2}.{3})",name,target.name,call.Target.ToString(),call.Method.Name);
                #endif

                localEvent.RemoveListener(call);
            }

        }

        virtual public void Invoke(GameObject target)
        {

            if (localEvents == null) return;// local events wasn't initalized meaning nothing was added yet, nothing to invoke on

            if(!target)
            {
                //assume target was destroyed, clean local event incase it has any null refs
                Clean();
                //Clean should have removed any of Targets limbo bindings
                return;
            }

            if(!Invokeable) return;

            UnityEvent localEvent;
            if(localEvents.TryGetValue(target,out localEvent))
            {
                #if UNITY_EDITOR
                if(Log) Debug.LogFormat(this,"{0}.Invoke()",name);
                #endif
                localEvent.Invoke();
            }
        }

        virtual protected void Clean()
        {
            if (localEvents == null)
                localEvents = new Dictionary<GameObject, ActionEvent>();

            else
                localEvents = localEvents
                    .Where(l=>(bool)l.Key)
                    .ToDictionary(l=>l.Key,l=>l.Value);

        }
        #endregion

    }

Example MonoBehaviour

public class ExampleScript: MonoBehaviour
{
    [SerializeField]private ActionEvenData OnAction;

    private void OnEnable()
    {
        if((bool)OnAction)
            OnAction.AddListener(SayHelloWorld);
    }

    private void OnDisable()
    {
        if((bool)OnAction)
            OnAction.RemoveListener(SayHelloWorld);
    }

    private void SayHelloWorld()
    {
        Debug.Log("Hello World!");
    }
}
1 Like

Hrmmm, I only quickly scanned your description, but that sounds interesting.

May look into it further when I get back from my dinner party. See how well a design like you describe could fit for my designer. He’s… special… when it comes to code.

1 Like

Yeah I realize that there can be more encapsulated ways of doing things, but I’ve recently gotten into the habit of writing classes in a way that gives non-programmer designers more toys to play with, and this is one of them. Its also why I recently gotten big into working with editor scripting.

I also have a generic version for the generic UnityEvent types, the code really isn’t that much different

public abstract class AbstractEventData<T,U>:AbstractEventData where U : UnityEvent<T>
{
// blah blah blah nearly identical code as ActionEventData
}

well… its different in that I don’t actually use UnityEvent but my own type of custom event classes, but the interaction is completely the same, and UnityEvent’s API is far more universally recognized.

1 Like

Yeah, same here. I wrote my own UnityEvent thing a long while back before UnityEvent came out. Been using it ever since, since it technically has a few bells and whistles that UnityEvent does not.

1 Like

So… I know this thread is old… but I’m trying to get Unity Event initialized through the start of a script.
Isn’t this possible with the default Unity system to this date?