Agregador de eventos para Event Unity3d

La idea de escribir su propio agregador de eventos extendido para Unity3d está muy pendiente. Después de leer varios artículos sobre este tema, me di cuenta de que no hay suficiente "correcto" (dentro de Unity3d) y el agregador que es necesario para mí, todas las soluciones se eliminan y no tienen la funcionalidad necesaria.

Funcionalidad requerida:


  1. Cualquier clase puede suscribirse a cualquier evento (a menudo los agregadores en una unidad hacen suscriptores específicos de Gameobject)
  2. Se debe excluir la posibilidad de doble suscripción de una instancia específica a un evento específico (en las herramientas estándar debe seguir esto usted mismo)
  3. Debe haber funcionalidades tanto para cancelar la suscripción manual como automática, en el caso de eliminar una instancia / deshabilitar monobekh (quiero suscribirme y no tomar un baño de vapor que el suscriptor arroja repentinamente el casco)
  4. los eventos deberían poder transferir datos / enlaces de cualquier complejidad (quiero suscribirme en una línea y obtener todo el conjunto de datos sin problemas)

Donde aplicarlo


  1. Esto es ideal para la IU, cuando existe la necesidad de reenviar datos desde cualquier objeto sin ninguna conexión.
  2. Mensajes sobre cambios de datos, algunos análogos del código reactivo.
  3. Para inyección de dependencia
  4. Callbacks globales

Puntos débiles


  1. Debido a las verificaciones de suscriptores muertos y tomas (revelaré más adelante) el código es más lento que soluciones similares
  2. Class / struct se usa como el núcleo del evento, para no asignar memoria + el problema principal, no se recomienda enviar eventos de spam en la actualización)

Ideologia general


La ideología general es que para nosotros un evento es un paquete de datos específico y relevante. Digamos que presionamos un botón en una interfaz / joystick. Y queremos enviar un evento con signos de presionar un botón específico para su posterior procesamiento. El resultado del procesamiento de clics son cambios visuales en la interfaz y algún tipo de acción en la lógica. En consecuencia, puede haber procesamiento / suscripción en dos lugares diferentes.

Cómo se ve el cuerpo del evento / paquete de datos en mi caso:

Ejemplo de cuerpo de evento
public struct ClickOnButtonEvent   {     public int ButtonID; //     enum    } 


Aspecto de la suscripción al evento:

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

Para suscribirse, necesitamos especificar:
Un objeto que es un suscriptor (generalmente es la clase en sí misma en la que se suscribe, pero esto no es necesario, el suscriptor puede especificar una de las instancias de clase de los campos de clase.
Tipo / evento al que nos estamos suscribiendo. Esta es la esencia clave de este agregador, para nosotros un cierto tipo de clase es un evento que escuchamos y procesamos.
Suscribirse mejor en Awake y OnEnable;

Ejemplo

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

Para dejar en claro cuál es el chip, considere un caso más complejo


Tenemos íconos de personajes que:
  1. Sepa a qué personaje están apegados.
  2. Refleja la cantidad de maná, hp, exp y estados (aturdimiento, ceguera, miedo, locura)

Y aquí puedes hacer varios eventos.

Para cambiar los indicadores:

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

Para cambiar estados negativos:

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

¿Por qué en ambos casos pasamos la clase de personaje? Aquí está el suscriptor del evento y su controlador:

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

Este es el marcador por el cual procesamos el evento y entendemos exactamente qué lo necesitamos.
¿Por qué no supones que te suscribes directamente a la clase Character? ¿Y enviarles spam?
Será difícil debutar, es mejor para un grupo de clases / eventos crear su propio evento por separado.

¿Por qué, de nuevo, simplemente no colocas a Character en el evento y tomas todo de él?
Por cierto, es posible, pero a menudo en las clases hay restricciones de visibilidad, y los datos necesarios para el evento pueden no ser visibles desde el exterior.

si la clase es demasiado pesada para usarla como marcador?
De hecho, en la mayoría de los casos, no se necesita un marcador; un grupo de clases actualizadas es bastante raro. Por lo general, una entidad específica necesita un evento: un modelo de controlador / vista, que generalmente muestra el estado del primer carácter. Por lo tanto, siempre hay una solución banal: identificaciones de diferentes tipos (desde inam hasta hash complejo, etc.).

¿Qué hay debajo del capó y cómo funciona?


Código de agregación directa
 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(); } } 


Comencemos con el principal

Este es un diccionario en el que la clave es tipo y el valor es contenedor

 public class EventContainer<T> : IDebugable 

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

¿Por qué almacenamos el contenedor como un objeto? El diccionario no sabe cómo almacenar genéricos. Pero debido a la clave, podemos llevar rápidamente el objeto al tipo que necesitamos.

¿Qué contiene el contenedor?

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

Contiene un multidelegado genérico y una colección donde la clave es el objeto que es el suscriptor, y el valor es el mismo método de controlador. De hecho, este diccionario contiene todos los objetos y métodos que pertenecen a este tipo. Como resultado, llamamos a un multidelegado, y él llama a todos los suscriptores, este es un sistema de eventos "honesto" en el que no hay restricciones para el suscriptor, pero en la mayoría de los otros agregadores, debajo del capó, se repite una colección de clases que se generalizan ya sea por una interfaz especial o heredadas de una clase que implementa el sistema. mensajes

Cuando se llama a un multidelegado, se realiza una verificación para ver si hay claves muertas, la colección se limpia de cadáveres y luego se acepta un multidelegado con suscriptores relevantes. Esto lleva tiempo, pero nuevamente, de hecho, si la funcionalidad de los eventos está separada, entonces un evento tendrá 3-5 suscriptores, por lo que la verificación no es tan aterradora, el beneficio de la comodidad es más obvio. Para las historias de red donde puede haber mil o más suscriptores, es mejor no usar este agregador. Aunque la pregunta sigue abierta, si elimina la verificación de cadáveres, que es más rápido, iterando sobre una matriz de suscriptores de 1k o llamando a un delegado múltiple de 1k suscriptores.

Características de uso


Una suscripción se empuja mejor a Despertar.

Si el objeto se activa / desactiva activamente, es mejor suscribirse a Awake y OnEnable, no se firmará dos veces, pero se excluirá la posibilidad de que GameObject esté inactivo.

Facturar eventos es mejor no antes del inicio, cuando todos los suscriptores serán creados y registrados.

El agregador limpia la lista en la descarga de la escena. En algunos agregadores, se sugiere limpiar la carga de la escena: este es un archivo, el evento de carga de la escena se produce después de Awake / OnEnable, los suscriptores agregados se eliminarán.

El agregador tiene: cadena pública estática DebugInfo (), puede ver qué clases están suscritas a qué eventos.

Repositorio de GitHub

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


All Articles