The next step up from simple events is the complete decoupling of the event publisher and event receiver through an event aggregator. Consider the case of a game over message. The game is decided to end by a rule manager class. Lots of things want to subscribe to this event: the game over UI, the input manager, any systems that spawn players or items or track score. But aside from the game over state, none of those systems have any reason to take a dependency on the rule manager. Instead, we’d like to abstract the publishing of events and receiving of them through an intermediary: the event aggregator.
I’ve seen several event dispatching solutions for sale on the Asset Store, but I decided to roll my own. I based my approach on the EventAggregator from Microsoft’s wonderful Prism library.
Where most event dispatchers operate on a string or enumeration lookup, the EventAggregator uses generics to facilitate a robust and type-safe interface:
/// <summary> /// This function is called when the object becomes enabled and active. /// </summary> internal void OnEnable() { GlobalEventAggregator.GetEvent<GameOverEvent>().Subscribe(this.OnGameOver); } /// <summary> /// This function is called when the behaviour becomes disabled or inactive. /// </summary> internal void OnDisable() { GlobalEventAggregator.GetEvent<GameOverEvent>().Unsubscribe(this.OnGameOver); }
The GlobalEventAggregator simply provides a global point of access for a static EventAggregator instance:
/// <summary> /// Gets an instance of an event type. /// </summary> /// <typeparam name="TEventType">The type of event to get.</typeparam> /// <returns>An instance of an event object of type TEventType.</returns> public static TEventType GetEvent<TEventType>() where TEventType : EventBase, new() { return EventAggregator.GetEvent<TEventType>(); }
Events are defined as derivatives of NotificationEvent. The GameOverEvent has no arguments:
public class GameOverEvent : NotificationEvent { }
But another example has a simple value type argument:
public class ScoreEvent : NotificationEvent<int> { }
And still another defines a complex type to carry more information:
public struct FireMessage { public WeaponType weapon; public Vector3 position; public Vector3 direction; } public class FireEvent : NotificationEvent<FireMessage> { }
This means that every distinct event funneled through this system requires its own type definition. This may seem like overkill when a unique string will do just fine, but I find the strong type safety to be well worth the additional overhead. Not only that, but now my IDE can auto-generate event handlers with the correct arguments and validate the message type matches what I was expecting, which beats the pants off of a short list of “supported” argument types. Instead of an implicit contract where the consumer has to know which of a collection of arguments is applicable and which can’t be trusted, we get a strongly-typed message type with only the exact parameters we need. It’s a trade-off, but one that fits well with my programming style.