📜 ⬆️ ⬇️

Event Aggregator for Unity3d (Event Aggregator)

The idea to write your own advanced 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 aggregator that I need, all solutions are trimmed and do not have the necessary functionality.

Required functionality:


  1. Any class can subscribe to any event (often aggregators in a unit make subscribers a specific Gameobject)
  2. The possibility of double subscription of a specific instance to a specific event should be excluded (in standard tools, you should follow this yourself)
  3. Must be functional as a manual unsubscribe, as well as automatic, in case of deletion of an instance / disconnection of a mono-tech (I want to subscribe and not to steam, that the subscriber suddenly throws off his hoof)
  4. Events should be able to transfer data / links of any complexity (I want to subscribe to one line and get the entire data set without problems)

Where to apply it


  1. This is ideal for a UI when there is a need to flush data from any object without any connection.
  2. Data change messages, a kind of reactive code analog.
  3. For injection dependencies
  4. Global Callbacks

Weak spots


  1. Because of checks for dead subscribers and duplicates (cut out later), the code is slower than similar solutions
  2. The class / struct is used as the core of the event, so as not to allocate memory + upper problem, spamming is not recommended with events in the update)

General ideology


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

What the event body / data packet looks like in my case:
')
Sample Event Body
public struct ClickOnButtonEvent   {     public int ButtonID; //     enum    } 


Subscription to the event looks like:

 public static void AddListener<T>(object listener, Action<T> action) 

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

Example

 public class Example : MonoBehaviour { private void Awake() { EventAggregator.AddListener<ClickOnButtonEvent>(this, ClickButtonListener); } private void ClickButtonListener(ClickOnButtonEvent obj) { Debug.Log("  " + obj.ButtonID); } } 

To make it clear what the chip is, consider a more difficult case.


We have character icons that:
  1. Know which character they are attached to.
  2. Reflect the amount of mana, HP, exp, as well as statuses (stunning, blindness, fear, insanity)

And here you can make several 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 do we pass the character class in both cases? Here is the event subscriber and its handler:

 private void Awake() { EventAggregator.AddListener<CharacterNegativeStatusEvent> (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 we understand that we need it.
Why not let us subscribe directly to the Character class? And spam them?
This will be difficult to debug, better for a group of classes / events to create your own separate event.

Why, again, inside the event just do not put the Character and take everything from it?
So by the way, it is possible, but often in the classes there are visibility limitations, 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, the marker is not needed, the group of updated classes is rather a rarity. Usually, an event needs one specific entity - the controller / model of the view, which usually displays the state of the 1st character. And so there is always a banal solution - ID of different types (from inam, to complex hash, etc.).

What is under the hood and how does it work?


Directly aggregator code
 namespace GlobalEventAggregator public delegate void EventHandler<T>(T e); { public class EventContainer<T> : IDebugable { private event EventHandler<T> _eventKeeper; private readonly Dictionary<WeakReference, EventHandler<T>> _activeListenersOfThisType = new Dictionary<WeakReference, EventHandler<T>>(); private const string Error = "null"; public bool HasDuplicates(object listener) { return _activeListenersOfThisType.Keys.Any(k => k.Target == listener); } public void AddToEvent(object listener, EventHandler<T> 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<T> 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<Type, object> GlobalListeners = new Dictionary<Type, object>(); static EventAggregator() { SceneManager.sceneUnloaded += ClearGlobalListeners; } private static void ClearGlobalListeners(Scene scene) { GlobalListeners.Clear(); } public static void AddListener<T>(object listener, Action<T> action) { var key = typeof(T); EventHandler<T> handler = new EventHandler<T>(action); if (GlobalListeners.ContainsKey(key)) { var lr = (EventContainer<T>)GlobalListeners[key]; if (lr.HasDuplicates(listener)) return; lr.AddToEvent(listener, handler); return; } GlobalListeners.Add(key, new EventContainer<T>(listener, handler)); } public static void Invoke<T>(T data) { var key = typeof(T); if (!GlobalListeners.ContainsKey(key)) return; var eventContainer = (EventContainer<T>)GlobalListeners[key]; eventContainer.Invoke(data); } public static void RemoveListener<T>(object listener) { var key = typeof(T); if (GlobalListeners.ContainsKey(key)) { var eventContainer = (EventContainer<T>)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

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

 public class EventContainer<T> : IDebugable 

 private static Dictionary<Type, object> GlobalListeners = new Dictionary<Type, object>(); 

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

What does the container contain?

 private event EventHandler<T> _eventKeeper; private readonly Dictionary<WeakReference, EventHandler<T>> _activeListenersOfThisType = new Dictionary<WeakReference, EventHandler<T>>(); 

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

When you call a multidelegate, it checks whether dead keys are present, the collection is cleaned of corpses, and then a multidelegate is inserted with current subscribers. It takes time, but again, in fact, if the event functional is separated, then one event will have 3-5 subscribers, so the check is not so terrible, the benefit from comfort is more obvious. For network stories where subscribers can be a thousand or more - it is better not to use this aggregator. Although there remains an open question - if you remove the test for corpses, which is faster - iterate over an array of subscribers from 1k or call a multi-delegate from 1k subscribers.

Features of use


Subscription is best to push in Awake.

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

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

The aggregator cleans the list at unloading the scene. Some aggregators offer to clean the scene loading - this is the 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

Source: https://habr.com/ru/post/446130/


All Articles