Usage Pattern for a Type-Safe Unity Event System

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

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 above) 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 {
  }
}

View on GitHub

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);
    }

  }
}

View on GitHub

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();

  }
}

View on GitHub

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 MonoBehviours are added and destoyed.

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();
    }

  }
}

View on GitHub

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;
    }

  }
}

View on GitHub

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 });
}

View on GitHub

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.

If you have appreciated this article or would like to see the games we are creating with this event system, please visit our Steam Greenlight page.

Our released game Fourtex Zen also uses this event system.

Author image
Coder with more gadgets that you can fit in a ten ton truck! Robert provides technical leadership and does the magic that makes the games work.
US
top