5

I am looking for c# delegate version of this Manager using UnityEvent. I don't want to use this because UnityEvent is slower than C# event at most time.

Any clue on how to implement this?

Programmer
  • 121,791
  • 22
  • 236
  • 328
weijia_yu
  • 965
  • 4
  • 14
  • 31

3 Answers3

22

You can use Action which is actually a delegate declared like this:

namespace System
{
    public delegate void Action();
}

1.Replace all the UnityAction with Action from the System namespace which uses delegates.

2.Replace all thisEvent.AddListener(listener); with thisEvent += listener;

3.Replace all thisEvent.RemoveListener(listener); with thisEvent -= listener;

Here is the modified version of Unity's original EventManager ported to use delegate/Action.

Without Parameter:

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class EventManager : MonoBehaviour
{

    private Dictionary<string, Action> eventDictionary;

    private static EventManager eventManager;

    public static EventManager instance
    {
        get
        {
            if (!eventManager)
            {
                eventManager = FindObjectOfType(typeof(EventManager)) as EventManager;

                if (!eventManager)
                {
                    Debug.LogError("There needs to be one active EventManger script on a GameObject in your scene.");
                }
                else
                {
                    eventManager.Init();
                }
            }

            return eventManager;
        }
    }

    void Init()
    {
        if (eventDictionary == null)
        {
            eventDictionary = new Dictionary<string, Action>();
        }
    }

    public static void StartListening(string eventName, Action listener)
    {
        Action thisEvent;
        if (instance.eventDictionary.TryGetValue(eventName, out thisEvent))
        {
            //Add more event to the existing one
            thisEvent += listener;

            //Update the Dictionary
            instance.eventDictionary[eventName] = thisEvent;
        }
        else
        {
            //Add event to the Dictionary for the first time
            thisEvent += listener;
            instance.eventDictionary.Add(eventName, thisEvent);
        }
    }

    public static void StopListening(string eventName, Action listener)
    {
        if (eventManager == null) return;
        Action thisEvent;
        if (instance.eventDictionary.TryGetValue(eventName, out thisEvent))
        {
            //Remove event from the existing one
            thisEvent -= listener;

            //Update the Dictionary
            instance.eventDictionary[eventName] = thisEvent;
        }
    }

    public static void TriggerEvent(string eventName)
    {
        Action thisEvent = null;
        if (instance.eventDictionary.TryGetValue(eventName, out thisEvent))
        {
            thisEvent.Invoke();
            // OR USE instance.eventDictionary[eventName]();
        }
    }
}

Test script:

The test script below test the event by triggering events every 2 seconds.

public class TestScript: MonoBehaviour
{
    private Action someListener;

    void Awake()
    {
        someListener = new Action(SomeFunction);
        StartCoroutine(invokeTest());
    }

    IEnumerator invokeTest()
    {
        WaitForSeconds waitTime = new WaitForSeconds(2);
        while (true)
        {
            yield return waitTime;
            EventManager.TriggerEvent("test");
            yield return waitTime;
            EventManager.TriggerEvent("Spawn");
            yield return waitTime;
            EventManager.TriggerEvent("Destroy");
        }
    }

    void OnEnable()
    {
        EventManager.StartListening("test", someListener);
        EventManager.StartListening("Spawn", SomeOtherFunction);
        EventManager.StartListening("Destroy", SomeThirdFunction);
    }

    void OnDisable()
    {
        EventManager.StopListening("test", someListener);
        EventManager.StopListening("Spawn", SomeOtherFunction);
        EventManager.StopListening("Destroy", SomeThirdFunction);
    }

    void SomeFunction()
    {
        Debug.Log("Some Function was called!");
    }

    void SomeOtherFunction()
    {
        Debug.Log("Some Other Function was called!");
    }

    void SomeThirdFunction()
    {
        Debug.Log("Some Third Function was called!");
    }
}

With Parameter:

From other questions, most people are asking how to support parameter. Here it is. You can use class/struct as parameter then add all the variables you want to pass into the function inside this class/struct. I will use EventParam as an example. Feel free to add/remove variables you want to pass in the event EventParam structure at the end of this code.

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class EventManager : MonoBehaviour
{

    private Dictionary<string, Action<EventParam>> eventDictionary;

    private static EventManager eventManager;

    public static EventManager instance
    {
        get
        {
            if (!eventManager)
            {
                eventManager = FindObjectOfType(typeof(EventManager)) as EventManager;

                if (!eventManager)
                {
                    Debug.LogError("There needs to be one active EventManger script on a GameObject in your scene.");
                }
                else
                {
                    eventManager.Init();
                }
            }
            return eventManager;
        }
    }

    void Init()
    {
        if (eventDictionary == null)
        {
            eventDictionary = new Dictionary<string, Action<EventParam>>();
        }
    }

    public static void StartListening(string eventName, Action<EventParam> listener)
    {
        Action<EventParam> thisEvent;
        if (instance.eventDictionary.TryGetValue(eventName, out thisEvent))
        {
            //Add more event to the existing one
            thisEvent += listener;

            //Update the Dictionary
            instance.eventDictionary[eventName] = thisEvent;
        }
        else
        {
            //Add event to the Dictionary for the first time
            thisEvent += listener;
            instance.eventDictionary.Add(eventName, thisEvent);
        }
    }

    public static void StopListening(string eventName, Action<EventParam> listener)
    {
        if (eventManager == null) return;
        Action<EventParam> thisEvent;
        if (instance.eventDictionary.TryGetValue(eventName, out thisEvent))
        {
            //Remove event from the existing one
            thisEvent -= listener;

            //Update the Dictionary
            instance.eventDictionary[eventName] = thisEvent;
        }
    }

    public static void TriggerEvent(string eventName, EventParam eventParam)
    {
        Action<EventParam> thisEvent = null;
        if (instance.eventDictionary.TryGetValue(eventName, out thisEvent))
        {
            thisEvent.Invoke(eventParam);
            // OR USE  instance.eventDictionary[eventName](eventParam);
        }
    }
}

//Re-usable structure/ Can be a class to. Add all parameters you need inside it
public struct EventParam
{
    public string param1;
    public int param2;
    public float param3;
    public bool param4;
}

Test script:

public class Test : MonoBehaviour
{
    private Action<EventParam> someListener1;
    private Action<EventParam> someListener2;
    private Action<EventParam> someListener3;

    void Awake()
    {
        someListener1 = new Action<EventParam>(SomeFunction);
        someListener2 = new Action<EventParam>(SomeOtherFunction);
        someListener3 = new Action<EventParam>(SomeThirdFunction);

        StartCoroutine(invokeTest());
    }

    IEnumerator invokeTest()
    {
        WaitForSeconds waitTime = new WaitForSeconds(0.5f);

        //Create parameter to pass to the event
        EventParam eventParam = new EventParam();
        eventParam.param1 = "Hello";
        eventParam.param2 = 99;
        eventParam.param3 = 43.4f;
        eventParam.param4 = true;

        while (true)
        {
            yield return waitTime;
            EventManager.TriggerEvent("test", eventParam);
            yield return waitTime;
            EventManager.TriggerEvent("Spawn", eventParam);
            yield return waitTime;
            EventManager.TriggerEvent("Destroy", eventParam);
        }
    }

    void OnEnable()
    {
        //Register With Action variable
        EventManager.StartListening("test", someListener1);
        EventManager.StartListening("Spawn", someListener2);
        EventManager.StartListening("Destroy", someListener3);

        //OR Register Directly to function
        EventManager.StartListening("test", SomeFunction);
        EventManager.StartListening("Spawn", SomeOtherFunction);
        EventManager.StartListening("Destroy", SomeThirdFunction);
    }

    void OnDisable()
    {
        //Un-Register With Action variable
        EventManager.StopListening("test", someListener1);
        EventManager.StopListening("Spawn", someListener2);
        EventManager.StopListening("Destroy", someListener3);

        //OR Un-Register Directly to function
        EventManager.StopListening("test", SomeFunction);
        EventManager.StopListening("Spawn", SomeOtherFunction);
        EventManager.StopListening("Destroy", SomeThirdFunction);
    }

    void SomeFunction(EventParam eventParam)
    {
        Debug.Log("Some Function was called!");
    }

    void SomeOtherFunction(EventParam eventParam)
    {
        Debug.Log("Some Other Function was called!");
    }

