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:
- Qualquer classe pode se inscrever em qualquer evento (geralmente os agregadores em uma unidade fazem assinantes específicos do Gameobject)
- 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)
- 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)
- 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
- Isso é ideal para a interface do usuário, quando há a necessidade de encaminhar dados de qualquer objeto sem conexão.
- Mensagens sobre alterações de dados, algumas análogas do código reativo.
- Para injeção de dependência
- Retornos de chamada globais
Pontos fracos
- Devido a verificações de assinantes mortos e leva (revelarei mais adiante), o código é mais lento que soluções similares
- 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 eventopublic struct ClickOnButtonEvent { public int ButtonID;
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:- Saiba a qual personagem eles estão ligados.
- Reflita a quantidade de mana, hp, exp e status (atordoamento, cegueira, medo, loucura)
E aqui você pode realizar vários eventosPara 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;
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 principalEste é 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