Unity3D C# Type-safe Event Manager


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

/*
* Copyright 2017 Ben D'Angelo
*
* MIT License
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this
* software and associated documentation files (the "Software"), to deal in the Software
* without restriction, including without limitation the rights to use, copy, modify, merge,
* publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
* to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or
* substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
* PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
* FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
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 s_Instance = null;
private Queue m_eventQueue = new Queue();
public delegate void EventDelegate<T> (T e) where T : GameEvent;
private delegate void EventDelegate (GameEvent e);
private Dictionary<System.Type, EventDelegate> delegates = new Dictionary<System.Type, EventDelegate>();
private Dictionary<System.Delegate, EventDelegate> delegateLookup = new Dictionary<System.Delegate, EventDelegate>();
private Dictionary<System.Delegate, System.Delegate> onceLookups = new Dictionary<System.Delegate, System.Delegate>();
// override so we don'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<T>(EventDelegate<T> del) where T : GameEvent {
// Early-out if we'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) => 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<T> (EventDelegate<T> del) where T : GameEvent {
AddDelegate<T>(del);
}
public void AddListenerOnce<T> (EventDelegate<T> del) where T : GameEvent {
EventDelegate result = AddDelegate<T>(del);
if(result != null){
// remember this is only called once
onceLookups[result] = del;
}
}
public void RemoveListener<T> (EventDelegate<T> 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<T> (EventDelegate<T> 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("Event: " + e.GetType() + " has no listeners");
}
}
//Inserts the event into the current queue.
public bool QueueEvent(GameEvent evt) {
if (!delegates.ContainsKey(evt.GetType())) {
Debug.LogWarning("EventManager: QueueEvent failed due to no listeners for event: " + 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 > 0) {
if (LimitQueueProcesing) {
if (timer > 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;
}
}
view raw EventManager.cs hosted with ❤ by GitHub
  • 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.

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.

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.

Related Posts

Ruby Gems For Pulling Dictionary Words

Simple Explanation of the Pinyin Sounds

Failed Attempt at Creating a Video Search Engine

Test Your Chinese Using This Quiz

Using Sidekiq Iteration and Unique Jobs

Using Radicale with Gnome Calendar

Why I Regret Switching from Jekyll to Middleman for My Blog

Pick Random Item Based on Probability

Quickest Way to Incorporate in Ontario

Creating Chinese Study Decks