Localized events for in-object communication

multiple broadcast
  • Part 1: basics event system
  • Part 2: event picker drawer for in-editor usage
  • Part 3: debugging with platform dependant compilation
  • Part 4: multiple dispatchers [you are here]
  • Part 5: optimization

Now we’ll see how to use the event dispatcher just everywhere. After all it’s not just objects that need to communicate with each other but also components of the same object. Why shouldn’t we apply the same approach inside an object then and use an in-object event system?

internal broadcast systems
internal broadcast systems

It would be good development practice of course to minimize code duplication while keeping separated the stuff that should be, so we naturally will want to avoid rewriting another event dispatcher from scratch (which means modifying the dispatcher we’ve been using all along) but we don’t want that it’s easy to mistakenly use the wrong events on the wrong object, so its best that each type of gameobject (e.g.:each prefab) has its own eventChannel.

Wait, wasn’t it all static references?

Yes, it was. That’s good if you only have one broadcast to rule them all, but as many of you will be thinking that isn’t going to work for a localized event dispatcher. We will need to separate static references and local references, while at the same time avoiding code duplication, because that would hinder code maintenance. And we still want to debug global broadcasts, of course.

Added difficulty: we just don’t want that pesky debug code in our deployment build.

So here’s the plan: the idea is to have one instance “proclaim itself” the controller of global debugging, so that we have an inspector interface to control that debug. Also, every function call will now need to exist in two versions, one global and one local.

The global debug controller

One voice, hear everywhere
One voice, hear everywhere
    public static eventHandlerManager globalDebugController;
    public bool debugGlobally;

    void OnValidate()
    {
        //old stuff

        if (debugGlobally)
        {
            eventHandlerManager temp = globalDebugController;
            globalDebugController = this;
            if (temp != null)
                if (temp.GetInstanceID() != this.GetInstanceID())
                    temp.debugGlobally = false;
        }
        else
        {
            if (globalDebugController != null)
                if (globalDebugController.GetInstanceID() == this.GetInstanceID())
                    globalDebugController = null;
        }
    }

Let’s begin by declaring a new static variable that will hold the reference to the one and only global debug controller, plus a confortable bool to control wich instance will act as a global controller. We could of course write a custom interface or extend the editor to manage this variable, but in a quick and dirty solution we can just use the validation step to check wether the flag is on or of and set or reset the static reference accordingly. Of course we should ensure that there’s only one instance with an active debugGlobally flag, therefore when one is activated we ensure that if there was another instance in charge before its flag gets set to false.

The data and the functions

Obviously we’ll also need to separate local lists of listeners from global ones, so where there was just a static ListenerFunctions we’ll instead have:

    static Dictionary<eventChannels, Dictionary<Enum, gameEventHandler>> globalListenerFunctions = initializeDicts();
    Dictionary<eventChannels, Dictionary<Enum, gameEventHandler>> ListenerFunctions = initializeDicts();

And once that the data is separated we need to separate also its access, so for every function that we had before, we’ll need 3 now. One to hold the actual procedure in a unified manner so that we don’t duplicate code, one to do the global broadcasts, the last one to make local broadcasts (as an alternative you may consider just passing the dispatcher reference as an argument and reserve a dispatcher to be the “global one”).

Since the change is the same on all the 3 functions I’ll go in detail just for one of them, the broadcast. Let’s start from the actual procedure:

    static void executeBroadcast(
#if UNITY_EDITOR
        bool debug, bool specific,
#endif
        MonoBehaviour source, eventChannels evType, Enum ev, eventArgExtend e, Dictionary<eventChannels, Dictionary<Enum, gameEventHandler>> target)
    {
#if UNITY_EDITOR
        //if the flags are true prints a list of what is going to be invoked before execution
        var list = target[evType][ev].GetInvocationList();
        if (list != null && debug && specific)
        {
            Debug.Log("EventHandleManager Broadcast" + evType + " - " + ev + " calling " + list.Length + " functions:");
            for (int i = 0; i < list.Length; i++)
            {
                Debug.Log(evType + " - " + ev + " [" + i + "](" + list[i].Method.DeclaringType.ToString() + ">" + list[i].Method.ToString() + ")");
            }
        }
#endif
        //invoke event delegates
        target[evType][ev](e);
#if UNITY_EDITOR
        if (debug && specific)
        {
            Debug.Log("EventHandleManager Broadcast - OVER");
        }
#endif
    }

I’ll admit, it looks quite the mess. Those #if/#endif in the middle of the function declaration are ugly AF, no way to deny it. But as we’ve said before, we don’t want to waste resources in the deployment build and since this will be a core component (if you are going to use it on every object) we need to optimize it. Actually, this isn’t even optimized enough, as Alessio Greco (to which I’m really grateful) made me realize, combining c# delegates will generate garbage, so we’ll have to fix this with a manual management of the listener references, but we’ll do this next week.

this is getting complicated
this is getting complicated

So, in detail, we’ve got the debug control arguments in the function signature inside a platform dependent if, but we also have a new argument target which can either be the global or the local variable holding the dictionary of listeners, so that this procedure can handle both calls.

    public void Broadcast(MonoBehaviour source, eventChannels evType, Enum ev, eventArgExtend e)
    {
        executeBroadcast(
#if UNITY_EDITOR
                debug, shouldDebugEvent(ev),
#endif
                source, evType, ev, e, ListenerFunctions);
    }

This is the local version of the function. It’s not static, of course, and will automatically pass its own ListenerFunctions to the executeBroadcast function, along with his own debug flags (that, of course, will still be platform dependent).

    public static void globalBroadcast(MonoBehaviour source, eventChannels evType, Enum ev, eventArgExtend e)
    {
#if UNITY_EDITOR
        if (globalDebugController == null)
            executeBroadcast(false, false, source, evType, ev, e, globalListenerFunctions);
        else
#endif
            executeBroadcast(
#if UNITY_EDITOR
                globalDebugController.debug, globalDebugController.shouldDebugEvent(ev),
#endif
                 source, evType, ev, e, globalListenerFunctions);
    }

And at last we’ve got our global call, just for clarity this is what will end up in the deployment build:

    public static void globalBroadcast(MonoBehaviour source, eventChannels evType, Enum ev, eventArgExtend e)
    {
            executeBroadcast( source, evType, ev, e, globalListenerFunctions);
    }

the rest is just a check to see if we’re debugging and to pass the global flags to the execute call, along with the globalListenerFunctions dictionary.

And for the AddListener and RemoveListener functions, just rinse and repeat.

As usual, the copy-pasteable code is down here, join me in the next and last part to delve into the pleasures of optimizaton as we’ll implement our own list of listeners to call. Register to the newsletter if you don’t want to lose it and hit me on twitter for any questions!

