An Event System

Recently I was tasked with creating an event system. I had never really made my own system to handle events before so it took a bit of researching with experimentation and iteration to get something I was satisfied with. I set up a few criteria for the system.

  • Events are fire and forget
  • An event producer shouldn’t need to know about any of the event handlers
  • There should be two types of messaging, broadcast and multicast
  • Subscribers for broadcast shouldn’t need to know anything about the event producer
  • Subscribers for multicast should specify a producer to receive events from
  • Any class can produce events, and any class can consume them
  • Any type of data can be passed along with the message

With these criteria in place there will exist a very loose coupling between the event producers and consumers, at least for broadcasting. Multicasting works by a consumer specifying an exact object to receive specific message types from. An example I was given was a player’s health bar listening for on damage events from the player, with multicasting this “local” messaging is enabled. This is beneficial as it means that a given object, the health bar for example, will never hear from a monster about a damage event, only the player.

I have chosen to implement channels, a channel splits up subscriptions to events based on message type and contains a set of broadcast and multicast subscribers. The code for this can be seen below.

    /// <summary>
    /// An event channel is a class encapsulating a set of broadcast and multicast subscriptions belonging to a given message type.
    /// </summary>
    private class EventChannel
    {
        public MessageTypes MessageType { get; set; }
        public Dictionary<ChannelLayerTypes, ISubscriptionLayer> BroadcastSubscribers { get; set; }
        public Dictionary<int, Dictionary<ChannelLayerTypes, ISubscriptionLayer>> MulticastSubscribers { get; set; } //Stored by value of source

        public EventChannel()
        {
            BroadcastSubscribers = new Dictionary<ChannelLayerTypes, ISubscriptionLayer>();
            MulticastSubscribers = new Dictionary<int, Dictionary<ChannelLayerTypes, ISubscriptionLayer>>();
        }
    }

Channels are further broken up into layers, each layer type inherits from the ISubscriptionLayer interface. In this example I’ve only made one class that implements, it is a base subscription layer that wraps a call to a delegate taking a variable of type T as a parameter. Others can easily be made where the invoked methods take more parameters of other types.

    /// <summary>
    /// ISubscriptionLayer is an interface that allows seperation of subscriptions into execution layers.
    /// </summary>
    private interface ISubscriptionLayer
    {
        ChannelLayerTypes LayerType { get; set; }
    }

    /// <summary>
    /// A subscription layer that has a delegate that is called when an event occurs.
    /// </summary>
    /// <typeparam name="T">The payload type.</typeparam>
    private class SubscriptionLayer<T> : ISubscriptionLayer
    {
        public ChannelLayerTypes LayerType { get; set; }
        public Broadcast<T> OnEvent { get; set; }

        public SubscriptionLayer(ChannelLayerTypes layerType)
        {
            LayerType = layerType;
            OnEvent = null;
        }
    }

I have chosen to allow for three layer types to be available via an enum, High, Medium and Low. These represent an ordering when an event is sent from a producer, and are executed from High to Low. One can therefore be assured that a subscriber A in the HighPriority layer will always be executed before subscribers in the lower layers.

With these data structures we have everything we need to create a messaging system. To handle the production and sending of messages from a producer to a subscriber, I have chosen the singleton pattern. All of the methods in the class are static, as well as the internal variables. This allows any object to easily broadcast or subscribe to the event system. The class keeps two variables a dictionary that has the message type as key and event channel as values, and a list of permanent message types that should not be removed when a level load occurs.

