Event Aggregator for Event Unity3d

The idea to write your own extended event aggregator for Unity3d is long overdue. After reading several articles on this topic, I realized that there is not enough “correct” (within Unity3d) and the aggregator that is necessary for me, all solutions are stripped down and do not have the necessary functionality. 

Required Functionality:


  1. Any class can subscribe to any event (often aggregators in a unit make specific Gameobject subscribers)
  2. The possibility of double subscribing of a specific instance to a specific event should be excluded (in standard tools you need to follow this yourself)
  3. There should be functionality both manual unsubscribing and automatic, in the case of deleting an instance / disabling monobekh (I want to subscribe and not take a steam bath that the subscriber suddenly throws back the hoof)
  4. events should be able to transfer data / links of any complexity (I want to subscribe in one line and get the whole set of data without troubles)

Where to apply it


  1. This is ideal for the UI, when there is a need to forward data from any object without any connection.
  2. Messages about data changes, some analogue of the reactive code.
  3. For dependency injection
  4. Global callbacks

Weak spots


  1. Due to checks on dead subscribers and takes (I will reveal later) the code is slower than similar solutions
  2. Class / struct is used as the core of the event, so as not to allocate memory + the top problem, it is not recommended to spam events in the update)

General ideology


The general ideology is that for us an event is a specific and relevant data package. Let's say we pressed a button on an interface / joystick. And we want to send an event with signs of pressing a specific button for further processing. The result of processing clicks is visual changes to the interface and some kind of action in the logic. Accordingly, there may be processing / subscription in two different places. 

What the event body / data packet looks like in my case:

Event body example
public struct ClickOnButtonEvent
    {
        public int ButtonID; // здесь может быть также enum клавиши
    }


What the event subscription looks like:


public static void AddListener(object listener, Action action)

