Platform dependent compilation: debug on event dispatcher

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

In today’s tutorial I’ll delve in a bit of meta-programming, the obscure art of telling the compiler what to do to generate code, instead of plainly writing it yourself. But don’t worry, at the end of the day, we’ll be using just an if. In this tutorial we’ll be editing my event-system in order to add a debugging feature to it, so that we can log what handlers get invoked on what events and comfortably control that in the inspector.

Platform Dependent Compilation
Platform Dependent Compilation allows you to change code depending on target platform

What’s Platform Dependent Compilation again?

It’s a nice trick you can use in unity to avoid burdening one platform with the code that’s only required for another one. And something that will render your in-editor tests useless when found in some platform-specific plugin, as I learned the painful way. In Unity3d is done by taking one compiler directive to check if some constants are already defined. Unity will define these constants according to build target. If you are developing, for instance, with pc as build target and write #if UNITY_ANDROID all the following code until the #endif will be as commented from the IDE’s point of view.

And when it’s time to build your code, that stuff will just be treated as a comment and not be included in your final build… unless you are building for that platform. In that case that code will be there as if nothing happened. This can be very useful, for instance, when it comes to dealing with input methods in a cross-platform game that’s both on mobile and pc.

Lets debug this
Lets debug this

How do we use Platform dependent compilation to debug an event system?

Basically, the only place where we’ll need it is the editor, so we’ll check for the variable UNITY_EDITOR and that’s it.

And here’s what we’ll need to add to our event script in terms of variables:

#if UNITY_EDITOR
    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()));
    }
    
    void OnValidate()
    {
        if (debugSpecificEvent)
            debug = true;
    }
#endif

So, inside the platform dependent code block we insert a series of flags and an EventPicker so that we’ll be able to manage what to debug when and even select a specific event to debug in isolation.

There are a couple of lines of code there that aren’t actually variables, let’s explain them:

  • shouldDebugEvent is a function, returns true in two cases, either when we’re not debugging a specific event, or when the event in argument is the one selected in thepicker.
  • OnValidate is a monobehaviour method that is invoked every time the data is changed in the inspector. In this case we want to ensure that when we’re debugging a specific event thedebugflag isn’t forgotten since it’ll need to be true to actually print anything.

Then we’ll need to change quite drastically our Broadcast , AddListener and RemoveListener functions.

This is our new broadcast:

    public static void Broadcast(eventChannels evType, Enum ev, eventArgExtend e)
    {
#if UNITY_EDITOR
        //if the flags are true prints a list of what is going to be invoked before execution
        var list = ListenerFunctions[evType][ev].GetInvocationList();
        if (list != null && debug &&  shouldDebugEvent(ev))
        {
            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
        ListenerFunctions[evType][ev](e);
#if UNITY_EDITOR
        if (debug &&  shouldDebugEvent(ev))
        {
            Debug.Log("EventHandleManager Broadcast - OVER");
        }
#endif
    }

The first thing that’s coming to many people’s mind right now is: “wait, what the fuck is that Method.ToString()?!”. Well, it’s C# reflection again. It keeps creeping in, but what we say to the god of the 10’000-words-long tutorials?

t4w9s7

So, just know that that debug code will print the name of the classes and the functions that the event dispatcher will invoke.
In the beginning we recover the list of functions that will be called from the delegate with GetInvocationList, then (provided this event broadcast should be debugged) for each one of them we’ll print its name and source, along with the position number.

Then we invoke the functions, and at last we print a nice “over” after the event reactions are finished.

Registering and unregistering

    static void AddListener(eventChannels evType, Enum ev, gameEventHandler eventListener)
    {
#if UNITY_EDITOR
        //if this event's execution should be logged, add a debugging delegate before the method
        if (debug && shouldDebugEvent(ev))
            ListenerFunctions[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
        ListenerFunctions[evType][ev] += eventListener;
    }

Here we do two things: in case the event broadcast debugging is active for this event we add a delegate that prints event channel, event name, invoking class and method name. Then, if we’re debugging registration we also log that, again with the same debug message.

The latter will also be done for the unregister function:

    public static void RemoveListener(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
        ListenerFunctions[evType][ev] -= eventListener;
    }

Here again we log all the same information when the listener is removed from the event.

So, wrapping this up, we’ve added debugging features to our event dispatcher and can now place it in any scene so that we can handle the debug operations in it using the inspector. As usual the full code is down here for copy-paste purposes, we’ll get next time to the semi-final form of the script that will allow not just one single dispatcher in the whole game but multiple independent channels of communication so that for instance a single character can have its own internal event system. Register to the newsletter if you don’t want to lose it and hit me on twitter for any questions!

And by the way, I’m looking for a job right now, check out my portfolio here!

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

public class eventHandlerManager : MonoBehaviour
{
#if UNITY_EDITOR
    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()));
    }
    
    void OnValidate()
    {
        if (debugSpecificEvent)
            debug = true;
    }
#endif
    public static Dictionary<eventChannels, Dictionary<Enum, gameEventHandler>> ListenerFunctions = initializeDicts();

    public static void Broadcast(eventChannels evType, Enum ev, eventArgExtend e)
    {
#if UNITY_EDITOR
        //if the flags are true prints a list of what is going to be invoked before execution
        var list = ListenerFunctions[evType][ev].GetInvocationList();
        if (list != null && debug &&  shouldDebugEvent(ev))
        {
            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
        ListenerFunctions[evType][ev](e);
#if UNITY_EDITOR
        if (debug &&  shouldDebugEvent(ev))
        {
            Debug.Log("EventHandleManager Broadcast - OVER");
        }
#endif
    }
    static void AddListener(eventChannels evType, Enum ev, gameEventHandler eventListener)
    {
#if UNITY_EDITOR
        //if this event's execution should be logged, add a debugging delegate before the method
        if (debug && shouldDebugEvent(ev))
            ListenerFunctions[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
        ListenerFunctions[evType][ev] += eventListener;
    }

    public static void RemoveListener(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
        ListenerFunctions[evType][ev] -= eventListener;
    }
    public void OnDestroy()
    {
        ListenerFunctions = initializeDicts();
    }

    static Dictionary<eventChannels, Dictionary<Enum, gameEventHandler>> initializeDicts()
    {
        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])
            {
                result[val].Add((Enum)ev, new gameEventHandler(delegate (eventArgExtend e) { }));
            }
        }
        return result;
    }
}
Share
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  

Leave a Reply

Your email address will not be published.