DEV Community

Carlos Rodriguez
Carlos Rodriguez

Posted on

Enceladus Project Dev Blog 2: Game Event Message Bus

Intro

Very early on in the design of this project, I knew that I wanted to allow different game objects and entities to communicate with each other in a clean, decoupled manner. I haven't worked on many Unity3D projects, but the ones I have worked on quickly devolved into spaghetti; a bunch of objects calling other objects explicitly and being dependent on each other. This worked for smaller scale projects, but it quickly became a burden as the scope of the project increased. That's why I created this simple Message Bus class

There's not much to this class, but it has been a game changer for the development of this project. In this post, I'll go over the implementation, and give some examples of how it is used in The Enceladus Project.

Usage

Let's start with the usage of this message bus, as this class can really be used without knowing how it works. This class has two public methods, Subscribe<T> and Emit<T>. The types (T) that you're subscribing to or emitting can truly be any type, though they're meant to represent a particular Event in the game that other objects may be interested in. One example object from The Enceladus Project is the CompleteLandingEvent

public class CompleteLandingEvent
{
    public GameObject LandingPad { get; set; }
    public GameObject Ship { get; set; }
}

This event is meant to be emitted when a ship successfully lands at a Landing Pad. This is an event that potentially many parts of the game might care about, and so it was a good fit for an Event in our system. Here is how this event is actually emitted in the game.

GameEventMessageBus.Emit(new CompleteLandingEvent {
    Ship = gameObject,
    LandingPad = _landingPad
});

And just like that, we've let the game know that the ship Ship has succesfully landed at landing pad LandingPad. Of course, if there's no one who Subscribed to this event, then nothing will happen. So let's add someone who might care.

    // LandingPad.cs
    public void Start()
    {
        GameEventMessageBus
            .Subscribe<CompleteLandingEvent>(msg => ShipLanded(msg.Ship, msg.LandingPad));
    }

    private void ShipLanded(GameObject ship, GameObject landingPad)
    {
        if (landingPad != gameObject) 
            return;

        Animator.SetBool("OpenDoor", true);
    }

The above script is attached to landing pads. They care about the CompleteLandingEvent because, when a ship lands at a given landing pad, that landing pad needs to play a “open door” animation.

In the Start method, we let the message bus know that we care about that event by calling the Subscribe method. We specify the type of event we’re subscribing to, and we specify the action that should happen when the event is received (play the appropriate animation)

Now, when the "CompleteLandingEvent" is fired, our station doors should open.

Alt Text

The beauty of this approach is that the code that fires the CompleteLandingEvent in the first place doesn't know anything at all about the door animations. It just lets "anyone who is interested" know. I can add a second interested party afterwards (say, to open up the Station UI screen) without having to touch any of the existing code. And like I always say, code that doesn't change doesn't break.

Implementation

Here's the code again.

At the top, we instantiate our Dictionary of subscribers. It maps different C# types (our event types like the CompleteLandingEvent) to a list of Actions that need to happen when that event is fired.

private static Dictionary<Type, List<Action<object>>> _subscriberDict = new Dictionary<Type, List<Action<object>>>();

When a caller calls the Subscribe method, it'll execute this line

_subscriberDict[type].Add(new Action<object>(o => handler((T)o)));

Which adds a new Action to our List of Actions that must be performed when this event is called. There's some wrapping and casting happening here, since our lists are List<object> but our subscribers are expecting the specific type they're subscribed to, but in the end the action will call the handler with the object of the appropriate type.

Now, when someone calls the Emit method...

    public static void Emit<T>(T message)
    {
        var type = typeof(T);
        InitializeTypeKey(type);
        foreach(var handler in _subscriberDict[type])
            handler.Invoke(message);
    }

we iterate over each handler for that type, and just invoke that handler with the correct message.

Conclusion

This message bus is very simple, and still needs a little work (it'd be nice to have an unsubscribe method as well, to clean up any destroyed objects. I haven't needed it yet but I'm sure I will soon), but this is a great base that has led this project so far to have a neat, decoupled architecture.

Top comments (0)