Island Invasion Tech IV – Event-Driven Development

The key to being a good software engineer is knowing the right solution to the problem. For software with complex, distinct components, an event-driven solution may be what you want.

Unity’s Development Paradigm

Traditional Unity (before ECS) uses a component-oriented approach. That is, GameObjects have attached a number of disparate components. This works well, because each component should be responsible for one aspect of the entity’s logic. For example, the Renderer component knows how to show the GameObject, whereas a MovementController will move it about the scene. The two shouldn’t care what the other is doing, because their responsibilities are completely different. But sometimes components do need to talk to one another.

Here’s an example. Imagine I’m making a turn-based strategy game, and to start with, it’s going to be local multiplayer only. The game is controlled by a GameStateController, which switches between player’s turns. Each player has a PlayerController, which is responsible for reporting back to the GameStateController when the player has finished taking their turn. The code might look like this:

public class GameStateController
{
    private static PlayerController[] playerControllers;
    private static int currentPlayerIndex = 0;

    public static void StartGame()
    {
        playerControllers[currentPlayerIndex].StartTurn();
    }

    public static void FinishTurn()
    {
        currentPlayerIndex = Mathf.Repeat(currentPlayerIndex + 1, playerControllers.Length);
        playerControllers[currentPlayerIndex].StartTurn();
    }
}

public class PlayerController
{
    public void StartTurn()
    {
        // enable input, highlight first unit, etc
    }

    private void EndTurn()
    {
        GameStateController.FinishTurn();
    }
}

This is fine for this example, but what if we’re scaling up? Now we want a single-player campaign, so we would create an AIPlayerController. In that class, we would likely remove any input handling, and instead implement logic for the computer to make decisions on what to do.

We can utilise polymorphism to make the PlayerController a base class, and create two subclasses: a HumanPlayerController and an AIPlayerController. GameStateController doesn’t need to know if it’s starting a human or AI player’s turn, so this is fine. But now we have more classes to worry about. Keeping the code hierarchy as flat as possible makes for much easier development.

And wait, hold the phone, the design document actually mentions that the game should have networked multiplayer too! So now we need to make further changes to the PlayerController structure to account for networked players.

The Mediator Design Pattern

A mediator is an agent that directs a task from one agent to another. That is, something tells the mediator what to do, and it chooses what should then perform that task. I use the word “agent” because you can relate this to real-world agents, like an estate agent. Their job is to mediate between the buyer and seller of a property, making sure things are communicated properly, and all relevant paperwork is sorted out in time.

Mediator patterns helps somewhat with the problem of coupling. Coupling occurs when distinct aspects of code become dependent on one-another. A good example of this is the above, where the PlayerController and GameStateController have become coupled. Also, I say “helps somewhat”, because often the mediator then becomes the coupled aspect. Rather than distinct aspects talking to one another directly, instead they are dependent on a mediator. If the mediator isn’t reliable, it can become a single point of failure. Imagine an estate agent who is slow to respond and doesn’t file paperwork on time. You’d rather just speak to the current homeowner directly!

Let’s design the above code again, but using a mediator:

public class GameStateController
{
    public static void StartGame()
    {
        PlayerTurnMediator.StartNextTurn();
    }

    public static void EndGame()
    {
        // show end screen, go back to main menu, etc.
    }
}

public class PlayerTurnMediator
{
    private static PlayerController[] playerControllers;
    private static int currentPlayerIndex = 0;

    public static void StartNextTurn()
    {
        playerControllers[currentPlayerIndex].StartTurn();
    }

    public static void FinishTurn()
    {
        bool gameOver = true;

        foreach (PlayerController pc in playerControllers)
        {
            if (!pc.NoTurnsLeft)
            {
                gameOver = false;
                break;
            }
        }

        if (gameOver)
        {
            GameStateController.EndGame();
            return;
        }

        currentPlayerIndex = Mathf.Repeat(currentPlayerIndex + 1, playerControllers.Length);
        playerControllers[currentPlayerIndex].StartTurn();
    }
}

public class PlayerController
{
    public bool NoTurnsLeft = false;

    public void StartTurn()
    {
        // enable input, highlight first unit, etc.
    }

