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

  • 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

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

Generating Better Random Numbers

Image Magick Tricks

My Game Dev Process