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 Subscribe
d 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.
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)