    private void EndTurn()
    {
        NoTurnsLeft = true;
        PlayerTurnMediator.FinishTurn();
    }
}

I’ve added some code because frankly, a classic mediator isn’t really want we need here – it just adds complexity. But it does demonstrate how the design pattern works. Now, the GameStateController is only responsible for the state the game is in. The PlayerMediator does the work to figure out which player’s turn it is, as well as checking all players to work out if the game is over. The PlayerController controls just the player’s turn, and tells the mediator when it’s done. So each component has a very distinct role.

I’ve tried using mediators (disguised as Managers or static Controllers) quite a few times in unfinished prototypes. The result, while better than direct communication, is often still untidy code. So what’s the solution?

Event-Driven Development

I’ll start with a disclaimer: event-driven development doesn’t solve all of your problems. At the start of this post, I said that a good software engineers knows the right way to solve a problem, of which there may be many. Event-driven development in my opinion is an excellent way to design games. But it might not be what you need in all circumstances.

Event-driven development is made up of a number of components:

  • Events, something that happens somewhere in the system
  • Publishers, something responsible for telling the system that the event has happened
  • Event “bus”, a system that carries or transmits the event
  • Subscribers, things that are interested in the event that has been published.

Let’s use the real-life example of a news article appearing on a website:

  • The event is something that happens out in the world, let’s say a bank robbery has occurred
  • A journalist will compile the event as an article, and publishes it to the website
  • The event bus here could be an RSS feed or other subscription method, which carries the event with a link to the published article
  • Finally, you receive a notification on your phone or desktop because you subscribed to that news website

The journalist and readers are not linked directly. However, they are both dependent on the event bus. From a design standpoint, this works really well, because the journalist can do their job, and the readers, wherever they are, can get the content they want when it’s available.

Admittedly not the best example, but hopefully it demonstrates a point. So how might this look in code? I’m going to use the EventSystem from Island Invasion to demonstrate, as it’s relatively mature:

namespace Curveball
{
    public class EventSystem
    {
        private static Dictionary<System.Type, ArrayList> subscriptions;

        public static void Subscribe<T>(UnityAction<T> handler, Object subscriber, bool checkAlreadySubscribed = false) where T : Event
        {
            if (subscriptions == null)
                subscriptions = new Dictionary<System.Type, ArrayList>();

            System.Type eventType = typeof(T);

            if (!subscriptions.ContainsKey(eventType))
                subscriptions.Add(eventType, new ArrayList());

            subscriptions[eventType].Add(new Subscription<T>(subscriber, handler));
        }
    }

    public abstract class Event {}
}

For simplicity, a lot of the logic has been removed; there is logic to work out whether a subscription already exists, and to unsubscribe, as well as other utility methods. But this is the core of it: something to subscribe to, and things subscribed to it.

If you’re unfamiliar with the intricacies of C# and object-oriented programming, such as generics, this may be a little confusing. So to summarise the above:

  • When subscribing, the event system maintains a list of subscribers, mapped against the type of event that has been published
  • When publishing, the event system runs through the list of subscribers mapped to that event type, and calls the function associated with that event
    • That listener will have the event as its only parameter, so the event will contain the state of the event

Rewriting what we have above, it looks a little like this:

public class StartTurnEvent : Event
{
    public int PlayerId;

    public StartTurnEvent(int playerId)
    {
        PlayerId = playerId;
    }
}

public class EndTurnEvent : Event { }

public class GameStateManager
{
    private const int PLAYER_COUNT = 4;
    private int currentPlayerId = -1;

    public GameStateManager()
    {
        EventSystem.Subscribe<EndTurnEvent>(OnPlayerTurnEnded);
        StartNextPlayersTurn();
    }

    void StartNextPlayersTurn()
    {
        currentPlayerId = Mathf.Repeat(currentPlayerId + 1, PLAYER_COUNT);

        EventSystem.Publish(new StartTurnEvent(currentPlayerId));
    }

    void OnPlayerTurnEnded(EndTurnEvent e)
    {
        StartNextPlayersTurn();
    }
}

public class PlayerController
{
    public int PlayerId;

    public PlayerController()
    {
        EventSystem.Subscribe<StartTurnEvent>(OnTurnStartEvent);
    }

