Unity3D C# Type-safe Event Manager

on unity3d

I been searching for a good event manager for Unity3D but haven't found one that suited all my needs.

I wanted an event manager with:

Type-Safe Events

This makes refactoring easier by using class names as the listener type instead of string names, typos can be prevented. This also prevents casting the base event to the event needed.

Event Queue

An event manager centric game will have a lot of events controlling every aspect of the game. So being able to queue events for the next frame will ensure not too many events will fire at once. It will prevent the game from advancing forward too quickly (event trigger chains) and will also help with the frame rate.

For frame sensitive events, I wanted to occasionally bypass the queueing functionally. So a direct trigger event method had to be available as well.

I ended up creating my own modified event manager from these:

What is an Event Manager

An event manager is generally a singleton that triggers events from anywhere in a game. It is a great way to decouple communication between objects by encapsulating the communication in an event.

For example when a monster takes damage a sound should be played and a damage number should appear on screen. Normally it would be coded like this.

public void TakeDamage(int damage, Monster monster, Attack attacker){
    monster.health.Minus(damage);

    this.soundManager.PlaySound("Ouch");
    this.guiManager.DisplayDamage(monster, "-" + damage);
}

This looks perfect at the start of the project, but will quickly turn into a nightmare. What if you don't want damage to always be displayed? Like if the game is currently in a cutscene? And how will that object get references to the sound manager and gui manager? Will they be passed through every object in the game?

Game logic, like taking damage, should always be completely decoupled from view logic like displaying points gained and sound effects.

public void TakeDamage(int damage, Monster monster, Attacker attacker){
    monster.health.Minus(damage);

    EventManager.Instance.QueueEvent(new TakeDamageEvent(damage, monster,
    attacker));
}

First a specific event is created to contain all the data. In this case TakeDamageEvent. It is then queued up to be triggered on the next frame. This prevents too many events from triggered all in the same frame. This event is then picked up by whoever is listening for it. In this case it would be the sound manager and gui manager.

public class TakeDamageEvent : GameEvent {
    public Monster monster { get; private set; }
    public int damage { get; private set; }
    public Attacker attacker { get; private set; }

    public TakeDamageEvent(int damage, Monster monster, Attacker attacker){
        this.damage = damage;
        this.monster = monster;
        this.attacker = attacker;
    }
}

A new event has to be created for every type of event. This probably sounds like a lot of work but I keep all my events in one file called "events.cs".

public class GuiManager {
    public void SetupListeners(){
        EventManager.Instance.AddListener<TakeDamageEvent>(OnTakeDamage);
    }

    public void Dispose(){
        EventManager.Instance.RemoveListener<TakeDamageEvent>(OnTakeDamage);
    }

    public void OnTakeDamage(TakeDamageEvent event){
        if(NotInCutscene){
            this.DisplayDamage(event.monster, "-" + event.damage);
        }
    }
}

The view logic and game logic are now completely decoupled. This makes game development so much easier. I have created games without an event manager and excluding non-trivial games, have always turned into balls of spaghetti.

For those of you who don't like global objects, neither do I but an event manager is worth it.