public class eventHandlerManager : MonoBehaviour
{
    //platform-conditional compilation helps to avoid the debug overhead in the final build.
#if UNITY_EDITOR
    //each flag controls a different debug message
    public bool debug;//print debug when any event is broadcasted, unless debugSpecificEvent is true, in which case only that one event is debugged
    public bool debugAdds; //print debug when any listener is added
    public bool debugRemovals;//print debug when any listener is removed
    public bool debugSpecificEvent;//print debug when a specific event is broadcasted, after each callback, 
    public EventPicker picker; //component that gives the UI element to select the event to debug with dropdowns
    //invoked after a debugAll check
    bool shouldDebugEvent(Enum ev)
    {
        return (!debugSpecificEvent) || (ev.ToString().Equals(picker.Selected.ToString()));
    }
    public static eventHandlerManager globalDebugController;
    public bool debugGlobally;//if true activates the debugging for the global eventHandlerManager
    /*
     in reaction to setting the debugGlobally variable, set the instance as 
    globalDebugController so that its debug fields are used for the static
    functions to control debugging, it also ensures that only one debugGlobally 
    variable can be set as true in all the instances of the eventHandlerManager
    */
    void OnValidate()
    {
        if (debugSpecificEvent)
            debug = true;
        if (debugGlobally)
        {
            eventHandlerManager temp = globalDebugController;
            globalDebugController = this;
            if (temp != null)
                if (temp.GetInstanceID() != this.GetInstanceID())
                    temp.debugGlobally = false;
        }
        else
        {
            if (globalDebugController != null)
                if (globalDebugController.GetInstanceID() == this.GetInstanceID())
                    globalDebugController = null;
        }
    }
#endif
    //these dictionaries host the callback delegates, they are initialized by the initializeDicts functions, so that they are already initialized when the first Awake is called.
    public static Dictionary<eventChannels, Dictionary<Enum, gameEventHandler>> globalListenerFunctions = initializeDicts();
    public Dictionary<eventChannels, Dictionary<Enum, gameEventHandler>> ListenerFunctions = initializeDicts();
    #region broadcast
    public static void globalBroadcast(MonoBehaviour source, eventChannels evType, Enum ev, eventArgExtend e)
    {
#if UNITY_EDITOR
        if (globalDebugController == null)
            executeBroadcast(false, false, source, evType, ev, e, globalListenerFunctions);
        else
#endif
            executeBroadcast(
#if UNITY_EDITOR
                globalDebugController.debug, globalDebugController.shouldDebugEvent(ev),
#endif
                 source, evType, ev, e, globalListenerFunctions);
    }
    public void Broadcast(MonoBehaviour source, eventChannels evType, Enum ev, eventArgExtend e)
    {
        executeBroadcast(
#if UNITY_EDITOR
                debug, shouldDebugEvent(ev),
#endif
                source, evType, ev, e, ListenerFunctions);
    }
    static void executeBroadcast(
#if UNITY_EDITOR
        bool debug, bool specific,
#endif
        MonoBehaviour source, eventChannels evType, Enum ev, eventArgExtend e, Dictionary<eventChannels, Dictionary<Enum, gameEventHandler>> target)
    {
#if UNITY_EDITOR
        //if the flags are true prints a list of what is going to be invoked before execution
        var list = target[evType][ev].GetInvocationList();
        if (list != null && debug && specific)
        {
            Debug.Log("EventHandleManager Broadcast" + evType + " - " + ev + " calling " + list.Length + " functions:");
            for (int i = 0; i < list.Length; i++)
            {
                Debug.Log(evType + " - " + ev + " [" + i + "](" + list[i].Method.DeclaringType.ToString() + ">" + list[i].Method.ToString() + ")");
            }
        }
#endif
        //invoke event delegates
        target[evType][ev](e);
#if UNITY_EDITOR
        if (debug && specific)
        {
            Debug.Log("EventHandleManager Broadcast - OVER");
        }
#endif
    }
    #endregion
    #region AddListener
    public static void globalAddListener(eventChannels evType, Enum ev, gameEventHandler eventListener)
    {
#if UNITY_EDITOR
        if (globalDebugController == null)
            executeAddListener(false, false, false, evType, ev, eventListener, globalListenerFunctions);
        else
#endif
            executeAddListener(
#if UNITY_EDITOR
                globalDebugController.debug, globalDebugController.shouldDebugEvent(ev), globalDebugController.debugAdds,
#endif
                 evType, ev, eventListener, globalListenerFunctions);
    }
    public void AddListener(eventChannels evType, Enum ev, gameEventHandler eventListener)
    {
        executeAddListener(
#if UNITY_EDITOR
                debug, shouldDebugEvent(ev), debugAdds,
#endif
         evType, ev, eventListener, ListenerFunctions);
    }
    static void executeAddListener(
#if UNITY_EDITOR
                bool debug, bool specific, bool debugAdds,
#endif
         eventChannels evType, Enum ev, gameEventHandler eventListener, Dictionary<eventChannels, Dictionary<Enum, gameEventHandler>> target)
    {
#if UNITY_EDITOR
        //if this event's execution should be logged, add a debugging delegate before the method
        if (debug && specific)
            target[evType][ev] += new gameEventHandler(delegate (eventArgExtend e)
            {
                Debug.Log("EventHandleManager execution" + evType + " - " + ev +
                    " finished " + eventListener.Method.DeclaringType.ToString() + " >" + eventListener.Method.ToString());
            });
        if (debugAdds)
            Debug.Log("EventHandleManager Added event " + evType + " - " + ev +
                        " by " + eventListener.Method.DeclaringType.ToString() + " >" + eventListener.Method.ToString());
#endif
        target[evType][ev] += eventListener;
    }
    #endregion
    #region RemoveListener
    public static void globalRemoveListener(eventChannels evType, Enum ev, gameEventHandler eventListener)
    {
#if UNITY_EDITOR
        if (globalDebugController == null)
            executeRemoveListener(false, evType, ev, eventListener, globalListenerFunctions);
        else
#endif
            executeRemoveListener(
#if UNITY_EDITOR
                globalDebugController.debugRemovals,
#endif
                 evType, ev, eventListener, globalListenerFunctions);
    }
    public void RemoveListener(eventChannels evType, Enum ev, gameEventHandler eventListener)
    {
        executeRemoveListener(
#if UNITY_EDITOR
                debugRemovals,
#endif
                evType, ev, eventListener, ListenerFunctions);
    }
    static void executeRemoveListener(
#if UNITY_EDITOR
                bool debugRemovals,
#endif
        eventChannels evType, Enum ev, gameEventHandler eventListener, Dictionary<eventChannels, Dictionary<Enum, gameEventHandler>> target)
    {
#if UNITY_EDITOR
        if (debugRemovals)
            Debug.Log("EventHandleManager Removed event " + evType + " - " + ev +
                        " by " + eventListener.Method.DeclaringType.ToString() + " >" + eventListener.Method.ToString());
#endif
        target[evType][ev] -= eventListener;
    }
    #endregion
    public void OnDestroy()
    {
        ListenerFunctions= initializeDicts();
    }
    /*
    this method initializes the ListenerFunctions dictionary by doubly indexing first by eventChannel and then by specific event. The initialization consists in an empity gameEventHandler to which new listeners will eventually be added with the AddListener functions
    */
    static Dictionary<eventChannels, Dictionary<Enum, gameEventHandler>> initializeDicts()
    {
        //gets the information on the structure of channels from ChannelEnums
        Dictionary<eventChannels, Array> enumChannelEventList = ChannelEnums.getChannelEnumList();
        Dictionary<eventChannels, Dictionary<Enum, gameEventHandler>> result = new Dictionary<eventChannels, Dictionary<Enum, gameEventHandler>>();
        foreach (var val in (eventChannels[])Enum.GetValues(typeof(eventChannels)))
        {
            result.Add(val, new Dictionary<Enum, gameEventHandler>());
            foreach (var ev in enumChannelEventList[val])
            {
                //adds an empity gameEventHandler for each event
                result[val].Add((Enum)ev, new gameEventHandler(delegate (eventArgExtend e) { }));
            }
        }
        return result;
    }
}
//delegate signature for the callback functions
public delegate void gameEventHandler(eventArgExtend e);
//argument class to be extended to add fields to the callbacks it without changing the signature
public class eventArgExtend : System.EventArgs
{
}

 

Share
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  

Leave a Reply

Your email address will not be published.