Ereignisaggregator für Ereignis Unity3d

Die Idee, einen eigenen erweiterten Ereignisaggregator für Unity3d zu schreiben, ist längst überfällig. Nachdem ich mehrere Artikel zu diesem Thema gelesen hatte, stellte ich fest, dass es nicht genug „Richtiges“ (in Unity3d) und den für mich erforderlichen Aggregator gibt. Alle Lösungen sind reduziert und verfügen nicht über die erforderliche Funktionalität.

Erforderliche Funktionalität:


  1. Jede Klasse kann jedes Ereignis abonnieren (häufig bilden Aggregatoren in einer Einheit bestimmte Gameobject-Abonnenten).
  2. Die Möglichkeit des doppelten Abonnierens einer bestimmten Instanz für ein bestimmtes Ereignis sollte ausgeschlossen werden (in Standardtools müssen Sie dies selbst befolgen).
  3. Es sollte sowohl manuelles als auch automatisches Abbestellen von Funktionen geben, wenn eine Instanz gelöscht oder Monobekh deaktiviert wird (ich möchte abonnieren und kein Dampfbad nehmen, das der Abonnent plötzlich den Huf zurückwirft).
  4. Ereignisse sollten in der Lage sein, Daten / Links beliebiger Komplexität zu übertragen (ich möchte in einer Zeile abonnieren und den gesamten Datensatz ohne Probleme abrufen).

Wo kann man es anwenden?


  1. Dies ist ideal für die Benutzeroberfläche, wenn Daten von einem Objekt ohne Verbindung weitergeleitet werden müssen.
  2. Meldungen über Datenänderungen, einige Analoga des reaktiven Codes.
  3. Zur Abhängigkeitsinjektion
  4. Globale Rückrufe

Schwachstellen


  1. Aufgrund von Überprüfungen toter Abonnenten und Takes (ich werde später offenbaren) ist der Code langsamer als ähnliche Lösungen
  2. Klasse / Struktur wird als Kern des Ereignisses verwendet. Um keinen Speicher + das Hauptproblem zuzuweisen, wird nicht empfohlen, Ereignisse im Update als Spam zu versenden.

Allgemeine Ideologie


Die allgemeine Ideologie ist, dass eine Veranstaltung für uns ein spezifisches und relevantes Datenpaket ist. Angenommen, wir haben eine Taste auf einer Schnittstelle / einem Joystick gedrückt. Und wir möchten ein Ereignis mit Anzeichen für das Drücken einer bestimmten Taste zur weiteren Verarbeitung senden. Das Ergebnis der Verarbeitung von Klicks sind visuelle Änderungen an der Benutzeroberfläche und eine Aktion in der Logik. Dementsprechend kann die Verarbeitung / das Abonnement an zwei verschiedenen Orten erfolgen.

Wie der Ereigniskörper / das Datenpaket in meinem Fall aussieht:

Beispiel für einen Ereigniskörper
public struct ClickOnButtonEvent   {     public int ButtonID; //     enum    } 


Wie das Veranstaltungsabonnement aussieht:

 public static void AddListener<T>(object listener, Action<T> action) 

Um sich anzumelden, müssen wir Folgendes angeben:
Ein Objekt, das ein Abonnent ist (normalerweise ist es die Klasse selbst, in der sich das Abonnement befindet, dies ist jedoch nicht erforderlich. Der Abonnent kann eine der Klasseninstanzen aus den Klassenfeldern angeben.
Typ / Ereignis, das wir abonnieren. Dies ist die Schlüsselessenz dieses Aggregators. Für uns ist eine bestimmte Art von Klasse ein Ereignis, das wir abhören und verarbeiten.
Am besten in Awake und OnEnable abonnieren;

Beispiel

 public class Example : MonoBehaviour { private void Awake() { EventAggregator.AddListener<ClickOnButtonEvent>(this, ClickButtonListener); } private void ClickButtonListener(ClickOnButtonEvent obj) { Debug.Log("  " + obj.ButtonID); } } 

Betrachten Sie einen komplexeren Fall, um zu verdeutlichen, was der Chip ist


Wir haben Charaktersymbole, die:
  1. Wissen Sie, an welchen Charakter sie gebunden sind.
  2. Reflektieren Sie die Menge an Mana, HP, Exp und Status (Betäubung, Blindheit, Angst, Wahnsinn).

Und hier können Sie mehrere Veranstaltungen durchführen

So ändern Sie die Indikatoren:

 public struct CharacterStateChanges { public Character Character; public float Hp; public float Mp; public float Xp; } 

So ändern Sie den negativen Status:

 public struct CharacterNegativeStatusEvent { public Character Character; public Statuses Statuses; //enum  } 

Warum bestehen wir in beiden Fällen die Zeichenklasse? Hier ist der Teilnehmer und sein Handler:

 private void Awake() { EventAggregator.AddListener<CharacterNegativeStatusEvent> (this, CharacterNegativeStatusListener); } private void CharacterNegativeStatusListener(CharacterNegativeStatusEvent obj) { if (obj.Character != _character) return; _currentStatus = obj.Statuses; } 

Dies ist der Marker, anhand dessen wir das Ereignis verarbeiten und verstehen, was genau wir brauchen.
Nehmen Sie an, Sie abonnieren direkt die Zeichenklasse. Und sie spammen?
Es wird schwierig zu debütieren sein, es ist besser für eine Gruppe von Klassen / Ereignissen, ein eigenes separates Ereignis zu erstellen.

Warum sollte man innerhalb des Events einfach nicht den Charakter setzen und alles daraus nehmen?
Übrigens ist es möglich, aber häufig gibt es in Klassen Sichtbarkeitsbeschränkungen, und die für das Ereignis erforderlichen Daten sind möglicherweise von außen nicht sichtbar.

Ist die Klasse zu schwer, um als Marker verwendet zu werden?
Tatsächlich wird in den meisten Fällen kein Marker benötigt, eine Gruppe aktualisierter Klassen ist eher selten. Normalerweise benötigt eine bestimmte Entität ein Ereignis - ein Controller- / Ansichtsmodell, das normalerweise den Status des ersten Zeichens anzeigt. Und so gibt es immer eine banale Lösung - IDs verschiedener Typen (von Inam bis zu komplexem Hash usw.).

Was ist unter der Haube und wie funktioniert es?


Direkt Aggregatorcode
 namespace GlobalEventAggregator public delegate void EventHandler<T>(T e); { public class EventContainer<T> : IDebugable { private event EventHandler<T> _eventKeeper; private readonly Dictionary<WeakReference, EventHandler<T>> _activeListenersOfThisType = new Dictionary<WeakReference, EventHandler<T>>(); private const string Error = "null"; public bool HasDuplicates(object listener) { return _activeListenersOfThisType.Keys.Any(k => k.Target == listener); } public void AddToEvent(object listener, EventHandler<T> action) { var newAction = new WeakReference(listener); _activeListenersOfThisType.Add(newAction, action); _eventKeeper += _activeListenersOfThisType[newAction]; } public void RemoveFromEvent(object listener) { var currentEvent = _activeListenersOfThisType.Keys.FirstOrDefault(k => k.Target == listener); if (currentEvent != null) { _eventKeeper -= _activeListenersOfThisType[currentEvent]; _activeListenersOfThisType.Remove(currentEvent); } } public EventContainer(object listener, EventHandler<T> action) { _eventKeeper += action; _activeListenersOfThisType.Add(new WeakReference(listener), action); } public void Invoke(T t) { if (_activeListenersOfThisType.Keys.Any(k => k.Target.ToString() == Error)) { var failObjList = _activeListenersOfThisType.Keys.Where(k => k.Target.ToString() == Error).ToList(); foreach (var fail in failObjList) { _eventKeeper -= _activeListenersOfThisType[fail]; _activeListenersOfThisType.Remove(fail); } } if (_eventKeeper != null) _eventKeeper(t); return; } public string DebugInfo() { string info = string.Empty; foreach (var c in _activeListenersOfThisType.Keys) { info += c.Target.ToString() + "\n"; } return info; } } public static class EventAggregator { private static Dictionary<Type, object> GlobalListeners = new Dictionary<Type, object>(); static EventAggregator() { SceneManager.sceneUnloaded += ClearGlobalListeners; } private static void ClearGlobalListeners(Scene scene) { GlobalListeners.Clear(); } public static void AddListener<T>(object listener, Action<T> action) { var key = typeof(T); EventHandler<T> handler = new EventHandler<T>(action); if (GlobalListeners.ContainsKey(key)) { var lr = (EventContainer<T>)GlobalListeners[key]; if (lr.HasDuplicates(listener)) return; lr.AddToEvent(listener, handler); return; } GlobalListeners.Add(key, new EventContainer<T>(listener, handler)); } public static void Invoke<T>(T data) { var key = typeof(T); if (!GlobalListeners.ContainsKey(key)) return; var eventContainer = (EventContainer<T>)GlobalListeners[key]; eventContainer.Invoke(data); } public static void RemoveListener<T>(object listener) { var key = typeof(T); if (GlobalListeners.ContainsKey(key)) { var eventContainer = (EventContainer<T>)GlobalListeners[key]; eventContainer.RemoveFromEvent(listener); } } public static string DebugInfo() { string info = string.Empty; foreach (var listener in GlobalListeners) { info += "     " + listener.Key.ToString() + "\n"; var t = (IDebugable)listener.Value; info += t.DebugInfo() + "\n"; } return info; } } public interface IDebugable { string DebugInfo(); } } 


Beginnen wir mit dem Haupt

Dies ist ein Wörterbuch, in dem der Schlüssel Typ und der Wert Container ist

 public class EventContainer<T> : IDebugable 

 private static Dictionary<Type, object> GlobalListeners = new Dictionary<Type, object>(); 

Warum speichern wir den Container als Objekt? Das Wörterbuch kann keine Generika speichern. Dank des Schlüssels können wir das Objekt jedoch schnell auf den gewünschten Typ bringen.

Was enthält der Container?

 private event EventHandler<T> _eventKeeper; private readonly Dictionary<WeakReference, EventHandler<T>> _activeListenersOfThisType = new Dictionary<WeakReference, EventHandler<T>>(); 

Es enthält eine generische Multidelegate und eine Sammlung, in der der Schlüssel das Objekt ist, das der Abonnent ist, und der Wert dieselbe Handlermethode ist. Tatsächlich enthält dieses Wörterbuch alle Objekte und Methoden, die zu diesem Typ gehören. Infolgedessen rufen wir ein Multidelegate auf, und er ruft alle Abonnenten an. Dies ist ein „ehrliches“ Ereignissystem, bei dem es keine Einschränkungen für den Abonnenten gibt. In den meisten anderen Aggregatoren wird jedoch eine Sammlung von Klassen, die entweder von einer speziellen Schnittstelle verallgemeinert oder von einer Klasse geerbt werden, die das System implementiert, unter der Haube iteriert Nachrichten.

Wenn ein Multidelegate aufgerufen wird, wird geprüft, ob tote Schlüssel vorhanden sind, die Sammlung wird von Leichen gereinigt, und anschließend wird ein Multidelegate mit relevanten Abonnenten aufgenommen. Dies braucht Zeit, aber wenn die Funktionalität der Ereignisse getrennt ist, hat ein Ereignis 3-5 Abonnenten, sodass die Überprüfung nicht so beängstigend ist und der Vorteil des Komforts offensichtlicher ist. Für Netzwerk-Storys mit tausend oder mehr Abonnenten ist es besser, diesen Aggregator nicht zu verwenden. Obwohl die Frage offen bleibt - wenn Sie die Prüfung auf Leichen entfernen, was schneller ist -, iterieren Sie über ein Array von Abonnenten von 1k oder rufen Sie einen Multi-Delegaten von 1k-Abonnenten an.

Merkmale der Verwendung


Ein Abonnement wird am besten in Awake gepusht.

Wenn ein Objekt aktiv ein- und ausgeschaltet wird, ist es besser, Awake und OnEnable zu abonnieren. Es wird nicht zweimal signiert, aber die Möglichkeit, dass ein inaktives GameObject für tot gehalten wird, wird ausgeschlossen.

Rechnungsereignisse sind besser nicht vor dem Start, wenn alle Abonnenten erstellt und registriert werden.

Der Aggregator bereinigt die Liste beim Entladen der Szene. In einigen Aggregatoren wird empfohlen, das Laden der Szene zu bereinigen. Dies ist eine Datei. Das Ereignis zum Laden der Szene erfolgt nach Awake / OnEnable. Die hinzugefügten Abonnenten werden gelöscht.

Der Aggregator hat - öffentliche statische Zeichenfolge DebugInfo (), Sie können sehen, welche Klassen welche Ereignisse abonniert haben.

GitHub-Repository

Source: https://habr.com/ru/post/de446130/


All Articles