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:
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; } } }
Och nu har jag en klass som behöver lyssna på dessa event, detta görs genom att implementera IShowErrorMessageGameEvent.
Kod:
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); } } }
Kod:
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; } } }
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.
SDL2 Isometric Game Tutorial Part 6