    void SomeThirdFunction(EventParam eventParam)
    {
        Debug.Log("Some Third Function was called!");
    }
}
Programmer
  • 121,791
  • 22
  • 236
  • 328
  • What extra is UnityEvent doing that causes it to be slower? Basically, what are we trading away to acheive more speed by using `Action` and a custom event manager? – Scott Chamberlain Feb 11 '17 at 20:20
  • @ScottChamberlain Memory allocation + too much use of reflection. The only advantage of it is that you can make it public in the Editor and assign events to it from the Editor since it is serialized. It fails at everything else and should not be used a real game. You can test it yourself or you can check [this](https://www.reddit.com/r/Unity3D/comments/35sa5h/unityevent_vs_delegate_event_benchmark_for_those/#bottom-comments) and [this](http://jacksondunstan.com/articles/3335). – Programmer Feb 11 '17 at 20:29
  • Hey, @Programmer redirected from [here](https://stackoverflow.com/questions/44903534/could-unity-script-listen-to-events-and-active-self-at-certain-circumstance) and I want to know is there a way to pass parameters to the listener by this implementation? – armnotstrong Jul 04 '17 at 10:56
  • I implemented this last night, but noticed it only informs 1 listener. e.g. If you have a second class with EventManager.StartListening("Destory", SomeFourthFunction);, then calling TriggerEvent("Destroy"); will only inform 'SomeThirdFunction' and 'SomeFourthFunction' never gets called. Anyone know a way around this? – Charlie S Jul 05 '17 at 14:38
  • @CharlieSeligman That's a typo in your part. *"Destory"* `!=` *"Destroy"*. Notice the spelling difference. Please create a new question with an example of what you did and what's not working if you still have problems. – Programmer Jul 05 '17 at 23:08
  • Good spot. The typo is only in the above comment (correctly spelt in the code). Just weird it only informs 1 of the 2 listeners. Anyone know why? – Charlie S Jul 07 '17 at 08:38
  • Like I said, ask a new question. Not a good idea to spam old question. – Programmer Jul 07 '17 at 08:47
  • @Programmer - but my question relates to the solution provided here so surely it is more relevant to query an issue with the solution where the solution is provided? – Charlie S Jul 11 '17 at 10:17
  • You have to create a new question and provided the exact code you have tried that is not working. Link to this question too. You can't post that code in the comment section. Create a new question then link this to show that you are having problems with an existing answer. – Programmer Jul 11 '17 at 10:21
  • Hello, @Programmer :) Your `EventManager` works like a charm. Thank you very much. However, I noticed that it can only work with `Actions` that _take no parameters_. I coded the change to accommodate this myself and overloaded the `TriggerEvent()` method with extra params... The problem is.. I used `DynamicInvoke()` to make that happen. O.o And DynamicInvoke is not really ideal and can be considered "hacky". Do you have any suggestions on how the usage of `DynamicInvoke` can be improved? Sorry, I got so many questions. :( – ReGaSLZR Aug 10 '17 at 15:13
  • Well at-least you modified it yourself instead of expecting me to do it for you like other people I tried to help with this. Can you provide me a link to the modified version? I will take a look at it. – Programmer Aug 10 '17 at 15:21
  • 1
    Ah, it's no problem now. I've made a workaround by creating a separate dictionary for events with parameters. You can take a look on my codes here: sta.sh/226znk8jknx3 \n However, I've encountered another issue. :( When I trigger an event with two or more subscribers on it, only one of the subscribers receives the notification. I'm still browsing some other links on how to solve this... I'll drop by here again when I've got updates on this. ^_^ but if you've got a solution, please tell me, huhu. :'( **P.S.** I noticed that @CharlieSeligman has the same issue as me. – ReGaSLZR Aug 14 '17 at 03:35
  • @Zarashi99 - yeah afraid this accepted answer has the bug that we both spotted. I have had to go back to the original less performant code as a result. – Charlie S Aug 15 '17 at 07:37
  • 1
    @CharlieSeligman and @Zarashi99 ...Found and fixed the `EventManager` bug. Can you both test and notify me on this? **Also if anyone wants to make this take a parameter, the easiest way to do this is to make it take a `class` or `struct` as a parameter. You can then declare all the variables you want to pass to the event inside that class/struct. This makes this easier to re-use since all you have to do is modify the class/struct to add/remove parameter from it.** – Programmer Aug 15 '17 at 16:49
  • +1 Thanks, @Programmer :) The `EventManager` is working fine now with multiple subscribers. Also, yeah, that's the workaround I implemented on my game; I use an EventParam class. Now, I'm using this `EventManager` class on my game and classes are reactive. Architecting my game's scripts just became so much easier. – ReGaSLZR Aug 22 '17 at 02:36
  • 1
    @Zarashi99 You are welcome. I noticed you mentioned DynamicInvoke in one of your comments. Don't use that. I just added parameter support to the answer since most people have asked me this on other questions. Check that out. Happy coding! – Programmer Aug 22 '17 at 07:19
5

!! Accepted answer is not complete !!

As a lazy programmer I simply copied what Programmer had written, but ran into the same problem people in the comment section ran into.

Programmer's solution does not work for multiple subscribers to the same event.

This is the fix (same changes for parameters version):

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System;

public class EventManager : MonoBehaviour
{

    private Dictionary<string, Action> eventDictionary;

    private static EventManager eventManager;

    public static EventManager instance
    {
        get
        {
            if (!eventManager)
            {
                eventManager = FindObjectOfType(typeof(EventManager)) as EventManager;

                if (!eventManager)
                {
                    Debug.LogError("There needs to be one active EventManger script on a GameObject in your scene.");
                }
                else
                {
                    eventManager.Init();
                }
            }

            return eventManager;
        }
    }

    void Init()
    {
        if (eventDictionary == null)
        {
            eventDictionary = new Dictionary<string, Action>();
        }
    }

    public static void StartListening(string eventName, Action listener)
    {
        if (instance.eventDictionary.ContainsKey(eventName))
        {
            instance.eventDictionary[eventName] += listener;
        }
        else
        {
            instance.eventDictionary.Add(eventName, listener);
        }
    }

    public static void StopListening(string eventName, Action listener)
    {
        if (instance.eventDictionary.ContainsKey(eventName))
        {
            instance.eventDictionary[eventName] -= listener;
        }
    }

    public static void TriggerEvent(string eventName)
    {
        Action thisEvent = null;
        if (instance.eventDictionary.TryGetValue(eventName, out thisEvent))
        {
            thisEvent.Invoke();
        }
    }
}

Here is a link to a StackOverflow question I posted on this

Why do I get a clone of Action<> when getting from dictionary?

When you call TryGetValue(eventName, out thisEvent) you are providing a reference to which the Dictionary will write the value. You are not getting a reference to what is inside the Dictionary (I mean, you are not getting a deep pointer to the Dictionary structure, meaning that assigning to it will NOT modify the Dictionary).

TheForgot3n1
  • 212
  • 2
  • 11
0

A little late to the party , @programmer answer at top really helped a lot , but still wanted to share an answer if some wants to trigger events with a return value , ofcourse moderators will know what to do with this answer.

.net provides func and action , func<param1,param2,returnValue> or func<param1,returnValue>

here is @programmers code with return value :

 private Dictionary<string, Func<EventParam,bool>> eventDictionary;

private static EventManager eventManager;

public static EventManager instance
{
    get
    {
        if (!eventManager)
        {
            eventManager = FindObjectOfType(typeof(EventManager)) as EventManager;

            if (!eventManager)
            {
                Debug.LogError("There needs to be one active EventManger script on a GameObject in your scene.");
            }
            else
            {
                eventManager.Init();
            }
        }
        return eventManager;
    }
}

void Init()
{
    if (eventDictionary == null)
    {
        eventDictionary = new Dictionary<string, Func<EventParam, bool>>();
    }
}

public static void StartListening(string eventName,Func<EventParam, bool> listener)
{
    Func<EventParam, bool> thisEvent;
    if (instance.eventDictionary.TryGetValue(eventName, out thisEvent))
    {

        thisEvent += listener;


        instance.eventDictionary[eventName] = thisEvent;
    }
    else
    {

        thisEvent += listener;
        instance.eventDictionary.Add(eventName, thisEvent);
    }
}

public static void StopListening(string eventName, Func<EventParam, bool> listener)
{
    if (eventManager == null) return;
    Func<EventParam, bool> thisEvent;
    if (instance.eventDictionary.TryGetValue(eventName, out thisEvent))
    {

        thisEvent -= listener;


        instance.eventDictionary[eventName] = thisEvent;
    }
}

public static bool TriggerEvent(string eventName, EventParam eventParam)
{
    Func<EventParam, bool> thisEvent = null;
    if (instance.eventDictionary.TryGetValue(eventName, out thisEvent))
    {
        bool value;
        value = thisEvent.Invoke(eventParam);
        return value;
    }
    return false;
}

}

public struct EventParam { public string param1;

}

So now Trigger can be called like this

EventParam newparam = new EventParam();
    newparam.param1 = "Ty Mr Programmer this custom eventmanager";
    bool checkme;
    checkme =  EventManager.TriggerEvent("API", newparam);
LumbusterTick
  • 1,067
  • 10
  • 21