A Case for Static/Global Events in Game Development

If there’s one thing that I’ve learned during my transition from software engineer to game developer, it’s that traditional anti-patterns are often fair game. Singletons are a prime example of this, being a common staple for developers in Unity and are even a built-in feature for Godot1.

But I’m not here to make a case for or against Singletons. Instead I want to talk about events and static/global delegates. This is a pattern my former colleagues and I strived to avoid when architecting software.

The two biggest reasons to avoid this pattern were:

  1. How do you isolate these delegates when creating automated test suites?
  2. How to track and debug calling points and subscriptions?

But in the case of games, things are a bit trickier. Games are incredibly event oriented and are often a lot less stateless or deterministic when compared to more traditional software. Things like a rapid stream of user input, physics calculations, network data, and so on define a constant barrage of information on the system.

The bigger concerns in game development are things like raw performance2, order of operations, and a clear separation of concerns. The latter becomes increasingly important as more and more systems are added and expected to “plug and play” with one another.

This is where relying on static delegates can really offer a powerful benefit. Let’s put aside the traditional reasons for avoiding this pattern for now (debugging and testing)3 and look at some real world scenarios where this approach offers a lot of stability and benefit.

Example Scenario

Let’s say we’re creating a multiplayer shooter game, where players can damage one another using an assortment of weapons. In the code for our player characters, we have some logic around receiving damage and reducing the damaged player’s health. This code will also check if player’s health has been reduced to 0, meaning the player was incapacitated by their attacker.

I’m going to omit additional networking events here and just assume that we just want to handle these events locally for each client.

Outside of this player character code, we also want to…

Now here’s where this gets interesting. It’s quite likely that the player stats, the kill feed UI, and the hit marker are entirely different systems from one another.

We could try and gather references to all the relevant things we want to notify, but then our player character code would be responsible for a number of things outside of itself. It’s also possible that some of those components may not even be available because they’ve been destroyed, disabled, or haven’t even been instantiated yet.

Or we could pipe all of these events through a service or singleton, but what if it’s not available (yet)?

The beautiful thing about a static is that it’s always present. This can be its downfall and make this solution more fragile, but I believe that if proper precautions are taken it can actually be less fragile than what I listed above.

A Code Example in Unity (C#)

Let’s do an example of what we outlined above using Unity, simply because it reads well in an article format like this.

In Unity, we can easily use System.Action with the event and static keywords to create a quick solution to this problem. Here we see a static event delegate that our other services can subscribe to in their OnEnable method and clean up in the correspondingOnDisable method.

With static delegates like this, the clean up portion becomes even more important as these subscriptions will persist across scenes! If you don’t clean these up effectively, you can run the risk of memory leaks and null exceptions.

Let’s start with the PlayerCharacter class that is handling the damage logic. This class will contain our static event delegate, OnDamaged, that will be invoked when this character is damaged via their TakeDamage method.

using System;
using UnityEngine;

public class PlayerCharacter : MonoBehaviour {

    /** Event that is called when a player character is damaged. */
    public static event Action<PlayerCharacter, PlayerCharacter, float> OnDamaged
        = (victim, attacker, damage) => {};

    /** Applies damage to an instance of a player character. */
    public void ApplyDamage(PlayerCharacter attacker, float amount) {

        // perform your damage calculations here

        OnDamaged(this, attacker, amount);
    }

}

One thing to note here is the arguments we’re relaying via the OnDamaged delegate. For these global events to work well, it’s important to provide as much relevant information to them as possible. This is because we’ll need to infer the context of the action from an almost “purely functional” standpoint.

Now let’s do the KillFeed script.

using UnityEngine;

class KillFeed : MonoBehaviour {

    /** Start listening for damage events for all player characters when this component is enabled. */
    private void OnEnable() {
        PlayerCharacter.OnDamaged += AddKillToFeed;
    }

    /** Stop listening for damage events for all player characters when this component is disabled. */
    private void OnDisable() {
        PlayerCharacter.OnDamaged -= AddKillToFeed;
    }

    /** Add the "attacker -> kill" message to the kill feed. */
    private void AddKillToFeed(PlayerCharacter victim, PlayerCharacter attacker, float damage) {
    }

}

Obviously I didn’t include the code here for appending the UI element, but you can still probably see that creating that code would be fairly straight forward. If you wanted to include the weapon/mechanism used for the kill, you could easily extend our OnDamaged event signature with another argument for the damageCauser.

Moving on to the HitMarker component, you’ll see things don’t look that much different from KillFeed.

using UnityEngine;

class HitMarker : MonoBehaviour {

    /** Determines if the given player character is for the same player this script is attached to. */
    private bool IsOwningCharacter(PlayerCharacter character) {
    }

    /** Start listening for damage events for all player characters when this component is enabled. */
    private void OnEnable() {
        PlayerCharacter.OnDamaged += PlayHit;
    }

    /** Stop listening for damage events for all player characters when this component is disabled. */
    private void OnDisable() {
        PlayerCharacter.OnDamaged -= PlayHit;
    }

    /** Triggers the animation and sound(s) for the hit marker. */
    private void PlayHit(PlayerCharacter damaged, PlayerCharacter attacker, float damage) {
        if (IsOwningCharacter(attacker)) {
            // play hit marker animation + sounds
        }
    }

}

The biggest difference here is that the HitMarker is going to exist on a per-player basis. So we only want to play the hit marker animation and sound effect(s) if the event is reported with the associated player as the attacker.

Hopefully these examples alone are enough to see how you could further adopt this pattern in your PlayerStats and GameState classes. The responsbility for each system is nicely encapsulated and could easily be implemented in any order after PlayerCharacter was created - all without having to modify the PlayerCharacter class further.

It also means that each of these components can be toggled on/off for testing, different game modes, via user preferences, you name it.


  1. There’s somewhat of a caveat here, it’s really an object that’s mounted at runtime in a root part of the tree rather than a true software singleton. ↩︎

  2. Performance and optimization is a way bigger concern in game development than in a lot of traditional software. An API call taking an extra 200 milliseconds to far more permissable than a single part of your game loop taking a full 50 milliseconds. ↩︎

  3. Especially since you could probably work around these concerns with a bit of intelligent design. ↩︎

  4. A kill feed is a UI element that displays a running list of what players were killed and who killed them. Here’s more info from the Call of Duty wiki ↩︎

  5. A hit marker is a UI element, often a cross near the center of the screen, that appears when a player deals damage to an enemy. Here’s a video displaying an example ↩︎