To subscribe, we need to indicate:
The object that is the subscriber (usually this is the class in which the subscription is, but this is not necessary, you can indicate by the subscriber one of the class instances from the class fields. The
type / event we subscribe to. This is the key essence of this aggregator, for us a certain type of class is an event that we listen to and process.
Subscribing best in Awake and OnEnable;

Example

public class Example : MonoBehaviour
{
    private void Awake()
    {
        EventAggregator.AddListener(this, ClickButtonListener);
    }
    private void ClickButtonListener(ClickOnButtonEvent obj)
    {
        Debug.Log("нажали на кнопку" + obj.ButtonID);
    }
}

To make it clear what the chip, consider a more complex case


We have character icons that:
  1. Know which character they are attached to.
  2. Reflect the amount of mana, hp, exp, and statuses (stun, blindness, fear, madness)

And here you can do a few events

To change indicators:

public struct CharacterStateChanges
{
    public Character Character;
    public float Hp;
    public float Mp;
    public float Xp;
}

To change negative statuses:

public struct CharacterNegativeStatusEvent
{
    public Character Character;
    public Statuses Statuses; //enum статусов
}

Why in both cases do we pass the character class? Here is the event subscriber and its handler:

private void Awake()
    {
        EventAggregator.AddListener
                (this, CharacterNegativeStatusListener);
    }
    private void CharacterNegativeStatusListener(CharacterNegativeStatusEvent obj)
    {
        if (obj.Character != _character)
            return;
        _currentStatus = obj.Statuses;
    }

This is the marker by which we process the event and understand what exactly we need it.
Why not suppose you subscribe directly to the Character class? And spam them?
It will be difficult to debut, it is better for a group of classes / events to create their own separate event.

Why, again, inside the event, just do not put Character and take everything from it?
So by the way it is possible, but often in classes there are visibility restrictions, and the necessary data for the event may not be visible from the outside.

if the class is too heavy to use as a marker?
In fact, in most cases, a marker is not needed; a group of updated classes is rather rare. Usually one specific entity needs an event - a controller / view model, which usually displays the state of the 1st character. And so there is always a banal solution - IDs of different types (from inam, to complex hash, etc.).

What's under the hood and how does it work?


Directly aggregator code
namespace GlobalEventAggregator
public delegate void EventHandler(T e);
{
    public class EventContainer : IDebugable
    {
        private event EventHandler _eventKeeper;
        private readonly Dictionary> _activeListenersOfThisType = new Dictionary>();
        private const string Error = "null";
        public bool HasDuplicates(object listener)
        {
            return _activeListenersOfThisType.Keys.Any(k => k.Target == listener);
        }
        public void AddToEvent(object listener, EventHandler action)
        {
            var newAction = new WeakReference(listener);
            _activeListenersOfThisType.Add(newAction, action);
            _eventKeeper += _activeListenersOfThisType[newAction];
        }
        public void RemoveFromEvent(object listener)
        {
            var currentEvent = _activeListenersOfThisType.Keys.FirstOrDefault(k => k.Target == listener);
            if (currentEvent != null)
            {
                _eventKeeper -= _activeListenersOfThisType[currentEvent];
                _activeListenersOfThisType.Remove(currentEvent);
            }
        }
        public EventContainer(object listener, EventHandler action)
        {
            _eventKeeper += action;
            _activeListenersOfThisType.Add(new WeakReference(listener), action);
        }
        public void Invoke(T t)
        {
            if (_activeListenersOfThisType.Keys.Any(k => k.Target.ToString() == Error))
            {
                var failObjList = _activeListenersOfThisType.Keys.Where(k => k.Target.ToString() == Error).ToList();
                foreach (var fail in failObjList)
                {
                    _eventKeeper -= _activeListenersOfThisType[fail];
                    _activeListenersOfThisType.Remove(fail);
                }
            }
            if (_eventKeeper != null)
                _eventKeeper(t);
            return;
        }
        public string DebugInfo()
        {
            string info = string.Empty;
            foreach (var c in _activeListenersOfThisType.Keys)
            {
                info += c.Target.ToString() + "\n";
            }
            return info;
        }
    }
    public static class EventAggregator
    {
        private static Dictionary GlobalListeners = new Dictionary();
        static EventAggregator()
        {
            SceneManager.sceneUnloaded += ClearGlobalListeners;
        }
        private static void ClearGlobalListeners(Scene scene)
        {
            GlobalListeners.Clear();
        }
        public static void AddListener(object listener, Action action)
        {
            var key = typeof(T);
            EventHandler handler = new EventHandler(action);
            if (GlobalListeners.ContainsKey(key))
            {
                var lr = (EventContainer)GlobalListeners[key];
                if (lr.HasDuplicates(listener))
                    return;
                lr.AddToEvent(listener, handler);
                return;
            }
            GlobalListeners.Add(key, new EventContainer(listener, handler));
        }
        public static void Invoke(T data)
        {
            var key = typeof(T);
            if (!GlobalListeners.ContainsKey(key))
                return;
            var eventContainer = (EventContainer)GlobalListeners[key];
            eventContainer.Invoke(data);
        }
        public static void RemoveListener(object listener)
        {
            var key = typeof(T);
            if (GlobalListeners.ContainsKey(key))
            {
                var eventContainer = (EventContainer)GlobalListeners[key];
                eventContainer.RemoveFromEvent(listener);
            }
        }
        public static string DebugInfo()
        {
            string info = string.Empty;
            foreach (var listener in GlobalListeners)
            {
                info += "тип на который подписаны объекты " +  listener.Key.ToString() + "\n";
                var t = (IDebugable)listener.Value;
                info += t.DebugInfo() + "\n";
            }
            return info;
        }
    }
    public interface IDebugable
    {
        string DebugInfo();
    }
}


Let's start with the main one.

This is a dictionary in which the key is type and the value is a container.

public class EventContainer : IDebugable

private static Dictionary GlobalListeners = new Dictionary();

Why do we store the container as an object? The dictionary does not know how to store generics. But due to the key, we are able to quickly bring the object to the type we need.

What does the container contain?

private event EventHandler _eventKeeper;
        private readonly Dictionary> _activeListenersOfThisType = new Dictionary>();

It contains a generic multidelegate and a collection where the key is the object that is the subscriber, and the value is the same handler method. In fact, this dictionary contains all the objects and methods that belong to this type. As a result, we call a multidelegate, and he calls all subscribers, this is an “honest” event system in which there are no restrictions on the subscriber, but in most other aggregators, under the hood, a collection of classes that are generalized either by a special interface or inherited from a class that implements the system is iterated messages.

When a multidelegate is called, a check is made to see if there are dead keys, the collection is cleaned of corpses, and then a multidelegate with relevant subscribers is taken in. This takes time, but again, in fact, if the functionality of the events is separated, then one event will have 3-5 subscribers, so the check is not so scary, the benefit of comfort is more obvious. For network stories where there can be a thousand or more subscribers, this aggregator is better not to use. Although the question remains open - if you remove the check for corpses, which is faster - iterating over an array of subscribers from 1k or calling a multi-delegate from 1k subscribers.

Features of use


A subscription is best pushed into Awake.

If an object is actively turning on / off, it is better to subscribe to Awake and OnEnable, it will not sign twice, but the possibility that an inactive GameObject will be taken for dead will be excluded.

Invoicing events is better not before the start, when all subscribers will be created and registered.

The aggregator cleans the list at the unloading of the scene. In some aggregators, it is suggested to clean up the scene loading - this is a file, the scene loading event comes after Awake / OnEnable, the added subscribers will be deleted.

The aggregator has - public static string DebugInfo (), you can see which classes are subscribed to which events.

GitHub repository

Also popular now: