Every game project needs an event system to prevent tight coupling between game systems and objects.
There are several approaches to implementing events, such as simply adding a UnityEvent to a MonoBehaviour
or going all in on the popular trend of SciptableObject events.
I am a big fan of event systems and have implemented many different variations. As a mostly solo indie developer though, some of these implementations can become time consuming and difficult to manage over time. I like the ScriptableObject
approach, but I don't have game designers who need to wire up events in the editor. So for me, with a developer-centric, small-team workflow I tend to favor something simple and effective.
A Simple Event System
After trying different approaches, these are the goals that this event system satisfies:
- Developer centric workflow, doesn't require wiring up in the editor
- Can quickly define new events of any type
- Requires a minimal number of classes, expandable for better team workflow with source control
- Follows a pub/sub model and is performant at scale
This approach involves creating 3 classes for your event system:
GameEvent
class- Generic
GameEvent<T>
class Events
static class that houses all of the defined events for the game
GameEvent Class
This is a simple C# class that defines a game event with no parameters and uses an Action with the event keyword to represent a multicast delegate.
While you could simply make the Action
public and allow subscribers to directly register themselves, I prefer to encapsulate it with simple Add
and Remove
methods. This gives me room to later add in validation or logging behavior to these methods if needed.
public class GameEvent
{
private event Action action = delegate { };
public void Publish()
{
action?.Invoke();
}
public void Add(Action subscriber)
{
action += subscriber;
}
public void Remove(Action subscriber)
{
action -= subscriber;
}
}
Generic GameEvent Class
Next, add a generic class for events that need to pass a type as a parameter.
public class GameEvent<T>
{
private event Action<T> action;
public void Publish(T param)
{
action?.Invoke(param);
}
public void Add(Action<T> subscriber)
{
action += subscriber;
}
public void Remove(Action<T> subscriber)
{
action -= subscriber;
}
}
You could keep adding classes for multiple types, such as public class GameEvent<S,T>
if needed, but I'd only do this when the requirement comes up.
Static Events Class
Lastly we now need somewhere to define and house events that can be used at runtime. When I need something global I typically ask, can it be static
in scope? If I don't need it to be visible in the Scene hierarchy, then I go with static instead of troubling myself with Singleton MonoBehaviours or ScriptableObject instances.
public static partial class Events
{
public static readonly GameEvent onJump = new();
public static readonly GameEvent<float> onDamage = new();
public static readonly GameEvent<GameObject> onPickup = new();
}
I marked this class partial, because after you start defining events, it can become a long list and if you have other team members modifying it, then it can introduce the risk of a merge conflict. With a partial class, you can organize your events into a multilpe .cs files, for example:
- Events_Menu.cs
- Events_Player.cs
- Events_Enemy.cs
Subscribing to Events
Subscribers will need to add and remove themselves in their OnEnable
and OnDisable
methods. It can be tedious, but I have snippets setup to quickly frame this out and fill in.
using UnityEngine;
public class Player: MonoBehaviour
{
private void OnEnable()
{
Events.onJump.Add(OnPickup);
Events.onDamage.Add(OnDamage);
}
private void OnDisable()
{
Events.onJump.Remove(OnPickup);
Events.onDamage.Remove(OnDamage);
}
private void OnPickup(GameObject item)
{
Debug.Log($"Picked up item: {item.name}");
}
private void OnDamage(float amount)
{
Debug.Log($"Damaged: {amount}");
}
}
Publishing Events
Publishing is as easy as a single line of code.
using UnityEngine;
public class Pickup: MonoBehaviour
{
private void OnCollisionEnter(Collision other)
{
if (other.CompareTag("player"))
{
Events.onPickup.Publish(gameObject);
}
}
}
Summary
This is a simple solution but it can be extended with logging, editor tooling to show subscribers during play mode, and because the Events class is marked partial
you can add multiple files to better organize your events or split them out by team members to avoid merge conflicts. Feel free to use this pattern, suggest improvements, and you can view a real world implementation in one of my recent game jam submissions, Moodledy.