Agregador de eventos para Event Unity3d

A idéia de escrever seu próprio agregador de eventos estendidos para o Unity3d está muito atrasada. Depois de ler vários artigos sobre esse tópico, percebi que não há o suficiente “correto” (no Unity3d) e o agregador necessário para mim, todas as soluções são reduzidas e não possuem a funcionalidade necessária.

Funcionalidade necessária:


  1. Qualquer classe pode se inscrever em qualquer evento (geralmente os agregadores em uma unidade fazem assinantes específicos do Gameobject)
  2. A possibilidade de assinatura dupla de uma instância específica para um evento específico deve ser excluída (nas ferramentas padrão, você mesmo deve seguir isso)
  3. Deve haver funcionalidade de cancelamento manual de inscrição e automática, no caso de excluir uma instância / desativar o monobekh (quero me inscrever e não tomar um banho de vapor que o assinante repentinamente retire o casco)
  4. os eventos devem poder transferir dados / links de qualquer complexidade (quero me inscrever em uma linha e obter todo o conjunto de dados sem problemas)

Onde aplicar


  1. Isso é ideal para a interface do usuário, quando há a necessidade de encaminhar dados de qualquer objeto sem conexão.
  2. Mensagens sobre alterações de dados, algumas análogas do código reativo.
  3. Para injeção de dependência
  4. Retornos de chamada globais

Pontos fracos


  1. Devido a verificações de assinantes mortos e leva (revelarei mais adiante), o código é mais lento que soluções similares
  2. Classe / estrutura é usada como o núcleo do evento, para não alocar memória + o problema principal, não é recomendável enviar eventos de spam na atualização)

Ideologia geral


A ideologia geral é que, para nós, um evento é um pacote de dados específico e relevante. Digamos que pressionamos um botão em uma interface / joystick. E queremos enviar um evento com sinais de pressionar um botão específico para processamento adicional. O resultado do processamento de cliques são alterações visuais na interface e algum tipo de ação na lógica. Consequentemente, pode haver processamento / assinatura em dois locais diferentes.

Como é o corpo do evento / pacote de dados no meu caso:

Exemplo do corpo do evento
public struct ClickOnButtonEvent   {     public int ButtonID; //     enum    } 


Como é a inscrição do evento:

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

Para se inscrever, precisamos especificar:
Um objeto que é um assinante (geralmente é a própria classe na qual a assinatura, mas isso não é necessário, o assinante pode especificar uma das instâncias da classe nos campos da classe.
Tipo / evento em que estamos nos inscrevendo. Essa é a essência principal desse agregador, para nós um certo tipo de classe é um evento que ouvimos e processamos.
Inscrevendo-se melhor no Awake e OnEnable;

Exemplo

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

Para deixar claro o que o chip, considere um caso mais complexo


Temos ícones de personagens que:
  1. Saiba a qual personagem eles estão ligados.
  2. Reflita a quantidade de mana, hp, exp e status (atordoamento, cegueira, medo, loucura)

E aqui você pode realizar vários eventos

Para alterar indicadores:

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

Para alterar status negativos:

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

Por que nos dois casos passamos na classe de personagem? Aqui está o assinante do evento e seu manipulador:

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

Esse é o marcador pelo qual processamos o evento e entendemos exatamente o que precisamos dele.
Por que não supor que você se inscreva diretamente na classe Character? E spam eles?
Será difícil estrear, é melhor para um grupo de classes / eventos criar seu próprio evento separado.

Por que, novamente, dentro do evento, apenas não coloque o Personagem e tire tudo dele?
Portanto, a propósito, é possível, mas geralmente nas aulas há restrições de visibilidade e os dados necessários para o evento podem não ser visíveis do lado de fora.

se a classe é muito pesada para usar como marcador?
De fato, na maioria dos casos, um marcador não é necessário; um grupo de classes atualizadas é bastante raro. Geralmente, uma entidade específica precisa de um evento - um controlador / modelo de exibição, que geralmente exibe o estado do 1º caractere. E, portanto, sempre há uma solução banal - IDs de diferentes tipos (de inam, a hash complexo etc.).

O que há sob o capô e como funciona?


Código agregador diretamente
 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(); } } 


Vamos começar com o principal

Este é um dicionário no qual a chave é do tipo e o valor é contêiner

 public class EventContainer<T> : IDebugable 

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

Por que armazenamos o contêiner como um objeto? O dicionário não sabe como armazenar genéricos. Mas, devido à chave, somos capazes de levar rapidamente o objeto ao tipo que precisamos.

O que o contêiner contém?

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

Ele contém uma multilinha genérica e uma coleção em que a chave é o objeto que é o assinante e o valor é o mesmo método manipulador. De fato, este dicionário contém todos os objetos e métodos que pertencem a esse tipo. Como resultado, chamamos uma multilinha e ele chama todos os assinantes. Esse é um sistema de eventos “honesto” no qual não há restrições para o assinante, mas na maioria dos outros agregadores, uma coleção de classes que é generalizada por uma interface especial ou herdada de uma classe que implementa o sistema é iterada sob o capô. mensagens.

Quando uma chamada multilateral é chamada, é feita uma verificação para ver se há chaves mortas, a coleção é limpa de cadáveres e, em seguida, uma multilateral com assinantes relevantes. Isso leva tempo, mas, novamente, de fato, se a funcionalidade dos eventos for separada, um evento terá de 3 a 5 inscritos; portanto, a verificação não é tão assustadora, o benefício do conforto é mais óbvio. Para histórias de rede em que pode haver mil ou mais assinantes, é melhor não usar esse agregador. Embora a questão permaneça em aberto - se você remover a verificação de cadáveres, o que é mais rápido - itereça sobre uma matriz de assinantes de 1k ou chama um delegado múltiplo de 1k de assinantes.

Recursos de uso


É melhor enviar uma assinatura para Desperta.

Se um objeto estiver ativando / desativando ativamente, é melhor assinar Awake e OnEnable, ele não será assinado duas vezes, mas será excluída a possibilidade de um GameObject inativo ser levado como morto.

Eventos de faturamento é melhor não antes do início, quando todos os assinantes serão criados e registrados.

O agregador limpa a lista na descarga da cena. Em alguns agregadores, sugere-se limpar o carregamento da cena - este é um arquivo. O evento de carregamento da cena ocorre após Despertar / OnEnable, os assinantes adicionados serão excluídos.

O agregador possui - sequência estática pública DebugInfo (), você pode ver quais classes estão inscritas em quais eventos.

Repositório do GitHub

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


All Articles