Introduction

A game is usually made up of many components, such as a scene, which contains various game objects, such as the character, enemies, bosses, power-ups, a tile map, explosions and particle effects. In addition, there could be a HUD that shows the score, health points and possibly a mini-map with the positions of various objects and items.

It is usually necessary for all of these components, or at least some of them, to exchange information with each other. For example, if the character is hit by an enemy, it loses health points, which causes the HUD to update. If the character destroys an enemy, the player is credited points and the defeated enemy could leave a power-up behind. The possibilities are endless.

You could now have the components communicate directly with each other. However, this often results in complex dependencies between different components, which can make the code difficult to understand and maintain.

Instead of making the components directly dependent on each other, another idea is to set up kind of central hub on which the components depend and through which all communication takes place. The components do not even have to know each other, but only that hub.

Events and Commands

The basic idea is that components can do two things: first, they can register message handlers to receive messages, and second, they can send messages to the hub, which are then forwarded to the appropriate handlers.

Messages typically come in two flavors: events and commands. While technically they are both implemented more or less similarly, there is a semantic difference between them:

An event is a fact that happened in the past, and to which other components (a.k.a. observers) can subscribe in order to react to them, e.g. a power up was collected.

A command is an instruction to perform an action, e.g. add a game object to the scene. While events can have multiple subscribers, commands can only have one subscriber, and this subscriber is responsible for executing the command. Additionally, a command can have a return value.

Distinguishing between events and commands can help keeping the code more clear and easier to maintain.

Implementation

Quick Overview

Since this hub receives messages and forwards them to the appropriate handlers, the component can be named message dispatcher.

Here is a quick diagram showing the flow of events and commands from some components via the message dispatcher forwarded to other components:

Internally, the message dispatcher consists of an event dispatcher and a command dispatcher. Both could as well be components of their own, we’ll just put them both into one message dispatcher for simplicity.

Two components A and B, each publishing an event X and Y respectively to the message dispatcher, which in turn forwards the events to components D, E for them to handle. A component C sends a command Z to the message dispatcher, which forwards it to component F. Components A, B and C don’t know anything about components D, E and F and vice versa. All they all know of is the message dispatcher, which acts as a kind of switchboard.

Here’s a possible implementation in C# - first, we’ll create two marker interfaces for events and commands, and then we’ll define interfaces for event and command related functionality:

public interface IEvent;

public interface ICommand;

public interface IEventDispatcher
{
    public void Subscribe<TEvent>(Action<TEvent> eventHandler) where TEvent : IEvent;
    public void Unsubscribe<TEvent>(Action<TEvent> eventHandler) where TEvent : IEvent;
    public void Publish<TEvent>(TEvent @event) where TEvent : IEvent;
}

public interface ICommandDispatcher
{
    void Register<TCommand>(Action<TCommand> commandHandler) where TCommand : ICommand;
    void Unregister<TCommand>(Action<TCommand> commandHandler) where TCommand : ICommand;
    void Send<TCommand>(TCommand command) where TCommand : ICommand;
}

Then, we create a combined message dispatcher interface and a concrete implementation:

public interface IMessageDispatcher : IEventDispatcher, ICommandDispatcher;
public class MessageDispatcher : IMessageDispatcher
{
    private readonly Dictionary<Type, List<Delegate>> _eventHandlers = new();
    private readonly Dictionary<Type, Delegate> _commandHandlers = new();

    public void Subscribe<TEvent>(Action<TEvent> eventHandler) where TEvent : IEvent
    {
        var eventType = typeof(TEvent);
        if (!_eventHandlers.TryGetValue(eventType, out var @delegate)) {
            @delegate = [];
            _eventHandlers[eventType] = @delegate;
        }

        @delegate.Add(eventHandler);
    }

    public void Unsubscribe<TEvent>(Action<TEvent> eventHandler) where TEvent : IEvent
    {
        var eventType = typeof(TEvent);
        if (_eventHandlers.TryGetValue(eventType, out var handlers)) {
            handlers.Remove(eventHandler);
        }
    }

    public void Publish<TEvent>(TEvent @event) where TEvent : IEvent
    {
        var eventType = typeof(TEvent);
        if (!_eventHandlers.TryGetValue(eventType, out var handlers)) {
            return;
        }

        foreach (var handler in handlers) {
            if (handler is Action<TEvent> action) {
                action(@event);
                continue;
            }

            // Fallback for when @event is only an IEvent
            handler.DynamicInvoke(@event);
        }
    }

