How to make use of a message bus in Unity. This is a good solution to decouple components and logically organise how your game runs. Instead of doing all the dragging in the editor of game objects and components into scripts the message bus works a bit like a proxy for the communication between scripts. This is good (makes complex things easier to manage – ie. no noodling of dependencies everywhere – and no heavy FindObject calls – plus I get to make a funny bus pun.
Events
When using Events your Objects become Subscribers and Publishers of actions. When you subscribe (or listen) to a particular event channel you get notified when something changes. These notifications go out to every object that is listening on the bus and each object script can respond to the message in their own way.
You can make this as complex as you like but it gets harder the more complex the data is that you are passing around on the bus. This example uses just four components and our events are nice and simple:
- An enum that lists the Events on the Bus.
- The YellowBus class that handles subscribtions and publishing (by using a dictionary of events)
- A BigYellowBusController script that is attached to our bus game object and subscribes to all the “bus” events like starting, stopping at stops, taking on passengers, etc.
- A PassengerController that is attached to the Bus Rider game objects (the coloured circles) which subscribes and reacts to rider type events like calling the bus, getting on and off, and ringing the bell when you want to get off.
EventsOnTheBus
This is just a simple collection of named constants that are meaningful for our events.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace AdvancedCoding.YellowBus
{
public enum EventsOnTheBus
{
CALL_BUS, ALL_ABOARD, START_ENGINE, NEXT_STOP_PLEASE, STOP_ENGINE, EMERGENCY_STOP, END_O_THE_LINE
}
}
YellowBus
This class sets up the dictionary of events matching the EventsOnTheBus to a UnityEvent call. (An even simpler example of an event call is in the docs: https://docs.unity3d.com/ScriptReference/Events.UnityEvent.html).
It exposes the Subscribe, Unsubscribe, and Publish methods.
Notice the using UnityEngine.Events directive at the top.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
namespace AdvancedCoding.YellowBus
{
public class YellowBus
{
private static readonly IDictionary<EventsOnTheBus, UnityEvent>
Events = new Dictionary<EventsOnTheBus, UnityEvent>();
public static void Subscribe(EventsOnTheBus eventType, UnityAction listener)
{
UnityEvent thisEvent;
if (Events.TryGetValue(eventType, out thisEvent))
{
thisEvent.AddListener(listener);
}
else
{
thisEvent = new UnityEvent();
thisEvent.AddListener(listener);
Events.Add(eventType, thisEvent);
}
}
public static void Unsubscribe(EventsOnTheBus type, UnityAction listener)
{
UnityEvent thisEvent;
if (Events.TryGetValue(type, out thisEvent))
{
thisEvent.RemoveListener(listener);
}
}
public static void Publish(EventsOnTheBus type)
{
UnityEvent thisEvent;
if (Events.TryGetValue(type, out thisEvent))
{
thisEvent.Invoke();
}
}
}
}
BigYellowBusController
The controller on the bus is a subscriber to the events that it needs to react to and defines private methods of handling those events when they are broadcast. You can have any number of subscribers listening in to your events. In this example we only got two: The YellowBus and the Rider.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace AdvancedCoding.YellowBus
{
public class BigYellowBusController : MonoBehaviour
{
private string _an_event;
private bool _engine_is_running;
private bool _rider_waiting;
private bool _next_stop;
public float speed = 3;
void OnEnable()
{
YellowBus.Subscribe(EventsOnTheBus.START_ENGINE, StartBus);
YellowBus.Subscribe(EventsOnTheBus.STOP_ENGINE, StopBus);
YellowBus.Subscribe(EventsOnTheBus.EMERGENCY_STOP, Emergency);
YellowBus.Subscribe(EventsOnTheBus.CALL_BUS, RiderWaiting);
YellowBus.Subscribe(EventsOnTheBus.NEXT_STOP_PLEASE, NeedToGetOff);
}
void OnDisable()
{
YellowBus.Unsubscribe(EventsOnTheBus.START_ENGINE, StartBus);
YellowBus.Unsubscribe(EventsOnTheBus.STOP_ENGINE, StopBus);
YellowBus.Unsubscribe(EventsOnTheBus.EMERGENCY_STOP, Emergency);
YellowBus.Subscribe(EventsOnTheBus.CALL_BUS, RiderWaiting);
YellowBus.Subscribe(EventsOnTheBus.NEXT_STOP_PLEASE, NeedToGetOff);
}
private void StartBus()
{
_an_event = "Started!";
_engine_is_running = true;
}
private void StopBus()
{
_an_event = "Stopped!";
_engine_is_running = false;
}
private void Emergency()
{
_an_event = "Emergency Stop!";
_engine_is_running = false;
}
private void RiderWaiting()
{
_rider_waiting = true;
_an_event = "Rider waiting...";
}
private void NeedToGetOff()
{
_next_stop = true;
_an_event = "Passenger needs to get off...";
}
private void OnGUI()
{
GUI.color = Color.green;
GUI.Label(new Rect(300, 60, 200, 20), "BUS EVENT is... " + _an_event);
if (GUILayout.Button("Start Bus Up")) YellowBus.Publish(EventsOnTheBus.START_ENGINE);
if (GUILayout.Button("Stop Bus")) YellowBus.Publish(EventsOnTheBus.STOP_ENGINE);
}
public void Update()
{
if (_engine_is_running)
{
transform.position = new Vector3(transform.position.x + speed * Time.deltaTime, transform.position.y, transform.position.z);
if (transform.position.x > 18f)
{
transform.position = new Vector3(-17f, 0f, 0f);
}
if (_rider_waiting)
{
if (transform.position.x > 0 && transform.position.x < 1)
{
YellowBus.Publish(EventsOnTheBus.STOP_ENGINE);
YellowBus.Publish(EventsOnTheBus.ALL_ABOARD);
_rider_waiting = false;
}
}
if (_next_stop)
{
if (transform.position.x > 0 && transform.position.x < 1)
{
YellowBus.Publish(EventsOnTheBus.STOP_ENGINE);
YellowBus.Publish(EventsOnTheBus.END_O_THE_LINE);
_next_stop = false;
}
}
}
}
}
}
RiderController
The Rider Controller does pretty much the same as the Big Yellow Bus Controller but only listens to those events that relate to the riders (some of which it shares with the Big Yellow Bus Controller). Now here is where it gets interesting… look at the NEXT_STOP_PLEASE parts. Start with the OnGui() method below on line 75 where a button press will publish the NEXT_STOP_PLEASE event. In the OnEnable() method our Subscription code calls the PressTheBell() private method (line 17) that let’s our game object handle the call and produce the text message. BUT the YellowBus Controller also subscribes to this event and handles it with the NeedToGetOff() method so that it knows it has to stop and let the passenger off (line 21, 59 and 95 in the BigYellowBusController script above). AAANNDDDD responds by triggering the STOP_ENGINE event and the END_O_THE_LINE event. See how they can chain together into larger behaviours and complex interactions between different game objects without actually entangling them at the script reference level.
Also be aware that on these scripts there is a subscription in OnEnable() and an Unsubscribe in OnDisable() (so we are not holding on to the listener when the game object disappears) the thing to note here is that we are listening to our own events. For example the RiderController triggers the CALL_BUS event and both our own script and the BigYellowBusController script react to it.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace AdvancedCoding.YellowBus
{
public class RiderController : MonoBehaviour
{
private bool _lightMeGUIup;
private string _message;
private GameObject _bus;
void OnEnable()
{
YellowBus.Subscribe(EventsOnTheBus.CALL_BUS, HopOnTheBus);
YellowBus.Subscribe(EventsOnTheBus.NEXT_STOP_PLEASE, PressTheBell);
YellowBus.Subscribe(EventsOnTheBus.ALL_ABOARD, JumpOn);
YellowBus.Subscribe(EventsOnTheBus.END_O_THE_LINE, JumpOff);
}
void OnDisable()
{
YellowBus.Unsubscribe(EventsOnTheBus.CALL_BUS, HopOnTheBus);
YellowBus.Unsubscribe(EventsOnTheBus.NEXT_STOP_PLEASE, PressTheBell);
YellowBus.Unsubscribe(EventsOnTheBus.ALL_ABOARD, JumpOn);
YellowBus.Unsubscribe(EventsOnTheBus.END_O_THE_LINE, JumpOff);
}
private void Start()
{
_bus = GameObject.Find("BigYellowBus");
}
private void PressTheBell()
{
_message = "I need to get off the bus !!";
_lightMeGUIup = true;
}
private void HopOnTheBus()
{
_message = "I need to get on the bus please!!";
_lightMeGUIup = true;
}
private void JumpOn()
{
_message = "Hi Driver!";
transform.parent = _bus.transform;
transform.position = new Vector3(0f, 0.2f, 0f);
}
private void JumpOff()
{
_message = "Thanks a lot!";
transform.parent = null;
transform.position = new Vector3(0f, -3f, 0f);
}
private void OnGUI()
{
if (GUI.Button(new Rect(160, 10, 100, 30), "Call Bus"))
{
YellowBus.Publish(EventsOnTheBus.CALL_BUS);
}
if (GUI.Button(new Rect(160, 50, 100, 30), "Bell Press"))
{
YellowBus.Publish(EventsOnTheBus.NEXT_STOP_PLEASE);
}
if (_lightMeGUIup)
{
GUI.color = Color.blue;
GUI.Label(new Rect(300, 40, 200, 20), "..." + _message);
StartCoroutine(DropGUI());
}
}
private IEnumerator DropGUI()
{
yield return new WaitForSeconds(2f);
_lightMeGUIup = false;
}
}
}
The other thing to note is that this message bus is like a party line – all messages are broadcast to anyone listening. All the passengers respond to the NEXT_STOP_PLEASE event as they all share the same code…but of course they could implement different controller scripts per player and all handle this event differently. It’s pretty powerful stuff.
The code above was modified from an example in Game Development Patterns with Unity 2021: Explore practical game development using software design patterns and best practices in Unity and C#, 2nd Edition by David Baron – I recommend buying a copy it is the best book of this type that I have read – take yourself from a beginner to an intermediate Unity programmer. 🙂