Event Manager Source

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class EventManager : MonoBehaviour { public bool LimitQueueProcesing = false; public float QueueProcessTime = 0.0f; private static EventManager sInstance = null; private Queue meventQueue = new Queue();

public delegate void EventDelegate&lt;T&gt; (T e) where T : GameEvent;
private delegate void EventDelegate (GameEvent e);

private Dictionary&lt;System.Type, EventDelegate&gt; delegates = new Dictionary&lt;System.Type, EventDelegate&gt;();
private Dictionary&lt;System.Delegate, EventDelegate&gt; delegateLookup = new Dictionary&lt;System.Delegate, EventDelegate&gt;();
private Dictionary&lt;System.Delegate, System.Delegate&gt; onceLookups = new Dictionary&lt;System.Delegate, System.Delegate&gt;();

// override so we don&#39;t have the typecast the object
public static EventManager Instance {
    get {
        if (s_Instance == null) {
            s_Instance = GameObject.FindObjectOfType (typeof(EventManager)) as EventManager;
        }
        return s_Instance;
    }
}

private EventDelegate AddDelegate&lt;T&gt;(EventDelegate&lt;T&gt; del) where T : GameEvent {
    // Early-out if we&#39;ve already registered this delegate
    if (delegateLookup.ContainsKey(del))
        return null;

    // Create a new non-generic delegate which calls our generic one.
    // This is the delegate we actually invoke.
    EventDelegate internalDelegate = (e) =&gt; del((T)e);
    delegateLookup[del] = internalDelegate;

    EventDelegate tempDel;
    if (delegates.TryGetValue(typeof(T), out tempDel)) {
        delegates[typeof(T)] = tempDel += internalDelegate; 
    } else {
        delegates[typeof(T)] = internalDelegate;
    }

    return internalDelegate;
}

public void AddListener&lt;T&gt; (EventDelegate&lt;T&gt; del) where T : GameEvent {
    AddDelegate&lt;T&gt;(del);
}

public void AddListenerOnce&lt;T&gt; (EventDelegate&lt;T&gt; del) where T : GameEvent {
    EventDelegate result = AddDelegate&lt;T&gt;(del);

    if(result != null){
        // remember this is only called once
        onceLookups[result] = del;
    }
}

public void RemoveListener&lt;T&gt; (EventDelegate&lt;T&gt; del) where T : GameEvent {
    EventDelegate internalDelegate;
    if (delegateLookup.TryGetValue(del, out internalDelegate)) {
        EventDelegate tempDel;
        if (delegates.TryGetValue(typeof(T), out tempDel)){
            tempDel -= internalDelegate;
            if (tempDel == null){
                delegates.Remove(typeof(T));
            } else {
                delegates[typeof(T)] = tempDel;
            }
        }

        delegateLookup.Remove(del);
    }
}

public void RemoveAll(){
    delegates.Clear();
    delegateLookup.Clear();
    onceLookups.Clear();
}

public bool HasListener&lt;T&gt; (EventDelegate&lt;T&gt; del) where T : GameEvent {
    return delegateLookup.ContainsKey(del);
}

public void TriggerEvent (GameEvent e) {
    EventDelegate del;
    if (delegates.TryGetValue(e.GetType(), out del)) {
        del.Invoke(e);

        // remove listeners which should only be called once
        foreach(EventDelegate k in delegates[e.GetType()].GetInvocationList()){
            if(onceLookups.ContainsKey(k)){
                delegates[e.GetType()] -= k;

                if(delegates[e.GetType()] == null)
                {
                    delegates.Remove(e.GetType());
                }

                delegateLookup.Remove(onceLookups[k]);
                onceLookups.Remove(k);
            }
        }
    } else {
        Debug.LogWarning(&quot;Event: &quot; + e.GetType() + &quot; has no listeners&quot;);
    }
}

//Inserts the event into the current queue.
public bool QueueEvent(GameEvent evt) {
    if (!delegates.ContainsKey(evt.GetType())) {
        Debug.LogWarning(&quot;EventManager: QueueEvent failed due to no listeners for event: &quot; + evt.GetType());
        return false;
    }

    m_eventQueue.Enqueue(evt);
    return true;
}

//Every update cycle the queue is processed, if the queue processing is limited,
//a maximum processing time per update can be set after which the events will have
//to be processed next update loop.
void Update() {
    float timer = 0.0f;
    while (m_eventQueue.Count &gt; 0) {
        if (LimitQueueProcesing) {
            if (timer &gt; QueueProcessTime)
                return;
        }

        GameEvent evt = m_eventQueue.Dequeue() as GameEvent;
        TriggerEvent(evt);

        if (LimitQueueProcesing)
            timer += Time.deltaTime;
    }
}

public void OnApplicationQuit(){
    RemoveAll();
    m_eventQueue.Clear();
    s_Instance = null;
}

}

  • AddListener: Adds listener to the given event.
  • AddListenerOnce: Adds listener and on the first trigger the listener is subsequently removed.
  • RemoveListener: Removes given listener.
  • HasListener: Checks if the listener is registered.
  • QueueEvent: Queues the even to trigger next frame.
  • TriggerEvent: Triggers the event to all listeners.
  • RemoveAll: Removes all listeners and queued events.

Local Usage

Normally the event manager is used globally and events are sent to everyone. In some cases this behaviour is not wanted. One problem I encountered is do you how listen to animation events from a specific game object?

public void OnComplete(AnimCompleteEvent event){
    this.cutsceneManager.MoveToNextCutscene();
}

EventManager.Instance.AddListener<AnimCompleteEvent>(OnComplete);

EventManager.Instance.TriggerEvent(new AnimCompleteEvent(monster, animHash));

This seems to be the desired behaviour but what if there are multiple animations running at once? The cutscene manager could possibly run to the next frame because a different monster finished its animation. Only animation complete events from a specific monster is wanted. You could try to do a simple if condition to check the wanted monster is correct but this will get tedious and is error-prone.

posts/unity3d_event_manager/eventManager.png

So I added the event manager to the specific monster prefab. Now listeners can be added directly to it.

monster.GetComponent<EventManager>().AddListener<AnimCompleteEvent>(OnComplete);

Triggering Events From Animations

I just want to mention how I setup triggering event manager events from Unity3ds animation events. I ended up creating an AnimEvents component which converts Unity3ds events.

posts/unity3d_event_manager/animEvents.png

public class AnimEvents : MonoBehaviour {

    private Entity _entity;
    private Animator _animator;
    private EventManager _eventManager;

    void Start(){
        _animator = GetComponent<Animator>();
        _entity = transform.parent.gameObject.GetComponent<Entity>();
        _eventManager = _entity.GetComponent<EventManager>();
    }

    private int GetHash(){
        return _animator.GetCurrentAnimatorStateInfo(0).nameHash;
    }

    public void OnComplete(string val){
        _eventManager.TriggerEvent(new AnimCompleteEvent(_entity, GetHash(), val));
    }

}

I use the AddListenerOnce() method because usually the entity will be moved to an other animation and subsequent events are not needed.

entity.GetComponent<EventManager>().AddListenerOnce<AnimCompleteEvent>(OnComplete);

private void OnComplete(AnimCompleteEvent e){
    // move to a different frame
}

Further Reading

Game Coding Complete - Has a great chapter on global event managers. Source code can be viewed for free.