• C# Game Event System

    För några dagar sedan skriv jag lite kod för mitt game event system, jag blev såpass nöjd att jag kände mig tvungen att dela. Detta exempel är i C# men bör kunna appliceras på andra typer av språk.

    Tidigare game event systems jag skrivit har varit en liten nagg i ögat då jag i stort sett har haft ett interface som eventsystemet, så fort ett event skickades ut så loopades alla som implementerar detta interface igenom och OnEvent kallas. Problemet var att i OnEvent fick jag nästan alltid event som jag egentligen inte var intresserad av.

    Denna gång så bestämde jag mig för fixa detta en gång för alla och klura ut ett smidigt sett för en klass att bara registera sig på event som dom är intresserade av. Det viktigaste är att allting skulle gå på så lite kod som möjligt.

    Så jag började med att skapa ett interface för varje typ av event, detta exempel visar en klass som är intresserade av ett ShowErrorMessage game event. Målet är att varje gång ett sådant event skickas ut att visa felmeddelande i ett litet debugfönster.
    Kod: [Visa]
    using System.Collections;
    using System.Collections.Generic;
    
    namespace AOEngine
    {
        public interface IShowErrorMessageGameEventListener {
            void OnEvent(ShowErrorMessageGameEvent gameEvent);
        }
    
        public class ShowErrorMessageGameEvent : GameEvent
        {
            public string ErrorMessage { get; set; }
            public float CloseAfter { get; set; }
        }
    }
    Detta skapar både själva eventet ShowErrorMessageGameEvent och ett interface IShowErrorMessageGameEvent för att lyssna på dessa event.

    Och nu har jag en klass som behöver lyssna på dessa event, detta görs genom att implementera IShowErrorMessageGameEvent.


    Kod: [Visa]
    using System.Collections;
    using System.Collections.Generic;
    
    namespace AOEngine
    {
        public class ErrorMessageController : 
        Controller,
        IShowErrorMessageGameEventListener
        {
            public static string errorMessageUiUid = "ErrorMessageUiDataUid";
    
            public override void OnCreate(IModel model, IView view) {
                base.OnCreate(model, view);
    
                GameEventSystem.RegisterListener(this);
            }
    
            public override void OnDestroy() {
            }
    
            public override void OnUpdate() {
            }
    
            public void OnEvent(ShowErrorMessageGameEvent gameEvent) {
                UnityEngine.Debug.Log("Somehow render the error message " + gameEvent.ErrorMessage);
            }
        }
    }
    Som ni ser så implementerar vi OnEvent(ShowErrorMessageGameEvent gameEvent) här, vilket är det viktiga, i OnCreate så ser vi även ett anrop till vår game event system: GameEventSystem.RegisterListener(this); men denna finns ju inte ännu, så det är dags att skapa.

    Kod: [Visa]
    using System;
    using System.Linq;
    using System.Reflection;
    using System.Collections;
    using System.Collections.Generic;
    
    namespace AOEngine
    {
    
        public static class GameEventSystem
        {
            public static Dictionary<Type, List<object>> listeners = new Dictionary<Type, List<object>>();
    
            public static void RegisterListener(object listener) {
                foreach(Type parameterType in GetAllOnEventParameterTypes(listener)) {
                    if(!listeners.ContainsKey(parameterType))
                        listeners[parameterType] = new List<object>();
                    listeners[parameterType].Add(listener);
                }
            }
    
            public static void UnregisterListener(object listener) {
                foreach(Type parameterType in GetAllOnEventParameterTypes(listener)) {
                    listeners[parameterType].Remove(listener);
                }
            }
    
            public static void FireEvent(GameEvent gameEvent) {
                if(!listeners.ContainsKey(gameEvent.GetType()))
                    return;
    
                List<object> gameEventListeners = listeners[gameEvent.GetType()];
    
                foreach(object listener in gameEventListeners) {
                    foreach(Type implementedInterface in listener.GetType().GetInterfaces()) {
                        MethodInfo method = implementedInterface.GetMethod(
                            "OnEvent",
                            new Type[] { gameEvent.GetType() });
    
                        if(method == null)
                            continue;
    
                        method.Invoke(listener, new object[] { gameEvent });
                    }
                }
            }
    
            private static List<Type> GetAllOnEventParameterTypes(object listener) {
                List<Type> onEventParameterTypes = new List<Type>();
                Type[] implementedInterfaces = listener.GetType().GetInterfaces();
                foreach(Type implementedInterface in implementedInterfaces) {
                    MethodInfo method = implementedInterface.GetMethod("OnEvent");               
                    if(method == null)
                        continue;
                    Type parameterType = method.GetParameters().First().ParameterType;
                    onEventParameterTypes.Add(parameterType);
                }
                return onEventParameterTypes;
            }
        }
    }
    Vårt game event system sparar en dictionary med olika typer av events, varje event har en lista med lyssnare. Här har vi 3 publika funktioner, RegisterListener, Unregister Listener och FireEvent.

    I RegisterListener så använder vi oss av C# reflection-system för att skanna alla interface lyssnaren implementerar som även har en metod som heter "OnEvent", när en sådant metod hittas så lägger vi till lyssnaren i vår dictionary på just det eventet. Genom att kolla (parameter).ParameterType får vi ut ifall parameter i OnEvent-funktion är en "IShowErrorMessageEvent" eller något annat. Detta betyder att ifall en lyssnare som lyssnar på 3 olika events, då kommer dom läggas 3 gånger i vår dictionary, men under olika event. Att lägga till en lyssnare är bara en pekare 4 eller 8 bytes data, så borde inte vara någon större overhead, även när vi har ett stort antal lyssnare och events.

    När vi sedan vill kalla på FireEvent för att skicka ut alla events, så använder vi Type(gameEvent) för att få ut "IShowErrorMessageEvent" eller annat event, detta leder oss direkt till vilken nycklen vår dictionary som har alla lyssnare. Sedan loopar vi igenom alla interface och hämtar ut "OnEvent" funktioner som har en signatur som matchar med typen var vårt spelevent.



    Slutsats, jag är ganska nöjd, det enda jag behöver göra för att snappa upp event är att implementera ett interface för detta spelevent och implementera dess OnEvent(TypeAvSpelEvent spelEvent), samt att registera mig som lyssnare i GameEventSystem. Då vet jag att jag bara får dom meddelande som jag har implementation för att ta hand om.



    Självkritik:

    • Jag borde kanske skapa ett namespace under AOEngine för spelevent, så jag kan skriva using AOEngine.Events och skapa klasserna IShowErrorMessage, istället för att stava ut hela IShowErrorGameEvent.
    • Egentligen så borde alla lyssnare kunna sparas i en enda lista och varje gång en event skickas ut kan man loopera igenom dessa och kolla vilka OnEvent funktioner som matchar signaturen med Type(gameEvent). Detta blir enklare dock lite mer resurskrävande, men detta kanske aldrig blir ett problem och jag har gjort en "premature otpimization".
    • Ifall detta skulle sig visa sig långsamt när flera lyssnare och events skapas, borde man kunna casha metoderna så att man slipper göra GetMethod lookups hela tiden.
    • Ifall min ErrorMessageController skulla ärva av ett annat interface som även den har en OnEvent-method, ja då kommer den läggas till i vår dictionary i GameEventSystem, detta bör dock inte vara något större problem så länge man inte anropar FireEvent med den främmande interfacet's signatur. Detta borde undervikas att även kontrollera att argumenet för OnEvent är har GameEvent som basklass när vi registerar vår lyssnare.