To subscribe to receive an event, a listener must simply invoke one of the following two methods.

    /// <summary>
    /// Subscribes a subscriber to all broadcasts of a given message type.
    /// </summary>
    /// <typeparam name="T">The payload type.</typeparam>
    /// <param name="messageType">The message type.</param>
    /// <param name="func">The delegate to call on event.</param>
    /// <param name="layerType">The layer to add to.</param>
    public static void SubscribeBroadcast<T>(MessageTypes messageType, Broadcast<T> func, ChannelLayerTypes layerType)
    {
        SubscriptionLayer<T> broadcastChannelLayer = null;
        if (channels.ContainsKey(messageType))
        {
            if (channels[messageType].BroadcastSubscribers.ContainsKey(layerType))
            {
                broadcastChannelLayer = channels[messageType].BroadcastSubscribers[layerType] as SubscriptionLayer<T>;
                if (broadcastChannelLayer != null)
                {
                    broadcastChannelLayer.OnEvent += func;
                }
            }
            else
            {
                channels[messageType].BroadcastSubscribers.Add(layerType, new SubscriptionLayer<T>(layerType));
                broadcastChannelLayer = channels[messageType].BroadcastSubscribers[layerType] as SubscriptionLayer<T>;
                if (broadcastChannelLayer != null)
                {
                    broadcastChannelLayer.OnEvent += func;
                }

            }
            return;
        }

        channels.Add(messageType, new EventChannel());
        channels[messageType].BroadcastSubscribers.Add(layerType, new SubscriptionLayer<T>(layerType));
        broadcastChannelLayer = channels[messageType].BroadcastSubscribers[layerType] as SubscriptionLayer<T>;
        if (broadcastChannelLayer != null)
        {
            broadcastChannelLayer.OnEvent += func;
        }

    }

    /// <summary>
    /// Subscribes to a multicast channel specified by the message type.
    /// </summary>
    /// <typeparam name="T">The payload type.</typeparam>
    /// <param name="messageType">The type of message.</param>
    /// <param name="func">The function to call on event.</param>
    /// <param name="producerId">A unique identifier (GetInstanceID()) for the producer.</param>
    /// <param name="layerType">The layer type to add to.</param>
    public static void SubscribeMulticast<T>(MessageTypes messageType, Broadcast<T> func, int producerId, ChannelLayerTypes layerType)
    {
        SubscriptionLayer<T> multicastChannelLayer;
        if (channels.ContainsKey(messageType))
        {
            if (channels[messageType].MulticastSubscribers.ContainsKey(producerId))
            {
                if (channels[messageType].MulticastSubscribers[producerId].ContainsKey(layerType))
                {
                    multicastChannelLayer = channels[messageType].MulticastSubscribers[producerId][layerType] as SubscriptionLayer<T>;
                    if (multicastChannelLayer != null)
                    {
                        multicastChannelLayer.OnEvent += func;
                    }
                }
                else
                {
                    channels[messageType].MulticastSubscribers[producerId].Add(layerType, new SubscriptionLayer<T>(layerType));
                    multicastChannelLayer = channels[messageType].MulticastSubscribers[producerId][layerType] as SubscriptionLayer<T>;
                    if (multicastChannelLayer != null)
                    {
                        multicastChannelLayer.OnEvent += func;
                    }
                }

                return;
            }

            channels[messageType].MulticastSubscribers.Add(producerId, new Dictionary<ChannelLayerTypes, ISubscriptionLayer>());
            channels[messageType].MulticastSubscribers[producerId].Add(layerType, new SubscriptionLayer<T>(layerType));
            multicastChannelLayer = channels[messageType].MulticastSubscribers[producerId][layerType] as SubscriptionLayer<T>;
            if (multicastChannelLayer != null)
            {
                multicastChannelLayer.OnEvent += func;
            }
            return;
        }
        channels.Add(messageType, new EventChannel());
        channels[messageType].MulticastSubscribers.Add(producerId, new Dictionary<ChannelLayerTypes, ISubscriptionLayer>());
        channels[messageType].MulticastSubscribers[producerId].Add(layerType, new SubscriptionLayer<T>(layerType));
        multicastChannelLayer = channels[messageType].MulticastSubscribers[producerId][layerType] as SubscriptionLayer<T>;
        if (multicastChannelLayer != null)
        {
            multicastChannelLayer.OnEvent += func;
        }
    }

To unsubscribe we must go through each of the layers and remove the delegate, if the layer’s delegate is null then we remove that layer as it no longer has any listeners. Below is the code to do this for the broadcast subscribers, the code for the multicast unsubscribe is very similar.

/// <summary>
    /// Unsubscribes a subscriber from all broadcasts of a given message type.
    /// </summary>
    /// <typeparam name="T">The type of the payload.</typeparam>
    /// <param name="messageType">The message type.</param>
    /// <param name="func">The delegate to remove.</param>
    public static void UnsubscribeBroadcast<T>(MessageTypes messageType, Broadcast<T> func)
    {
        SubscriptionLayer<T> broadastChannelLayer;
        if (channels.ContainsKey(messageType))
        {
            EventChannel channel = channels[messageType];

            //Must check each layer
            if (channel.BroadcastSubscribers.ContainsKey(ChannelLayerTypes.HighPriority))
            {
                broadastChannelLayer = channel.BroadcastSubscribers[ChannelLayerTypes.HighPriority] as SubscriptionLayer<T>;
                if (broadastChannelLayer != null)
                {
                    if (broadastChannelLayer.OnEvent != null)
                    {
                        broadastChannelLayer.OnEvent -= func;
                    }

                    if (broadastChannelLayer.OnEvent == null) //no more subscribers, remove this layer
                    {
                        channel.BroadcastSubscribers.Remove(ChannelLayerTypes.HighPriority);
                    }
                }
            }

            if (channel.BroadcastSubscribers.ContainsKey(ChannelLayerTypes.MedPriority))
            {
                broadastChannelLayer = channel.BroadcastSubscribers[ChannelLayerTypes.MedPriority] as SubscriptionLayer<T>;
                if (broadastChannelLayer != null)
                {
                    if (broadastChannelLayer.OnEvent != null)
                    {
                        broadastChannelLayer.OnEvent -= func;
                    }

                    if (broadastChannelLayer.OnEvent == null) //no more subscribers, remove this layer
                    {
                        channel.BroadcastSubscribers.Remove(ChannelLayerTypes.MedPriority);
                    }
                }
            }

            if (channel.BroadcastSubscribers.ContainsKey(ChannelLayerTypes.LowPriority))
            {
                broadastChannelLayer = channel.BroadcastSubscribers[ChannelLayerTypes.LowPriority] as SubscriptionLayer<T>;
                if (broadastChannelLayer != null)
                {
                    if (broadastChannelLayer.OnEvent != null)
                    {
                        broadastChannelLayer.OnEvent -= func;
                    }

                    if (broadastChannelLayer.OnEvent == null) //no more subscribers, remove this layer
                    {
                        channel.BroadcastSubscribers.Remove(ChannelLayerTypes.LowPriority);
                    }
                }
            }
        }
    }

