Enquanto estudava a engine Unity notei como é desajeitado usar referências para se comunicar entre objetos. Aumentando a necessidade de configurações conforme a quantidade de objetos que precisavam se comunicar, o que resulta em maiores chances de erros durante o desenvolvimento.
Conforme pesquisava sobre o assunto encontrei uma solução interessante usando um sistema de eventos centralizado, mas essa solução também tinha alguns pequenos problemas que mereciam melhorias, então decidi botar a mão nassa.
O resultado foi um sistema centralizado que permite fácil configuração tanto pelo lado de programação quanto pelo lado dos designers.
Conversa Técnica
O Problema
Usar a forma de referências mais conhecida no Unity traz algumas limitações:
Se uma referência pode ser destruída, a lógica precisa verificar nulos toda hora.
Multas lógicas precisam ser avaliadas a cada frame esperando atualizações de estado.
Se é preciso fazer uma conversa entre vários objetos, pode se tornar um inferno de referências nulas.
Se torna difícil e custoso fazer a comunicação entre o diferentes objetos em tela de forma dinâmica e modular.
Pesquisando sobre o assunto, encontrei uma apresentação de Mike Mittleman no Unite 2008 e um bloco de código criado por Will R. Miller que lidam com esse problema usando um gerenciador de eventos. Com isso:
Resolvemos em boa parte os problemas de referência já que os objetos ficam responsáveis por se cadastrarem, sair e acionar os eventos sem precisarem se conhecer.
As chamadas são feitas sob-demanda, evitando a necessidade de inúmeros Update() rodando a cada frame, economizando processamento.
As chamadas centralizadas exigem bem menos configurações no Editor.
A identificação de cada grupo de eventos é feita usando derivações de uma classe base como argumento, não tem erro e aceita quantos dados forem necessários dentro da classe de argumento.
É uma boa solução, mas senti que a implementação do Will merecia algumas melhorias:
Delegates precisam ser públicos e isso cria uma falha onde qualquer código que acesse a classe está sujeito a erros de lógica que podem apagar todas as referências de eventos.
Pessoalmente, não gosto de singleton. Além disso, mantendo o gerenciador como objeto instanciável permite criar quantos forem necessários. Útil para ter tanto comunicações globais quanto locais.
A classe é bem simples, podendo ter algumas funções extras de gerenciamento e permitir que seja usada assim que colocada no projeto.
O Resultado
A primeira coisa que precisei fazer foi abandonar o delegate e usar Action<T>, uma versão mais "avançada" que exige implementar o evento de forma privada e também lida com genérico. Em seguida encapsulei esse evento em uma classe para permitir fazer múltiplas instâncias para tipos de eventos diferentes.
Para diferenciar esses eventos usei uma Interface pra evitar que qualquer variável seja usada e crie uma confusão de eventos diferentes sendo tratados como iguais. A unica exigência da interface é definir o objeto que fez a chamada do evento, padrão clássico de eventos no .NET que costuma ser bem útil.
Substitui o singleton por instâncias que podem ser encapsuladas em Componentes e ScriptableObjects, isso permite que a equipe de design possa desenvolver sem dor de cabeça.
Testes de uso
Criei um projeto de exemplo que simula um teste de estresse. Nele, cada novo objeto criado manda mensagens para todos os outros a cada frame.
O consumo de processamento se demonstrou muito leve, podendo disparar por segundo milhares de eventos resultando em milhões de mensagens. Claro que esse números dependem tanto da plataforma quanto do conteúdo do projeto.
Já no lado da memória fiz a avaliação pelo Profiler, o que indicou um uso bem gordo de memória por segundo enviando milhares de mensagens, o uso aumentou ainda mais conforme a quantidade de dados enviados no argumento, isso gerou um uso excessivo do Garbage Collector, o que em situações reais pode resultar em micro-travamentos constantes.
Felizmente descobri uma solução engenhosa. Tratando os argumentos como Struct ao invés de Classe faz os dados trocarem o Heap pelo Stack, uma área da memória que é muito mais eficiente em lidar com limpeza de dados e não precisa do GC.
Nesse cenário de teste, 1.000 objetos ativos geraram 60.000.000 de mensagens recebidas por segundo e mantiveram uma boa estabilidade de processamento e pouco uso de memória. É importante lembrar que esse é um caso extremo e que o gargalo de desempenho vem da quantidade excessiva de mensagens. Em um cenário real é possível ter centenas de objetos ativos que estarão se comunicando de forma mais esparsa, o que faz esse sistema se tornar praticamente invisível.
Observações
Infelizmente não consegui pensar em uma forma prática de desconectar automaticamente os eventos quando os objetos são destruídos. Isso ainda precisa ser feito manualmente quando o objeto é destruído ou desativado, sendo uma potencial fonte de erros e vazamento de memória.
Créditos
Conceito original por Mike Mittleman
Código original por Will R. Miller
Licença
Publiquei o meu projeto usando a licença MIT, porém o autor original nunca deu uma licença explicita ao código.
Seguindo as conversas no blog dele vi várias pessoas que fizeram código derivados e até alguém que colocou uma versão modificada na Asset Store de graça com a benção do próprio autor.
Mas, sem uma licença explícita, pode ser arriscado usar essas classes o a original em projetos comerciais. Recomendo consultar o autor antes.