    public void Register<TCommand>(Action<TCommand> commandHandler) where TCommand : ICommand
    {
        var commandType = typeof(TCommand);

        if (!_commandHandlers.TryAdd(commandType, commandHandler)) {
            throw new InvalidOperationException($"Handler already registered for {commandType.Name}");
        }
    }

    public void Unregister<TCommand>(Action<TCommand> commandHandler) where TCommand : ICommand
    {
        _commandHandlers.Remove(typeof(TCommand));
    }

    public void Send<TCommand>(TCommand command) where TCommand : ICommand
    {
        var commandType = typeof(TCommand);

        if (_commandHandlers.TryGetValue(commandType, out var handler)) {
            if (handler is Action<TCommand> action) {
                action(command);
                return;
            }

            // Fallback for when command is only an ICommand
            handler.DynamicInvoke(command);
            return;
        }

        throw new InvalidOperationException($"No handler registered for command {commandType}");
    }
}

Application

Now, how can we use this in a game? Let’s look at the following example: we want to implement a space shooter where the player steers a ship which is always on the brink of being damaged by enemy bullets. An enemy ship spawns a bullet, which hits the player ship. As a result, the player ship loses health points and the bullet is destroyed.

Let’s further assume that we organize all our game objects through a scene component, which holds everything together, i.e. is responsible for spawning and destroying game objects, calling the Update (and Draw) methods, and probably more.

The scene and the game objects know the message dispatcher. The scene also knows the game objects. The game objects, on the other hand, know nothing about other game objects or the scene. These should remain decoupled. Here is a small UML diagram that shows the dependencies:

Now, let’s look at the example above regarding the player ship being damaged by enemy bullets. We have the following commands:

  • SpawnBulletCommand - this command is sent to the message dispatcher when a bullet should be spawned and added to the scene.
  • DestroyBulletCommand - this command is sent to the message dispatcher when a bullet should be destroyed and removed from the scene.

Then, we have the following event:

  • HealthChangedEvent - this event is published when the player ship’s health points change.

The collision between the enemy bullet and the player ship could also be seen as an event, however, we’ll leave that out of our example and simply assume that the bullet and the ship are notified about their collision by some sort of collision detection system.

Below is a sequence diagram showing a possible sequence of steps for the example above. Since the message dispatcher is a central component, it is highlighted with a green background. Click image to enlarge.

Sequence Diagram Sequence Diagram

A sequence diagram showing the flow of commands and events

To keep the diagram from getting even more cluttered, we’ll omit the details of registering and subscribing to commands and events.

The steps in detail:

  1. At some point an enemy ship sends a SpawnBulletCommand to the message dispatcher
  2. The message dispatcher calls the scene’s handler for the SpawnBulletCommand
  3. The scene spawns an enemy bullet and adds it to the list of game objects
  4. A few frames later, the enemy bullet collides with the player ship
  5. The bullet sends a DestroyBulletCommand to the message dispatcher …
  6. … which in turn calls the scene’s handler for removing the bullet from the scene
  7. The bullet is removed from the scene
  8. Then, the player ship loses health points …
  9. … and publishes a HealthChangedEvent
  10. The message dispatcher calls the HUD’s handler …
  11. … for updating the health bar
  12. The message dispatcher calls the audio player’s handler …
  13. … for playing the sound when health points have changed

Note that there is only one handler for the command, but two handlers for the event.

Now, let’s whip up a few classes, all boiled down to the absolute minimum. First, we need a command for spawning bullets at a certain position and a command for destroying bullets and removing them from the scene:

public class SpawnBulletCommand : ICommand
{
    public required Vector2 Position { get; init; }
}

public class DestroyBulletCommand : ICommand
{
    public required GameObject Bullet { get; init; }
}

Next, we need an event for when the player ship loses health points:

public class HealthChangedEvent : IEvent
{
    public required int Health { get; init; }
}

Then we need an abstract base class for game objects. A game object needs a position in the game world, and it needs a reference to the message dispatcher to be able to send commands, publish events, but also to be able to subscribe to events, should the need arise:

public abstract class GameObject(IMessageDispatcher messageDispatcher)
{
    protected IMessageDispatcher MessageDispatcher => messageDispatcher;

    public Vector2 Position { get; set; }
    
    public virtual void Update(GameTime gameTime)
    {
    }
    
    // ...
}

Now that we have a base class for game objects, we can define concrete classes for our game objects. Let’s begin with an interface that defines the contract for game objects that can deal damage, like enemy bullets:

public interface IDamaging
{
    int Damage { get; }
}

Now the concrete game objects:

public class EnemyShip(IMessageDispatcher messageDispatcher) : GameObject(messageDispatcher)
{   
    private float _coolDown = 2000f;
     
