The essence of the pattern is that it creates a centralized bus through which components can send and receive messages (events). The bus acts as a mediator between the components, and they can exchange messages without the need for explicit coupling with each other. Hello everyone, my name is Dmitrii Ivashchenko, and I’m a Software Engineer at MY.GAMES. In this article, we’ll talk about an enum-based event bus in Unity, which we’ll get to in just a moment. Back to the event bus pattern itself, there are several ways to implement this, but the general idea remains the same: components send messages (events) to the bus, and other components subscribe to these events and receive them. There are two main methods to implement an event system: and : Subscribe Raise // At the point where the event occurs, we call Raise // with the event type and data EventBus.Raise(some_event_type, arguments); // In the handler, we subscribe to a specific type // of events and call the OnSomeEventRaised method EventBus.Subscribe(some_event_type, OnSomeEventRaised); Often the event type is set as a string, but there are also implementations where a class for the passed data is used as the event type: public struct BuyEvent { public int ItedId; } // Raising a new event EventBus.Raise<BuyEvent>(new BuyEvent{ ItemId = 1 }); // Subscription to event EventBus.Subscribe<BuyEvent>(OnPlayerBuy); There are also more interesting implementations where interfaces are used for subscription. The event bus can be shared across the entire code, but there may also be separate buses that differ in purpose and method of message delivery. Another interesting way to implement EventBus is Enum-based. Why enums? Using enums as event types has several advantages: Improved code readability Low overhead for Enum processing Code autocompletion in IDE, with ease of adding new types Eliminated possibility of incorrect spelling, which is the case when using strings It adds the possibility of adding extension methods to extend functionality To more easily illustrate the main principle, some details have been omitted in the code examples below. We’ll also be using the class from the library to transmit events. Instead of UniRx, any other means of storing message handlers could also be used, such as C# delegates and events. ReactiveCommand UniRx . The complete source code is available on GitHub Using an Enum as a Event Bus One way to view an is as a type of data bus, with its elements representing concrete types of events delivered by this bus: Enum // Event bus for user interface-generated events public enum UiEvent { // Event on clicking on the "buy" button BuyButtonClicked, // Event on opening an in-game window ScreenOpened, // Event on completing the tutorial TutorialFinished, ... } Using C# extension methods, we can add methods for sending and subscribing to events to this enum: public static class UiEventExtensions { // Declare a static readonly ReactiveCommand of type UiEvent private static readonly ReactiveCommand<UiEvent> _uiEventCommand = new ReactiveCommand<UiEvent>(); // Extension method to raise a UiEvent public static void Raise(this UiEvent evnt) { // Execute the ReactiveCommand with the UiEvent _uiEventCommand.Execute(evnt); } // Extension method to subscribe to a UiEvent public static IDisposable Subscribe(this UiEvent evntType, Action action) { // Return a subscription to the ReactiveCommand, // filtered by the UiEvent type return _uiEventCommand.Where(evnt => evnt == evntType) .Subscribe(evnt => action()); } } Now, we can send and receive events in the game logic like so: // Raise the BuyButtonClicked event **UiEvent.BuyButtonClicked.Raise();** // Subscribe to the BuyButtonClicked event var disposable = **UiEvent.BuyButtonClicked.Subscribe**(OnBuyButtonClicked); // ... // Unsubscribe from the event disposable.Dispose(); To pass parameters together with an event, we’ll need an additional class that will store the event type and data. Example implementation: // This class is used to store information about a UI event public class UiEventParam { // The type of the UI event public UiEvent Type { get; } // Constructor that sets the type of the UI event protected UiEventParam(UiEvent type) => Type = type; // Get the data associated with the UI event public T Get<T>() => ((UiEventParam<T>) this).Data; } // This class is used to store data associated with a UI event public class UiEventParam<T> : UiEventParam { // The data associated with the UI event public readonly T Data; // Constructor that sets the type and data of the UI event public UiEventParam(UiEvent type, T data) : base(type) => Data = data; } public static class UiEventExtensions { // Declare a static readonly ReactiveCommand of type UiEventParam private static readonly ReactiveCommand<UiEventParam> _uiEventParam = new ReactiveCommand<UiEventParam>(); // Create a method to raise an event with a generic type public static void Raise<T>(this UiEvent evnt, T data) // Execute the ReactiveCommand with a new UiEventParam object containing the event and data => _uiEventParam.Execute(new UiEventParam<T>(evnt, data)); // Create a method to subscribe to an event with a generic type public static IDisposable Subscribe<T>(this UiEvent evntType, Action<T> action) { // Return a subscription to the ReactiveCommand, filtering for the specified event type return _uiEventParam .Where(evnt => evnt.Type == evntType) .Subscribe(evnt => // Execute the action with the data from the UiEventParam object action(evnt.Get<T>())); } public static IObservable<T> Where<T>(this UiEvent evntType, Func<T, bool> predicate) { // Create an observable sequence from the UiEventParam var observable = _uiEventParam.Where(evnt => // Check if the event type matches the given event type // and the predicate returns true evnt.Type == evntType && predicate(evnt.Get<T>())); return observable.Select(evnt => evnt.Get<T>()); } } In the example above, we created the class for generalized access to the field of the class, and we also added the method that allows filtering events based on the passed data. With these changes, we can further expand the possibilities of using the event bus: UiEventParam Data UiEventParam<T> Where void ExampleOfRaising() { // Raise the SetSoundVolume event with a value **UiEvent.SetSoundVolume.Raise(0.5f);** } void SubscribeOnEvents() { _disposable = new CompositeDisposable { // Subscribe to the SetSoundVolume event with any value **UiEvent.SetSoundVolume.Subscribe<float>(OnSetVolume),** // Subscribe to the SetSoundVolume event // and call the OnMute method when it is raised with a value of 0f **UiEvent.SetSoundVolume.Where<float>(x => x == 0f).Subscribe(OnMute),** }; } Encapsulating network events in an enum-based bus Similarly, we can wrap any network interactions in an enum-extension, whether that’s message exchange with the backend, or anything else. Let's consider the example of the network library . We'll write a C# extension that wraps the receiving and sending of events in an enum called : Photon Unity Networking 2 NetEvent public static class NetEventExtensions { // Create a new ReactiveCommand of type NetEventParam private static readonly ReactiveCommand<NetEventParam> _netEventParam = new ReactiveCommand<NetEventParam>(); // Extension method to raise a NetEvent with data public static void Raise<T>(this NetEvent evnt, T data) { // Raise the event with the data, options, and reliable send PhotonNetwork.RaiseEvent((byte) evnt, data, GetOptions(evnt), SendOptions.SendReliable); } // Extension method to subscribe to a NetEvent public static IDisposable Subscribe<T>(this NetEvent evntType, Action<T> action) { // Return a subscription to the ReactiveCommand, // filtered by the event type, // and call the action with the data return _netEventParam.Where(evnt => evnt.Type == evntType) .Subscribe(evnt => action(evnt.Get<T>())); } // Method to be called when a PhotonEvent is received public static void OnPhotonEventReceived(EventData data) { // Cast the data code to a NetEvent var evnt = (NetEvent) data.Code; // Execute the ReactiveCommand with a new NetEventParam containing the event and custom data _netEventParam.Execute(new NetEventParam<object>(evnt, data.CustomData)); } } The method should be called in your class implementing the . We can now work with network events, just like we did before, with UI events: OnPhotonEventReceived Photon interface IOnEventCallback.OnEvent // Enum for network events public enum NetEvent { // Event for player state PlayerReady, // Event for client ping ClientPing, // Event for battle finish BattleFinish, // ... } // Method to raise the PlayerReady event public void OnGameLoaded() { // Raise the PlayerReady event with the player's ID **NetEvent.PlayerReady.Raise(_playerId);** } // Method to subscribe to the PlayerReady event public void SomeGameManagerOnAnotherDevice() { _disposable = new CompositeDisposable { // Subscribe to the PlayerReady event **NetEvent.PlayerReady.Subscribe<int>(OnPlayerReady)** }; } Other types of buses A bus can be characterized not only by the method of delivery, but also by the environment for message propagation. The above examples show buses that deliver events at runtime, but we can also identify a separate bus for editor events (in Edit and Play-mode): [InitializeOnLoad] public static class EditorBridge { // Static constructor that is called when the class is loaded static EditorBridge() { var disposable = new CompositeDisposable { // Subscribe to the event and log a message when it is raised **EditorEvent.SomeEventName.Subscribe**(() => Debug.Log("Editor event")) }; // Add a handler to the EditorApplication.quitting event // that disposes of the CompositeDisposable EditorApplication.quitting += () => disposable.Dispose(); } // Create a menu item in the Tools menu [MenuItem("Tools/Raise Test Event")] public static void RaiseTestEvent() { **EditorEvent.SomeEventName.Raise();** } } In this case, the implementation of the and methods will not differ from . In Edit-Mode, the event bus can be used to exchange messages between different custom editors, allowing for interface updates and synchronizing data between them. Subscribe Raise UiEventExtensions Another useful type of bus is a . In the previous examples, the sent event is executed synchronously in the same frame. However, there are tasks that require time for processing but do not demand immediate execution. For such events, a separate manager is required which will check whether there is enough time in the current frame to execute the handlers, or whether to wait for the next frame: deferred execution bus public class DelayedEventsProcessor { // This method is called when the game starts [RuntimeInitializeOnLoadMethod] public static async void Init() { // Create a cancellation token source var source = new CancellationTokenSource(); // Store the current delta time var time = Time.deltaTime; // While the cancellation is not requested, // the queue is not empty // and the execution limit is not exceeded while (!source.IsCancellationRequested && !DelayedEventsQueue.IsEmpty() && (Time.deltaTime - time) > ExecutionLimit) { // Process the events in the queue DelayedEventsQueue.ProcessEvents(); // Waiting for next frame await Task.Yield(); // Update the delta time time = Time.deltaTime; } // When the application is quitting, cancel the token Application.quitting += () => source.Cancel(); } } One can separately highlight the bus, and its events will only be handled in development builds and will be turned off in release configurations. DebugEvent Pros and cons of event buses Using the event bus pattern improves Unity game architecture, simplifies development and maintenance, and enhances flexibility and performance. Let’s look in more detail: : The Event Bus pattern allows for the separation of game logic into components, which simplifies its understanding and maintenance. Each component can process only the events it needs, reducing coupling and increasing system flexibility. Separation of logic : Using an event bus makes it easy to add and remove components, simplifying game expansion and functionality changes. Flexibility : Events processed through the bus can be called asynchronously, improving game performance and reducing the likelihood of delays in operation. Asynchronicity : The event bus pattern can be used to create plugins and modules that can be easily integrated into the game. Extensibility However, it should be remembered that the Event Bus can also lead to some negative effects: : events that are processed through the bus can be triggered asynchronously, which makes it difficult to debug the application. Worsening of debugging : if event processing is not organized correctly, unpredictable interaction between components may occur, leading to errors and failures. Loss of flow control : code that uses the event bus may be harder to understand and maintain, as different components may be subscribed to the same event. Increased code complexity : transmitting messages through the bus can increase the delay in application operation, as events must be processed at the other end of the bus. Increased message transmission delay Conclusion We’ve learned how to use enums to mark data buses and message types, and have explored how to wrap event buses transmitted in different ways and in different environments with enums. Putting it all together, we can use this approach to uniformly handle any type of event, thereby improving code readability and comprehension. public void Initialize() { _disposable = new CompositeDisposable { // Uniform subscription to events // incoming from different buses **UiEvent.ExitBattleClick**.Subscribe(OnExitBattleClick), **BackendEvent.BattleReset**.Subscribe(OnBattleReset), **DelayedEvent.ChangeWindDirection**.Subscribe<Direction>(OnChangeWindDirection), **NetEvent.PlayerReady**.Subscribe<PlayerReadyData>(OnPlayerReady), **DebugEvent.KillOpponentUnits**.Subscribe(OnKillOpponentUnits), **EditorEvent.SelectScene**.Subscribe<string>(OnEditorSelectScene), }; } To recap once more: the event bus pattern has several advantages, such as reducing the coupling between components, more flexible and extensible code, and the ability to handle messages asynchronously. However, its use can lead to code complexity and increase the difficulty of debugging the application. Therefore, before using the pattern, it is necessary to carefully evaluate its necessity and advantages in a specific project. . Source code is available on GitHub