EventManager Overview
A type-safe event system for Unity based on the event listener pattern was originally described by Will Miller here http://www.willrmiller.com/?p=87. We use this pattern at Salty Dog. We made a few minor modifications, added some unit tests and example code, and published it as the asset EventManager. The full MIT licensed source code is located here https://github.com/robertwahler/EventManager.
This pattern is useful when you want to keep your codebase loosely coupled. Publishers and subscribers don’t need to know anything about each other. This is not a solution for exposing callbacks in the Unity Editor, Unity’s own EventSystem may be more appropriate for that use case.
Features
- Loosely coupled
- Type-safe
- Avoids breaking changes when new event parameters are added
Example Usage
The original article does a good job of explaining the purpose and the inner workings of this event system. This usage example, Colors Clicker (shown in screenshot below) will walk through a simple use case. The example scene, full code, and installation instructions are available in the EventManager repository.
Create Events
Events are straight-up C# classes based on SDD.Events.Event
namespace SDD.Events {
/// <summary>
/// Base event for all EventManager events.
/// </summary>
public class Event {
}
}
The Colors.Events.ButtonClickEvent
descends from SDD.Events.Event
and has a single simple property reference to Colors.ButtonHandler
. Events can have zero to as many properties as needed. This simple flexibility permits easily adding properties to events as a project evolves without breaking existing functionality.
namespace Colors.Events {
/// <summary>
/// Raised event signals a button was clicked
/// </summary>
public class ButtonClickEvent : SDD.Events.Event {
public ButtonHandler ButtonHandler { get; set; }
/// <summary>
/// Return a string
/// </summary>
public override string ToString(){
return string.Format("{0}, ButtonHandler {1}", base.ToString(), ButtonHandler);
}
}
}
Implement IEventHandler
EventManager provides a generic interface to allow MonoBehaviours to add and remove listener delegates.
namespace SDD.Events {
/// <summary>
/// Interface for event handlers
/// </summary>
public interface IEventHandler {
/// <summary>
/// Subscribe to events
///
/// @example
/// Events.AddListener<MoveResolvedEvent>(OnMoveResolved);
/// or
/// EventManager.OnSetRule += OnSetRule;
/// </summary>
void SubscribeEvents();
/// <summary>
/// Unsubscribe from events
///
/// @example
/// Events.RemoveListener<MoveResolvedEvent>(OnMoveResolved);
/// or
/// EventManager.OnSetRule -= OnSetRule;
/// </summary>
void UnsubscribeEvents();
}
}
Color Clicker EventHandler Abstract Class
This class is used as a base class for all event event handlers in the project. The implementation ensures event listeners are added and removed appropriately when MonoBehaviours are added and destroyed.
using UnityEngine;
using SDD.Events;
namespace Colors.Events {
/// <summary>
/// Event handler
/// </summary>
public abstract class EventHandler : MonoBehaviour, IEventHandler {
/// <summary>
/// Subscribe to events
///
/// @example
/// EventManager.Instance.AddListener<MoveResolvedEvent>(OnMoveResolved);
/// </summary>
public abstract void SubscribeEvents();
/// <summary>
/// Unsubscribe from events
///
/// @example
/// EventManager.Instance.RemoveListener<MoveResolvedEvent>(OnMoveResolved);
/// </summary>
public abstract void UnsubscribeEvents();
protected virtual void OnEnable() {
SubscribeEvents();
}
protected virtual void OnDisable() {
UnsubscribeEvents();
}
}
}
Concrete EventHandler Implementation
This header text handler responds to events by displaying text at the top of the screen. See screenshot above.
using UnityEngine;
using UnityEngine.UI;
using SDD.Events;
using Colors.Events;
namespace Colors {
/// <summary>
/// Header text handler
/// </summary>
public class HeaderText : EventHandler {
/// <summary>
/// Header text. Assign in IDE.
/// </summary>
public Text text;
public override void SubscribeEvents() {
Debug.Log(string.Format("HeaderText.SubscribeEvents() name {0}", name));
EventManager.Instance.AddListener<ButtonClickEvent>(OnButtonClickEvent);
EventManager.Instance.AddListener<ButtonRemovedEvent>(OnButtonRemovedEvent);
}
public override void UnsubscribeEvents() {
Debug.Log(string.Format("HeaderText.UnsubscribeEvents() name {0}", name));
EventManager.Instance.RemoveListener<ButtonClickEvent>(OnButtonClickEvent);
EventManager.Instance.RemoveListener<ButtonRemovedEvent>(OnButtonRemovedEvent);
}
/// <summary>
/// A button in the scene was clicked
/// </summary>
public void OnButtonClickEvent(ButtonClickEvent e) {
Debug.Log(string.Format("HeaderText.OnClick({0})", e));
string caption = string.Format("{0} '{1}' was clicked.\nEventManager.DelegateLookupCount is {2}", e.ButtonHandler.kind, e.ButtonHandler.name, EventManager.Instance.DelegateLookupCount);
text.text = caption;
}
/// <summary>
/// A button in the scene was destroyed
/// </summary>
public void OnButtonRemovedEvent(ButtonRemovedEvent e) {
Debug.Log(string.Format("HeaderText.OnButtonRemoved({0})", e));
string caption = string.Format("'{0}' was removed.", e.Name);
text.text = caption;
}
}
}
Raising Events
Events can be raised anywhere without knowing any details of the listening behaviours. Here is a snippet from the Unity EventSystem OnClick handler for the big colored buttons. This snippet creates the ButtonClickEvent each time it raises the event. This is fine for seldom raised events but can create a garbage collection issue if the event is raised often. Create a reference cache to prevent memory allocations after initialization. See the field buttonRemovedEvent for an example of zero allocations after initialization.
/// <summary>
/// OnClick handler for the button on this specific GameObject
/// </summary>
public void OnClick() {
Debug.Log(string.Format("ButtonHandler.OnClick() name {0}", name));
EventManager.Instance.Raise(new ButtonClickEvent(){ ButtonHandler=this });
}
Simple and Flexible
There you have it. Thanks for reading! We think EventManager is simple and flexible and makes event system maintenance and trouble-shooting about as easy as it gets in a fully decoupled architecture.
This article originally appeared on SaltyDog.digital