    public override void Update(GameTime gameTime)
    {
        // Spawn a bullet every two seconds
        
        _coolDown -= gameTime.ElapsedGameTime.TotalMilliseconds;
        
        if (_coolDown <= 0) {
            _coolDown = 2000f;
            MessageDispatcher.Send(new SpawnBulletCommand { Position = Position });
        }
    }
}

public class EnemyBullet(IMessageDispatcher messageDispatcher) : GameObject(messageDispatcher), IDamaging
{
    public int Damage => 10;
    
    public override void Update(GameTime gameTime)
    {
        // move bullet
    }
    
    // This method is called magically by some sort of collision detection system
    public void OnCollisionEnter(GameObject _)
    {
        MessageDispatcher.Send(new DestroyBulletCommand { Bullet = this });  
    }
}

public class Ship(IMessageDispatcher messageDispatcher) : GameObject(messageDispatcher)
{
    private int _health = 100;
    
    // This method is called magically by some sort of collision detection system
    public void OnCollisionEnter(GameObject other)
    {
        if (other is IDamaging damaging) {
            _health -= damaging.Damage;
            MessageDispatcher.Publish(new HealthChangedEvent { Health = _health });
            
            if (_health <= 0) {
                // Game Over
            }       
        }                
    }
}

Every two seconds, the enemy ship’s Update method sends a SpawnBulletCommand to the message dispatcher. We hand of the task of creating bullets to somewhere else, we needn’t bother with this here. Later we’ll look at how to handle bullet spawning.

Let’s look at the EnemyBullet class next. It has a method OnCollisionEnter that is called magically by some sort of collision detection system, and which is responsible for handling collisions. If the bullet collides with anything, we want it destroyed. To do that, we send a DestroyBulletCommand to the message dispatcher.

Then, there is the Ship class which represents the player ship. It too has a method OnCollisionEnter that is called by a collision detection system. On collision it checks whether the other object is damaging, and if so, decreases the ship’s health and publishes a HealthChangedEvent event with the current health as payload.

Next, we need classes for the HUD, the audio player, and the scene that holds it all together:

public class HUD
{    
    private int _playerHealth;
    
    public HUD(IMessageDispatcher messageDispatcher)
    {
        messageDispatcher.Subscribe<HealthChangedEvent>(OnHealthChanged);
    }   
    
    public void OnHealthChanged(HealthChangedEvent @event)
    {
        _playerHealth = @event.Health;
    }
    
    public void Draw()
    {
        // draw health bar
    }
}

public class AudioPlayer
{
    public AudioPlayer(IMessageDispatcher messageDispatcher)
    {
        messageDispatcher.Subscribe<HealthChangedEvent>(OnHealthChanged);
    }
    
    private void OnHealthChanged(HealthChangedEvent @event)
    {
        // play health changed sound
    }
}

public class Scene
{
    private readonly IMessageDispatcher _messageDispatcher = new MessageDispatcher();
    private readonly HUD _hud = new HUD(_messageDispatcher);
    private readonly AudioPlayer _audioPlayer = new AudioPlayer(_messageDispatcher);
    private readonly List<GameObject> _gameObjects = [];

    public Scene()
    {
        _gameObjects.Add(new PlayerShip(_messageDispatcher));
        
        _messageDispatcher.Register<SpawnBulletCommand>(SpawnBullet);
        _messageDispatcher.Register<DestroyBulletCommand>(DestroyBullet);
    }

    private void SpawnBullet(SpawnBulletCommand command)
    {
        var bullet = new Bullet(_messageDispatcher) { 
            Position = @event.Position 
        };
        AddGameObjectToScene(bullet);
    }
    
    private void DestroyBullet(DestroyBulletCommand command)
    {
        RemoveGameObjectFromScene(command.Bullet);
    }

    public void Update(GameTime gameTime)
    {
        // Update game objects
    }
    
    private void AddGameObjectToScene(GameObject gameObject)
    {
        // Add game object
    }
    
    private void RemoveGameObjectFromScene(GameObject gameObject)
    {
        // Remove game object
    }
}

Both the HUD and the audio player register handlers for the HealthChangedEvent: the HUD updates the health bar, and the audio player plays a sound effect. And that’s essentially it.

Conclusion

The message dispatcher helps keep your game components loosely coupled by centralizing communication. Instead of direct dependencies, components publish and subscribe to events, and send and register for commands, making the codebase easier to manage. This pattern isn’t a silver bullet, but it’s a practical way to reduce complexity as your game logic grows.