Ao implementar padrões comuns de design de programação de jogos em seu projeto Unity, você pode criar e manter com eficiência uma base de código limpa, organizada e legível. Os padrões de design não apenas reduzem a refatoração e o tempo gasto com testes, mas também aceleram os processos de integração e desenvolvimento, contribuindo para uma base sólida para o crescimento do jogo, da equipe de desenvolvimento e dos negócios.
Pense nos padrões de design não como soluções prontas que você pode copiar e colar em seu código, mas como ferramentas extras que podem ajudá-lo a criar aplicativos maiores e escalonáveis quando usados corretamente.
Esta página explica o padrão observer e como ele pode ajudar a apoiar o princípio de acoplamento frouxo entre objetos que interagem uns com os outros.
O conteúdo aqui é baseado no livro eletrônico gratuito, Eleve o nível de seu código com padrões de programação de jogosque explica padrões de design bem conhecidos e compartilha exemplos práticos para usá-los em seu projeto Unity.
Outros artigos da série de padrões de design de programação de jogos Unity estão disponíveis no hub de práticas recomendadas do Unity ou clique nos links a seguir:
No tempo de execução, várias coisas podem ocorrer em seu jogo. O que acontece quando o jogador destrói um inimigo? Ou quando eles recebem um aumento de poder ou de nível? Muitas vezes, você precisa de um mecanismo que permita que alguns objetos notifiquem outros sem fazer referência direta a eles. Infelizmente, à medida que sua base de código aumenta, isso adiciona dependências desnecessárias que podem levar à inflexibilidade e à sobrecarga excessiva na manutenção do código.
O padrão de observador é uma solução comum para esse problema. Ele permite que seus objetos se comuniquem, mas permaneçam fracamente acoplados usando uma dependência "um-para-muitos". Quando um objeto muda de estado, todos os objetos dependentes são notificados automaticamente.
Uma analogia para ajudar a visualizar isso é uma torre de rádio que transmite para muitos ouvintes diferentes. Ele não precisa saber quem está sintonizando, apenas que a transmissão está sendo feita ao vivo na frequência certa e no momento certo.
O objeto que está sendo transmitido é chamado de sujeito. Os outros objetos que estão ouvindo são chamados de observadores ou, às vezes, de assinantes (esta página usa o nome observador em todo o texto).
A vantagem do padrão é que ele separa o sujeito do observador, que não conhece realmente os observadores nem se importa com o que eles fazem quando recebem o sinal. Embora os observadores dependam do sujeito, os próprios observadores não sabem uns dos outros.
Os observadores são simplesmente notificados sempre que o estado do sujeito muda, permitindo que eles se atualizem de acordo. Dessa forma, fica mais fácil modificar ou estender o código sem afetar outras partes do sistema.
Outro benefício é que o padrão de observador incentiva o desenvolvimento de código reutilizável, pois os observadores podem ser reutilizados em diferentes contextos sem a necessidade de modificação. Por fim, isso geralmente melhora a legibilidade do código, pois as dependências entre os objetos são claramente definidas.
Você pode criar suas próprias classes de sujeito-observador, mas isso geralmente é desnecessário, pois o C# já implementa o padrão usando eventos. O padrão de observador é tão difundido que foi incorporado à linguagem C#, e por um bom motivo: Ele pode ajudá-lo a criar um código mais modular, reutilizável e de fácil manutenção.
O que é um evento? É uma notificação que indica que algo aconteceu e envolve algumas etapas:
O editor (também conhecido como o sujeito) cria um evento com base em um delegado que estabelece uma assinatura de função específica. O evento é apenas uma ação que o sujeito executará em tempo de execução (por exemplo, receber dano, clicar em um botão etc.). O editor mantém uma lista de seus dependentes (os observadores) e envia notificações a eles quando seu estado muda, o que é representado por esse evento.
Em seguida, cada observador cria um método chamado manipulador de eventos, que deve corresponder à assinatura do delegado. Os observadores são objetos que recebem notificações do editor e se atualizam de acordo.
O manipulador de eventos de cada observador se inscreve no evento do editor. Você pode ter quantos observadores forem necessários para participar da assinatura. Todos eles aguardarão o acionamento do evento.
Quando o editor sinaliza a ocorrência de um evento em tempo de execução, isso é chamado de aumento do evento. Isso, por sua vez, invoca os manipuladores de eventos dos observadores, que executam sua própria lógica interna em resposta.
Dessa forma, você faz com que muitos componentes reajam a um único evento do assunto. Se o sujeito indicar que um botão foi clicado, os observadores poderão reproduzir uma animação ou som, acionar uma cena ou salvar um arquivo. A resposta deles pode ser qualquer coisa, e é por isso que você encontrará com frequência o padrão observer usado para enviar mensagens entre objetos.
Delegados versus eventos
Um delegado é um tipo que define uma assinatura de método. Isso permite que você passe métodos como argumentos para outros métodos. Pense nisso como uma variável que contém uma referência a um método, em vez de um valor.
Um evento, por outro lado, é essencialmente um tipo especial de delegado que permite que as classes se comuniquem umas com as outras de uma forma livremente acoplada. Para obter informações gerais sobre as diferenças entre delegados e eventos, consulte Distinguindo delegados e eventos em C#.
Vamos dar uma olhada em como você pode definir um assunto/editor básico no código abaixo.
Na classe Subject do exemplo de código abaixo, você herda do MonoBehaviour para poder anexá-lo a um GameObject com mais facilidade, embora isso não seja um requisito.
Embora você tenha liberdade para definir seu próprio delegado personalizado, também pode usar o System.Action, que funciona na maioria dos casos de uso. No exemplo de código, não há necessidade de enviar parâmetros com o evento, mas se isso for necessário, é tão fácil quanto usar o delegado Action<T> e passá-los como uma List<T> dentro dos colchetes angulares (até 16 parâmetros).
No trecho de código, ThingHappened é o evento real, que o sujeito invoca no método DoThing.
O operador "?." é um operador condicional nulo, o que significa que o evento só será chamado se não for nulo. O método Invoke é usado para gerar um evento, o que significa que ele executará todos os manipuladores de eventos que estiverem inscritos no evento. Nesse caso, o método DoThing acionará o evento ThingHappened se ele não for nulo, o que executará todos os manipuladores de eventos que estiverem inscritos no evento.
Você pode fazer o download de um projeto de amostra que demonstra o observador e outros padrões de design em ação. Esse exemplo de código está disponível aqui.
Para ouvir o evento, você pode criar uma classe Observer de exemplo, como o exemplo de código reduzido abaixo (também disponível no projeto do Github).
Anexe esse script a um GameObject como um componente e faça referência ao subjectToObserver no Inspector para ouvir o evento ThingHappened.
O método OnThingHappened pode conter qualquer lógica que o observador execute em resposta ao evento. Muitas vezes, os desenvolvedores adicionam o prefixo "On" para indicar o manipulador de eventos (use a convenção de nomenclatura do seu guia de estilo).
Em Awake ou Start, você pode se inscrever no evento com o operador +=. Isso combina o método OnThingHappened do observador com o ThingHappened do sujeito.
Se algo executar o método DoThing do sujeito, isso gerará o evento. Em seguida, o manipulador do evento OnThingHappened do observador é invocado automaticamente e imprime a instrução de depuração.
Observação: Se você excluir ou remover o observador em tempo de execução enquanto ele ainda estiver inscrito no ThingHappened, a chamada desse evento poderá resultar em um erro. Portanto, é importante cancelar a assinatura do evento no método OnDestroy do MonoBehaviour com um operador -= nos momentos apropriados do ciclo de vida do objeto.
Se você fizer o download do projeto de amostra e for para a pasta chamada 11 Observer, encontrará um exemplo que mostra um botão simples (ExampleSubject) e um alto-falante (AudioObserver), uma animação (AnimObserver) e um efeito de partícula (ParticleSystemObserver).
Quando você clica no botão, o ExampleSubject invoca um evento ThingHappened. O AudioObserver, o AnimObserver e o ParticleSystemObserver invocam seus métodos de tratamento de eventos em resposta.
Os observadores podem existir no mesmo GameObjects ou em GameObjects diferentes. Observe que o AnimObserver produz a animação do botão no ExampleSubject, enquanto os AudioObservers e o ParticleSystemObserver ocupam GameObjects diferentes.
O ButtonSubject permite que o usuário invoque um evento Clicked com o botão do mouse. Vários outros GameObjects com os componentes AudioObserver e ParticleSystemObserver podem, então, responder de suas próprias maneiras ao evento.
A determinação de qual objeto é um sujeito e qual é um observador varia apenas de acordo com o uso. Qualquer coisa que suscite o evento atua como sujeito, e qualquer coisa que responda ao evento é o observador. Componentes diferentes no mesmo GameObject podem ser sujeitos ou observadores. Até mesmo o mesmo componente pode ser um sujeito em um contexto e um observador em outro.
Por exemplo, o AnimObserver do exemplo adiciona um pouco de movimento ao botão quando ele é clicado. Ele atua como um observador, embora faça parte do GameObject ButtonSubject.
O Unity também inclui um sistema separado de UnityEventsque usa o UnityAction da API UnityEngine.Events. Ele pode ser configurado no Inspector (que fornece uma interface gráfica para o padrão de observador), permitindo que os desenvolvedores especifiquem quais métodos devem ser invocados quando o evento é gerado.
Se você já usou o sistema de interface do usuário do Unity (por exemplo, criando o evento OnClick de um botão da interfacedo usuário), já tem alguma experiência com isso.
Na imagem acima, o evento OnClick do botão invoca e aciona uma resposta dos métodos OnThingHappened dos dois AudioObservers. Assim, você pode configurar o evento de um sujeito sem código.
Os UnityEvents são úteis se você quiser permitir que designers ou não programadores criem eventos de jogabilidade. No entanto, esteja ciente de que eles podem ser mais lentos do que seus eventos ou ações equivalentes do namespace System. As UnityActions também têm o benefício extra de serem usadas para invocar métodos que recebem argumentos, enquanto os UnityEvents são limitados a métodos que não têm argumentos.
Pese o desempenho em relação ao uso ao considerar UnityEvents e UnityActions. Os UnityEvents são mais simples e fáceis de usar, mas são mais limitados em termos dos tipos de métodos que podem ser invocados. Alguns também podem argumentar que eles podem ser mais propensos a erros ao expor todos os eventos no Inspector.
Consulte o módulo Criar um sistema de mensagens simples com eventos no Unity Learn para ver um exemplo.
A implementação de um evento exige um pouco mais de trabalho, mas oferece vantagens:
O padrão de observador ajuda a desacoplar seus objetos: O editor do evento não precisa saber nada sobre os próprios assinantes do evento. Em vez de criar uma dependência direta entre uma classe e outra, o sujeito e o observador se comunicam enquanto mantêm um grau de separação (acoplamento frouxo).
Você não precisa construí-lo: O C# inclui um sistema de eventos estabelecido, e você pode usar o delegado System.Action em vez de definir seus próprios delegados. Como alternativa, o Unity também inclui UnityEvents e UnityActions.
Cada observador implementa sua própria lógica de tratamento de eventos: Dessa forma, cada objeto de observação mantém a lógica necessária para responder. Isso facilita a depuração e o teste de unidade.
Ele é adequado para a interface do usuário: Seu código principal de jogabilidade pode viver separadamente da lógica da interface do usuário. Seus elementos de interface do usuário ouvem eventos ou condições específicas do jogo e respondem adequadamente. Os padrões MVP e MVC usam o padrão de observador para essa finalidade.
No entanto, você também deve estar ciente dessas ressalvas:
Isso adiciona mais complexidade: Assim como outros padrões, a criação de uma arquitetura orientada por eventos exige mais configuração no início. Além disso, tenha cuidado ao excluir sujeitos ou observadores. Certifique-se de cancelar o registro dos observadores em OnDestroy para que a referência de memória seja liberada corretamente quando o observador não for mais necessário.
Os observadores precisam de uma referência à classe que define o evento: Os observadores ainda têm uma dependência da classe que está publicando o evento. O uso de um EventManager estático (consulte a próxima seção) que lida com todos os eventos pode ajudar a separar os objetos uns dos outros
O desempenho pode ser um problema: A arquitetura orientada por eventos adiciona uma sobrecarga extra. Cenas grandes e muitos GameObjects podem prejudicar o desempenho.
Embora apenas uma versão básica do padrão de observador tenha sido apresentada aqui, você pode expandi-la para atender a todas as necessidades do seu aplicativo de jogo.
Considere estas sugestões ao configurar o padrão do observador:
Use a classe ObservableCollection: O C# fornece uma coleção dinâmica ObservableCollection para rastrear alterações específicas. Ele pode notificar seus observadores quando os itens são adicionados, removidos ou quando a lista é atualizada.
Passe um ID de instância exclusivo como argumento: Cada GameObject na hierarquia tem um ID de instância exclusivo. Se você acionar um evento que possa se aplicar a mais de um observador, passe o ID exclusivo para o evento (use o tipo Action<int>). Em seguida, execute a lógica no manipulador de eventos somente se o GameObject corresponder ao ID exclusivo.
Criar um EventManager estático: Como os eventos podem conduzir grande parte da sua jogabilidade, muitos aplicativos Unity usam um EventManager estático ou único. Dessa forma, seus observadores podem fazer referência a uma fonte central de eventos do jogo como o assunto para facilitar a configuração.
O Microgame FPS tem uma boa implementação de um EventManager estático que implementa GameEvents personalizados e inclui métodos auxiliares estáticos para adicionar ou remover ouvintes.
O Unity Open Project também apresenta uma arquitetura de jogo em que os ScriptableObjects retransmitem UnityEvents. Ele usa eventos para reproduzir áudio ou carregar novas cenas.
Criar uma fila de eventos: Se houver muitos objetos em sua cena, talvez você não queira gerar os eventos todos de uma vez. Imagine a cacofonia de mil objetos reproduzindo sons quando você invoca um único evento. A combinação do padrão de observador com o padrão de comando permite que você encapsule seus eventos em uma fila de eventos. Em seguida, você pode usar um buffer de comando para reproduzir os eventos um de cada vez ou ignorá-los seletivamente conforme necessário (por exemplo, se você tiver um número máximo de objetos que possam emitir sons de uma só vez).
O padrão de observador está fortemente inserido no padrão arquitetônico Model View Presenter (MVP), que é abordado no e-book Eleve o nível de seu código com padrões de programação de jogos.
Você encontrará muitas outras dicas sobre como usar padrões de design em seus aplicativos Unity, bem como os princípios SOLID, no e-book gratuito Eleve o nível de seu código com padrões de programação de jogos.
Todos os e-books e artigos técnicos avançados do Unity estão disponíveis no hub de práticas recomendadas. Os e-books também estão disponíveis na página de práticas recomendadas avançadas na documentação.