    void StartTurn()
    {
        // do something until EndTurn
    }

    void EndTurn()
    {
        EventSystem.Publish(new EndTurnEvent());
    }

    void OnTurnStartEvent(StartTurnEvent e)
    {
        if (PlayerId == e.PlayerId)
        {
            StartTurn();
        }
    }
}

So what’s happening? We have two events: a StartTurnEvent and an EndTurnEvent. Because the EventSystem is just a “dumb” publisher, it shouldn’t be the one to work out who’s turn has started, so the players work this out for themselves. This is done by providing the (hopefully unique!) PlayerId in the event “payload”. Then, each player can work out whether it’s their turn or not. In this example, the GameStateController actually knows who’s turn it is, because we’ve assumed there are always 4 players. The key difference now is that the GameStateManager and PlayerController classes don’t need to know about one another.

Ok, but this is just more code than the previous example to achieve the same thing. Let’s add a turn start message component, as part of the UI:

public class TurnStartMessageController
{
    public GameObject TurnStartMessage;

    public TurnStartMessageController()
    {
        EventSystem.Subscribe<StartTurnEvent>(OnPlayerTurnStarted);
    }

    void ShowMessage()
    {
        TurnStartMessage.SetActive(true);

        // we'd probably want a timeout here to hide the message again afterwards, imagine it exists!
    }

    void OnPlayerTurnStarted(StartTurnEvent e)
    {
        ShowMessage();
    }
}

How easy is that? The PlayerController doesn’t need to do anything differently, and it doesn’t need to know about the TurnStartMessageController. This is great for development too, because it becomes really simple to add and remove components without touching existing classes.

Where the event system shines is where you have a lot of classes interested in the events happening in your game. So for example in Island Invasion, when the WaveCompletedEvent is fired:

  • EnemyWaveManager creates the next wave of enemies, ready to go
  • HUD shows the wave end message with your current score and resource gain
  • ConstructionManager iterates through your oil rigs and ore mines to add resources to your bank
  • MusicController plays the wave end jingle

Four very distinct components, none of which the EnemySpawnManager, which checked there are no enemies left alive or to spawn, cares about. Developing this way meant much less spaghetti code, driving from the fact that an event had occurred, rather than the event publisher being the one to decide what should happen when the wave has ended.

Pros and Cons

I’ll go back again to my original statement, that a good engineer knows the right solution to a given problem. The word “right” is important: it doesn’t always need to be the fastest, or shiniest, or even cleanest. But if it gets exactly the job you want doing done, it’s the right solution.

With that in mind, I’ve compiled a list of pros and cons to do with the event-driven approach:

  • Pro – code is decoupled, so components can stay within their own domain and not worry what everyone else is doing
  • Pro – similar to the above, components only need to work when they find out something has happened (an event), so they can lay idle for most of the time until they’re needed
  • Pro – adding extra components is as simple as writing a new class and subscribing to the event, no other code needs to be changed
  • Pro – similarly, removing a component is as simple as deleting the class that was subscribing to the event
  • Pro – events encapsulate the state of an occurrence at a point in time, so the subscriber can listen when they want
  • Pro – as events drive the state of the system, it is possible to store and replay all events to return to the same state every time (this is difficult in games due to the highly interactive and random nature of most games)
  • Pro – most concepts in a game’s lifecycle can be encapsulated in an event, e.g. start turn, end turn, attack enemy, update score…
  • Con – the event system becomes a single point of failure. If it breaks or isn’t well designed, the subscribers don’t have any logic to work out what to do
  • Con – the event system adds an extra processing step, which, for performance intensive games, may be too much overhead
  • Con – often, you will need some components to know about the state of others, so you will likely find a mix of event-driven and coupled code (e.g. the GameStateManager above might still check whether all the players are alive to end the game)
  • Con – event-driven development is a more complex paradigm than traditional OO programming, so may be confusing to some

No doubt there are more you can think of, but that’s the gist of it.

Further Reading
Island Invasion sounds like an amazing game, where can I play it?

I’m glad you asked.

Check out Island Invasion’s page on this website, or if you’re super-keen, there’s a link to Steam below.