Event system: tutorial for Unity3D & C#

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

Today I’ll start presenting you the single most useful piece of code that I’ve ever written. Since writing it I used it in every single project I’ve made, however simple. Even in game jams.
I’m speaking of an Event System. A class with no other purpose than to let other objects communicate with each other, with as little overhead as possible. And it’s implemented with delegates.

And it’s way better than SendMessage, if you ask me.

event_driven_programming

What’s an Event System?

Those of you who already know, just skip this paragraph. Still with me? good. The main problem you’ll have with SendMessage is that it needs to use a method’s name as an input. So the object sending the message not only needs to know what method should be invoked by the receiver, but also his name. In a case sensitive way. Want to change that name? too bad, the IDE won’t change the string for you. You’ll have to remember every single one of those and do it by yourself… or never change a function name ever again. Or remove one.

Sounds bad? it is. It’s a maintenance nightmare. Plus, it uses strings. Fuck strings. Strings are evil. They use your memory, trigger your garbage collector and spit on your mother. Never use them unless at gunpoint… and even then think twice about it and instead use an enum.ToString() if you can.

So what’s the solution? we need something more like this: you need to send a message between objects (or scripts) when a “thing” happens. So the scripts in the other object react to the “thing”. The caller script doesn’t need to know who or how will do anything in reaction to the “thing”. It just needs to shout out “Hey! I have a thing here!” and eventually how big the “thing” is. For instance, “Hey! I have a 3.5 Damage here!”. Then the health script subtracts the damage and the particle script spills blood everywhere. But the collider doesn’t need to know that. The “thing” is an Event. Shouting is a Broadcast. The reactions are called Callbacks or Handlers.

But this is nothing I invented, you can just study it here or here, or even here.

What’s a Delegate?

As before, if you know already, just skip ahead. Delegates are something I wish they did teach me in university, because they are just awesome. To put it bluntly: in C# you can treat functions as if they were variables and pass them as an argument to other functions. This means that you can change behaviours of an object at runtime. Or have an object hold and call a function without it even knowing what it does. Absolute decoupling. A maintenance heaven.

Before you can use a delegate, you first need to declare its type. Which means telling the compiler which return type and what arguments will be assigned to it. Not its name. Not its content. Just the signature structure. Of course, if you use as a signature something like:

public delegate object nameFunct(object o);

you can then pass anything and get anything (but say goodbye to compiler type-checks).

After you have declared a delegate type, you can then declare a delegate variable and assign to it a function with a matching signature. Just like you would do with any other variable. If you still need to delve deeper you can go here or here.

Get on with it!

gowi-2

Now, if you have seen my portfolio the event system there can be quite intimidating, but don’t worry, we won’t be doing that version right now. We’ll start with version 0.0.3 while that one is 1.0.1 [edit: this was before optimizing for the last part of the tutorial]. That makes about 100 rows of code and 20 headaches of difference.

Before we get to the main class, the event dispatcher, let’s define some stuff we’ll use there.

public delegate void gameEventHandler(eventArgExtend e);

This will be our event signature. Nothing to return because we don’t want the event sender to have anything to do with the receivers. The event type is this thing here:

public class eventArgExtend : System.EventArgs { }

Why didn’t I use directlySystem.EventArgs? Because in the future I may want to add something between those parentheses and when it happens it will cost me nothing to do so. Had I used directly that type it could require more work to do it.

Now, let’s get to something more interesting: defining the events themselves. To do so I’ll use something infinitely superior to strings: enums.

public enum eventChannels
{
    inGame
}
public enum inGameChannelEvents
{
    thing
}

Now we’ll need a comfortable way to pass this information to the dispatcher. For this I just created a class with a static function:

public class ChannelEnums
{
    public static Dictionary<eventChannels, System.Array> getChannelEnumList()
    {

        Dictionary<eventChannels, System.Array> enumChannelEventList = new Dictionary<eventChannels, System.Array>();
        enumChannelEventList.Add(eventChannels.inGame, System.Enum.GetValues(typeof(inGameChannelEvents)));
        return enumChannelEventList;
    }
}

Notice: adding a channel requires to add a new enum type, and there is no automated way to set a link between an enum value ineventChannelsand an other enumtype. So for each channel you need to add, you’ll have to write a new row of code like the one just before thereturn.

What we did here is to create a static function for the dispatcher. The function will recovery every event channel and every corresponding array of values, so that we can initialize the list of listeners in the dispatcher. All in the nice form of a Dictionary.