Finally we have our broadcast method, invoking this is done by EventMessenger.Broadcast(messageType, args). We use the message type to select the correct channel and get all three layers. After that it is simply invoking the OnEvent delegate with the args passed to the method.

 /// <summary>
    /// Sends an event to all subscribers of a given message type.
    /// </summary>
    /// <typeparam name="T">The type of argument that will be sent with the event.</typeparam>
    /// <param name="messageTypes">The message type.</param>
    /// <param name="args">The argument to pass to the subscribers.</param>
    public static void Broadcast<T>(MessageTypes messageTypes, T args)
    {
        if (channels.ContainsKey(messageTypes))
        {
            EventChannel channel = channels[messageTypes];

            //Execute all layers
            ISubscriptionLayer highPriority = null;
            ISubscriptionLayer medPriority = null;
            ISubscriptionLayer lowPriority = null;

            channel.BroadcastSubscribers.TryGetValue(ChannelLayerTypes.HighPriority, out highPriority);
            channel.BroadcastSubscribers.TryGetValue(ChannelLayerTypes.MedPriority, out medPriority);
            channel.BroadcastSubscribers.TryGetValue(ChannelLayerTypes.LowPriority, out lowPriority);

            SubscriptionLayer<T> chainedLayer = highPriority as SubscriptionLayer<T>;
            if (highPriority != null && chainedLayer != null && chainedLayer.OnEvent != null)
            {
                chainedLayer.OnEvent(args);
            }

            chainedLayer = medPriority as SubscriptionLayer<T>;
            if (medPriority != null && chainedLayer != null && chainedLayer.OnEvent != null)
            {
                chainedLayer.OnEvent(args);
            }

            chainedLayer = lowPriority as SubscriptionLayer<T>;
            if (lowPriority != null && chainedLayer != null && chainedLayer.OnEvent != null)
            {
                chainedLayer.OnEvent(args);
            }
        }
    }

For a multicast we further route the event by using a unique identifier for the producer (GetInstanceID()) to retrieve the layers from the dictionary.

/// <summary>
    /// Sends a message to all subscribers of the given message type that come from source.
    /// </summary>
    /// <param name="messageTypes">The message type.</param>
    /// <param name="args">The arguments to send.</param>
    /// <param name="producerId">The source object unique id.</param>
    public static void Multicast<T>(MessageTypes messageTypes, T args, int producerId)
    {
        if (channels.ContainsKey(messageTypes))
        {
            EventChannel channel = channels[messageTypes];

            if (channel.MulticastSubscribers.ContainsKey(producerId))
            {
                //Execute all layers
                ISubscriptionLayer highPriority = null;
                ISubscriptionLayer medPriority = null;
                ISubscriptionLayer lowPriority = null;

                channel.MulticastSubscribers[producerId].TryGetValue(ChannelLayerTypes.HighPriority, out highPriority);
                channel.MulticastSubscribers[producerId].TryGetValue(ChannelLayerTypes.MedPriority, out medPriority);
                channel.MulticastSubscribers[producerId].TryGetValue(ChannelLayerTypes.LowPriority, out lowPriority);

                SubscriptionLayer<T> chainedLayer = highPriority as SubscriptionLayer<T>;
                if (highPriority != null && chainedLayer != null && chainedLayer.OnEvent != null)
                {
                    chainedLayer.OnEvent(args);
                }

                chainedLayer = medPriority as SubscriptionLayer<T>;
                if (medPriority != null && chainedLayer != null && chainedLayer.OnEvent != null)
                {
                    chainedLayer.OnEvent(args);
                }

                chainedLayer = lowPriority as SubscriptionLayer<T>;
                if (lowPriority != null && chainedLayer != null && chainedLayer.OnEvent != null)
                {
                    chainedLayer.OnEvent(args);
                }
            }
        }
    }

And that’s basically all there is to it, the main difference between my implementation and others for Unity is that this one allows for subscribers to choose which object they want to hear from.

Something possibly worth adding is the ability to use a broadcast method that will also send this event to all multicast targets of the broadcaster.