3 min read

[Tutorial] Communication with Events

[Tutorial] Communication with Events

Events occur in games all the time: the player's life changes, another player joins the game, a building is completed or the energy is recharged.

In order to let the different components of the game communicate with each other, an event system can be used. This makes it possible to listen for certain events without the listener and trigger having to know each other. It also makes it easier to test features. For example, we can test that a player's life bar lights up when he takes damage by manually triggering the appropriate event.

Interface

Our EventService should provide the following 3 functionalities:

  • Register a callback for a specific event type
  • Removing a registered callback
  • Triggering an event
using System;

namespace ProductionReady
{
    public interface IEventService
    {
        /// <summary>
        /// Adds the handler to the event dispatcher. The handler will be called when an event of the type is triggered.
        /// </summary>
        /// <param name="handler">The function that is called when the event is triggered</param>
        /// <typeparam name="TEvent">The event type the handler is used for</typeparam>
        void Subscribe<TEvent>(Action<TEvent> handler);
        
        /// <summary>
        /// Removes the handler from the event dispatcher.
        /// </summary>
        /// <param name="handler">The function that should not be called anymore.</param>
        /// <typeparam name="TEvent">The event type the handler is used for</typeparam>
        void Unsubscribe<TEvent>(Action<TEvent> handler);
        
        /// <summary>
        /// Forwards the passed event to all subscribed handlers.
        /// </summary>
        /// <param name="event">The event instance that is passed to the handlers</param>
        void Trigger<TEvent>(TEvent @event);
    }
}

Implementation

using System;

namespace ProductionReady
{
    public class EventService : IEventService
    {
        private static class EventDispatcher<T>
        {
            public static Action<T> EventHandler;
        }
        
        public void Subscribe<T>(Action<T> handler)
        {
            EventDispatcher<T>.EventHandler += handler;
        }

        public void Unsubscribe<T>(Action<T> handler)
        {
            EventDispatcher<T>.EventHandler -= handler;
        }

        public void Trigger<T>(T @event)
        {
            EventDispatcher<T>.EventHandler?.Invoke(@event);
        }
    }
}

We use an internal static class for our implementation. But why?

Besides the manageable amount of code, there is one big advantage:

For each event type we use, a version of the methods and static class is compiled. This means that neither casting nor boxing is necessary for our event instances. Thus, only the creation of an event instance, if it is a class, generates garbage. Struct events, on the other hand, do not generate any. Since several events may be triggered in each frame, this is important.

Usage

We can now declare any event types we want:

public struct MessageEvent
{
    public string Message;
}

And then use them as follows:

using UnityEngine;

namespace ProductionReady
{
    public class EventUser : MonoBehaviour
    {
        [SerializeField] private string _eventMessage = "Production Ready!";
        
        private void Start()
        {
            IEventService eventService = new EventService();
            
            // Subscribe
            eventService.Subscribe<MessageEvent>(OnMessageEventOccured);
            
            // Trigger
            MessageEvent messageEvent = new MessageEvent() { Message = _eventMessage };
            eventService.Trigger(messageEvent);
            
            // Unsubscribe
            eventService.Unsubscribe<MessageEvent>(OnMessageEventOccured);
        }

        private void OnMessageEventOccured(MessageEvent @event)
        {
            Debug.Log($"Message received: {@event.Message}");
        }
    }
}

Summary

With the help of the EventService, components can now communicate with each other purely via the event type without having a direct dependency on each other!

[Unity][C#] Event Service
[Unity][C#] Event Service. GitHub Gist: instantly share code, notes, and snippets.

Potential Improvements

To better track and debug the flow of the various events and their data, the service for the editor can be extended via a decorator service. The EditorEventService is then used in the editor instead of the standard EventService. A tool could then visualize the data, for example.

using System;
using UnityEngine;

namespace ProductionReady
{
    public class EditorEventService : IEventService
    {
        private readonly IEventService _baseEventService;

        public EditorEventService(IEventService baseEventService)
        {
            _baseEventService = baseEventService;
        }

        public void Subscribe<T>(Action<T> handler)
        {
            Debug.Log($"Handler {handler.Method.Name} subscribed to event {typeof(T).Name}");
            _baseEventService.Subscribe(handler);
        }

        public void Unsubscribe<T>(Action<T> handler)
        {
            // Notify event editor
            Debug.Log($"Handler {handler.Method.Name} unsubscribed from event {typeof(T).Name}");
            _baseEventService.Unsubscribe(handler);
        }

        public void Trigger<T>(T @event)
        {
            // Notify event editor
            Debug.Log($"Event '{typeof(T).Name}' triggered: {@event}");
            _baseEventService.Trigger(@event);
        }
    }
}