Agrégateur d'événements pour Event Unity3d

L'idée d'écrire votre propre agrégateur d'événements étendu pour Unity3d est attendue depuis longtemps. Après avoir lu plusieurs articles sur ce sujet, je me suis rendu compte qu'il n'y a pas assez de «correct» (dans Unity3d) et de l'agrégateur qui me sont nécessaires, toutes les solutions sont supprimées et n'ont pas les fonctionnalités nécessaires.

Fonctionnalité requise:


  1. Toute classe peut s'abonner à n'importe quel événement (souvent les agrégateurs d'une unité font des abonnés Gameobject spécifiques)
  2. La possibilité de double abonnement d'une instance spécifique à un événement spécifique doit être exclue (dans les outils standard, vous devez suivre cela vous-même)
  3. Il devrait y avoir une fonctionnalité de désabonnement manuel et automatique, dans le cas de la suppression d'une instance / de la désactivation de monobekh (je souhaite m'abonner et ne pas prendre un bain de vapeur que l'abonné rejette soudainement le sabot)
  4. les événements doivent pouvoir transférer des données / liens de toute complexité (je souhaite m'abonner sur une seule ligne et obtenir l'ensemble des données sans problème)

Où l'appliquer


  1. Ceci est idéal pour l'interface utilisateur, lorsqu'il est nécessaire de transférer des données depuis n'importe quel objet sans aucune connexion.
  2. Messages sur les changements de données, certains analogues du code réactif.
  3. Pour l'injection de dépendance
  4. Rappels globaux

Points faibles


  1. En raison des contrôles sur les abonnés morts et prend (je vais révéler plus tard) le code est plus lent que des solutions similaires
  2. La classe / struct est utilisée comme noyau de l'événement, afin de ne pas allouer de mémoire + le problème principal, il n'est pas recommandé de spammer les événements dans la mise à jour)

Idéologie générale


L'idéologie générale est que pour nous, un événement est un ensemble de données spécifiques et pertinentes. Disons que nous avons appuyé sur un bouton d'une interface / joystick. Et nous voulons envoyer un événement avec des signes d'appuyer sur un bouton spécifique pour un traitement ultérieur. Le résultat du traitement des clics est des modifications visuelles de l'interface et une sorte d'action dans la logique. En conséquence, il peut y avoir un traitement / abonnement à deux endroits différents.

À quoi ressemble le corps de l'événement / le paquet de données dans mon cas:

Exemple de corps d'événement
public struct ClickOnButtonEvent   {     public int ButtonID; //     enum    } 


À quoi ressemble l'abonnement à l'événement:

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

Pour vous abonner, nous devons spécifier:
Un objet qui est un abonné (généralement c'est la classe elle-même dans laquelle l'abonnement, mais ce n'est pas nécessaire, l'abonné peut spécifier l'une des instances de classe à partir des champs de classe.
Type / événement auquel nous sommes abonnés. C'est l'essence même de cet agrégateur, pour nous un certain type de classe est un événement que nous écoutons et traitons.
S'abonner au mieux dans Awake et OnEnable;

Exemple

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

Pour clarifier ce qu'est la puce, considérons un cas plus complexe


Nous avons des icônes de caractères qui:
  1. Sachez à quel personnage ils sont attachés.
  2. Refléter la quantité de mana, hp, exp et statuts (étourdissement, cécité, peur, folie)

Et ici, vous pouvez faire plusieurs événements

Pour changer les indicateurs:

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

Pour modifier des statuts négatifs:

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

Pourquoi dans les deux cas passons-nous la classe de personnage? Voici l'abonné à l'événement et son gestionnaire:

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

C'est le marqueur par lequel nous traitons l'événement et comprenons exactement ce dont nous avons besoin.
Pourquoi ne pas supposer que vous vous abonnez directement à la classe Personnage? Et les spammer?
Il sera difficile de débuter, il est préférable pour un groupe de classes / événements de créer leur propre événement séparé.

Pourquoi, encore une fois, à l'intérieur de l'événement, ne mettez pas de personnage et n'en prenez pas tout?
Donc au fait c'est possible, mais souvent dans les classes il y a des restrictions de visibilité, et les données nécessaires pour l'événement peuvent ne pas être visibles de l'extérieur.

si la classe est trop lourde pour être utilisée comme marqueur?
En fait, dans la plupart des cas, un marqueur n'est pas nécessaire; un groupe de classes mises à jour est plutôt rare. Habituellement, une entité spécifique a besoin d'un événement - un modèle de contrôleur / vue, qui affiche généralement l'état du 1er caractère. Et donc il y a toujours une solution banale - des identifiants de différents types (de l'inam, au hachage complexe, etc.).

Qu'y a-t-il sous le capot et comment ça marche?


Code directement agrégateur
 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(); } } 


Commençons par le principal

Il s'agit d'un dictionnaire dans lequel la clé est de type et la valeur est conteneur

 public class EventContainer<T> : IDebugable 

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

Pourquoi stockons-nous le conteneur en tant qu'objet? Le dictionnaire ne sait pas comment stocker les génériques. Mais grâce à la clé, nous sommes en mesure d'amener rapidement l'objet au type dont nous avons besoin.

Que contient le conteneur?

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

Il contient un multidélégué générique et une collection où la clé est l'objet qui est l'abonné et la valeur est la même méthode de gestionnaire. En fait, ce dictionnaire contient tous les objets et méthodes qui appartiennent à ce type. En conséquence, nous appelons un multidélégué, et il appelle tous les abonnés, il s'agit d'un système d'événement «honnête» dans lequel il n'y a pas de restrictions sur l'abonné, mais dans la plupart des autres agrégateurs, sous le capot, une collection de classes qui sont généralisées soit par une interface spéciale, soit héritées d'une classe qui implémente le système est itérée des messages.

Lorsqu'un multidélégataire est appelé, une vérification est effectuée pour voir s'il y a des clés mortes, la collecte est nettoyée des cadavres, puis un multidélégué avec les abonnés concernés est pris en charge. Cela prend du temps, mais encore une fois, en fait, si la fonctionnalité des événements est séparée, alors un événement aura 3 à 5 abonnés, donc le chèque n'est pas si effrayant, l'avantage du confort est plus évident. Pour les histoires de réseau où il peut y avoir mille abonnés ou plus, cet agrégateur est préférable de ne pas utiliser. Bien que la question reste ouverte - si vous supprimez la vérification des cadavres, ce qui est plus rapide - itérer sur un tableau d'abonnés à partir de 1k ou appeler un multi-délégué à partir de 1k abonnés.

Caractéristiques d'utilisation


Un abonnement est mieux poussé dans Awake.

Si un objet s'active / se désactive activement, il est préférable de s'abonner à Awake et OnEnable, il ne signera pas deux fois, mais la possibilité qu'un GameObject inactif soit pris pour mort sera exclue.

La facturation des événements est préférable pas avant le début, lorsque tous les abonnés seront créés et enregistrés.

L'agrégateur nettoie la liste au déchargement de la scène. Dans certains agrégateurs, il est suggéré de nettoyer le chargement de la scène - il s'agit d'un fichier, l'événement de chargement de la scène vient après Awake / OnEnable, les abonnés ajoutés seront supprimés.

L'agrégateur a - une chaîne statique publique DebugInfo (), vous pouvez voir quelles classes sont abonnées à quels événements.

Dépôt GitHub

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


All Articles