3 min read

[Tutorial] Kommunikation mit Events (Deutsch)

[Tutorial] Kommunikation mit Events (Deutsch)

Events gibt es in Spielen die ganze Zeit: Das Leben des Spielers ändert sich, ein anderer Spieler tritt dem Spiel bei, ein Gebäude wurde fertig gebaut oder die Energie ist wieder aufgeladen.

Um nun die verschiedenen Komponenten des Spiels miteinander kommunizieren zu lassen, bietet sich die Verwendung von Events an. Dies ermöglicht das Hören auf bestimmte Ereignisse ohne das sich Zuhörer und Auslöser kennen müssen. Außerdem vereinfacht es auch das Testen von Funktionen. Wir können beispielsweise testen, dass die Lebensleiste eines Spielers aufleuchtet, wenn er Schaden nimmt indem wir das passende Event manuell auslösen.

Interface

Unser EventService soll die folgenden 3 Funktionalitäten bereitstellen:

  • Registrieren eines Callbacks für einen bestimmten Event Typ
  • Entfernen eines registrierten Callbacks
  • Das Auslösen eines Events
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);
    }
}

Implementierung

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);
        }
    }
}

Wir verwenden eine interne statische Klasse für unsere Implementierung. Aber warum?

Neben der übersichtlichen Menge an Code gibt es einen großen Vorteil:

Für jeden von uns verwendeten Event-Typ wird eine Version der Methoden und der statischen Klasse kompiliert. Dadurch ist für unsere Event-Instanzen weder Casting noch Boxing nötig. Somit erzeugt lediglich das Erstellen einer Event-Instanz, sofern es eine Klasse ist, Garbage. Struct-Events erzeugen hingegen gar keinen. Da gegebenenfalls jedes Frame mehrere Events geworfen werden, ist das wichtig.

Verwendung

Wir können nun beliebige Event-Typen deklarieren:

public struct MessageEvent
{
    public string Message;
}

Und diese dann folgendermaßen verwenden:

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}");
        }
    }
}

Zusammenfassung

Mit Hilfe des EventServices können Komponenten rein über den Event-Typ miteinander kommunizieren ohne eine direkte Abhängigkeit aufeinander zu haben!

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

Mögliche Verbesserungen

Um den Fluss der verschiedenen Events und ihrer Daten besser nachvollziehen und debuggen zu können, kann der Service für den Editor via Decorator-Pattern erweitert werden. Im Editor wird dann der EditorEventService anstelle des Standard-EventServices verwendet. Ein Editor-Tool könnte die Daten dann beispielsweise visualisieren.

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);
        }
    }
}