Now let’s get to the Event System

Our event system needs to be usable at EVERY stage of game play. This includes the Awake function of the first objects to be present in the first scene. Which means that I can’t use the Awake function to initialize the event system, or I would get in a race condition. That’s fucked up. But years of Unity3D ninjutsu allowed me to discover a precious thing: default values are initialized before Awake, and you can get them with static functions.

public class eventHandlerManager : MonoBehaviour
{
    static Dictionary<eventChannels, Dictionary<Enum, gameEventHandler>> ListenerFunctions = initializeDicts();

What’s thatinitializeDicts? we’ll see later. For now just look at the type of ListenerFunctions: it’s a dictionary of dictionaries. It allows us to index gameEventHandler delegates first by channel and then by event.

Now we can write the key functions of the dispatcher: one to raise an event, one to add a delegate as listener, one to remove it.

public static void Broadcast( eventChannels evType,Enum ev,eventArgExtend e) 
	{
		ListenerFunctions[evType][ev](e);
	}
	
	public static void AddListener(eventChannels evType,Enum ev, gameEventHandler eventListener)
	{
		ListenerFunctions[evType][ev]+=eventListener;
	}
	public static void RemoveListener(eventChannels evType,Enum ev, gameEventHandler eventListener)
	{
		ListenerFunctions[evType][ev]-=eventListener;
	}

Why are they all static? because that way you don’t need to get the other classes to find the instance. Yes, this is limiting: you can’t have a damage event that only gets notified to one character. But for something more selective we’ll need to make a lot of changes, for the time being just add an identifier as field in the argument. Then have the receivers check that value. We’ll get back to that in future tutorials.

This is all that’s needed on the outside to use this event handler. Other classes that want to react to an event just need to declare a function that matches thegameEventHandlersignature and then invoke theaddListenerfunction giving that function as the last argument (without parentheses), and do the same for removal like this:

eventHandlerManager.AddListener(eventChannels.inGame, inGameChannelEvents.thing, onThing);

The broadcast use is even simpler:

eventHandlerManager.Broadcast(eventChannels.inGame, inGameChannelEvents.thing, new eventArgExtend());

So, now we’re almost done. There is just one last detail for the magic to fully work: how do we initialize and cleanListenerFunctions?

    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;
    }

Wait a minute: did I initialize ListenerFunctions on destroy? Yes, because that is a static field. It will survive to the existence of the eventHandlerManager instance. And since that should only happen on a scene load, I can be sure that nothing should stay in the dictionary and that the new scene has a ready new clean slate to work on when the first Awake is called.

The initialization works by first getting the enum data from the static function we defined before in ChannelEnums, then using that data to initialize every field of the dictionary with an empitygameEventHandler; this way we can later add the Handlers with a+=instead of checking for a null field every time.

That’s all folks!

Not really. Actually there is a lot more that we can add to this class, mainly for debugging purposes, plus the “local” version of the event dispatcher that I mentioned before. But this tutorial is already huge, so maybe it’s better to deal with that stuff next week. If you want to be sure not losing it, subscribe to my newsletter. For any feedback comments are below or you can just add me on twitter.

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

public class eventHandlerManager : MonoBehaviour
{
    public static Dictionary<eventChannels, Dictionary<Enum, gameEventHandler>> ListenerFunctions = initializeDicts();

    public static void Broadcast(eventChannels evType, Enum ev, eventArgExtend e)
    {
        ListenerFunctions[evType][ev](e);
    }

    public static void AddListener(eventChannels evType, Enum ev, gameEventHandler eventListener)
    {
        ListenerFunctions[evType][ev] += eventListener;
    }
    public static void RemoveListener(eventChannels evType, Enum ev, gameEventHandler eventListener)
    {
        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;
    }
}

public enum eventChannels
{
    inGame
}

public enum inGameChannelEvents
{
    thing
}

public class eventArgExtend : System.EventArgs { }

public delegate void gameEventHandler(eventArgExtend e);

public class ChannelEnums
{
    public static Dictionary<eventChannels, System.Array> getChannelEnumList()
    {

        Dictionary<eventChannels, System.Array> enumChannelEventList = new Dictionary<eventChannels, System.Array>();
        enumChannelEventList.Add(eventChannels.inGame, System.Enum.GetValues(typeof(inGameChannelEvents)));
        return enumChannelEventList;
    }
}

 

Share
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  

Leave a Reply

Your email address will not be published.