
Use ScriptableObjects como canais de eventos no código do jogo
Esta página da Web foi automaticamente traduzida para sua conveniência. Não podemos garantir a precisão ou a confiabilidade do conteúdo traduzido. Se tiver dúvidas sobre a precisão do conteúdo traduzido, consulte a versão oficial em inglês da página da Web.
Como você faz com que sistemas diferentes em seu aplicativo funcionem juntos? Uma solução comum é usar um evento para enviar mensagens entre objetos. Continue lendo para aprender como usar ScriptableObjects como canais de eventos no seu projeto Unity.
Este é o quinto de uma série de seis miniguias criados para auxiliar os desenvolvedores do Unity com a demonstração que acompanha o e-book, Crie arquitetura de jogo modular no Unity com ScriptableObjects.
A demonstração é inspirada na mecânica clássica de jogos de arcade de bola e raquete e mostra como o ScriptableObjects pode ajudar você a criar componentes testáveis, escaláveis e fáceis de usar.
Juntos, o e-book, o projeto de demonstração e esses miniguias fornecem práticas recomendadas para usar padrões de design de programação com a classe ScriptableObject no seu projeto Unity. Essas dicas podem ajudar você a simplificar seu código, reduzir o uso de memória e promover a reutilização do código.
Esta série inclui os seguintes artigos:
Nota importante antes de começar
Antes de mergulhar no projeto de demonstração do ScriptableObject e nesta série de miniguias, lembre-se de que, em essência, os padrões de design são apenas ideias. Elas não se aplicam a todas as situações. Essas técnicas podem ajudar você a aprender novas maneiras de trabalhar com Unity e ScriptableObjects.
Cada padrão tem prós e contras. Escolha apenas aquelas que beneficiam significativamente seu projeto específico. Seus designers dependem muito do Unity Editor? Um padrão baseado em ScriptableObject pode ser uma boa escolha para ajudá-los a colaborar com seus desenvolvedores.
Em última análise, a melhor arquitetura de código é aquela que se adapta ao seu projeto e à sua equipe.
Acoplamento solto, alta coesão
Ao construir diferentes módulos ou sistemas em um aplicativo, geralmente é útil pensar neles como “ilhas de código”. Cada módulo pode ter vários componentes ou GameObjects que trabalham juntos para um propósito comum.
Por exemplo, a raquete do jogador pode compreender um script que interpreta a entrada do jogador, um que lida com movimentos ou colisões e assim por diante. Se essas partes tiverem interdependências entre si, você pode usar o Inspetor para fazer essas conexões próximas.
No entanto, tenha em mente que toda vez que você adiciona uma dependência a outro objeto, isso acarreta um pequeno risco. Sempre que possível, você deve minimizar essas dependências com objetos externos. A comunicação com coisas que estão fora do seu módulo ou sistema não será tão direta.
Você poderia fazer com que o script Paddle se referisse à Bola no seu jogo, mas isso significa que eles têm uma conexão. Uma vez que eles são unidos por uma dependência, fazer uma alteração em um pode afetar o outro.
O ideal é que você consiga modificar parte do aplicativo sem quebrar mais nada. O objetivo é manter seus módulos internamente coesos, mas externamente desacoplados.
Você pode usar a classe NullRefChecker do projeto para emitir um aviso educado quando referências necessárias no Inspetor estiverem faltando. Basta chamar o método estático Validate em algum lugar (por exemplo, em Awake) depois que cada componente tiver sido configurado ou inicializado.
Adicione o atributo opcional personalizado para ignorar a verificação caso o campo possa ser deixado indefinido.
Usando eventos
Então, como você faz com que esses sistemas diferentes em seu aplicativo funcionem juntos?
Uma solução é usar um evento para enviar mensagens entre objetos. Os eventos seguem o modelo emissor-ouvinte, visualizado na imagem acima.
Aqui, o objeto de escuta assina o evento no transmissor, em vez de chamar um método ou referenciar uma propriedade diretamente.
Alterações feitas em um componente têm menos impacto nos outros. As coisas ainda podem quebrar quando você modifica o código, mas os objetos não ficarão tão interligados. O evento no meio funciona como um buffer entre eles.
Frequentemente descrevemos objetos nessa relação emissor-ouvinte como frouxamente acoplados.
Você pode ler mais sobre eventos e o padrão observador em nosso e-book técnico, Melhore seu código com padrões de programação de jogos.

UM SISTEMA DE EVENTOS CENTRALIZADO
Eventos centralizados
No cenário acima, a emissora é responsável apenas por enviar um sinal. Não importa quais objetos estão escutando.
No entanto, o ouvinte ainda precisa ter algum conhecimento do transmissor para assinar e cancelar a assinatura do delegado usando os métodos OnEnable e OnDisable.
Você pode desacoplar ainda mais o transmissor e o ouvinte movendo eventos para uma classe estática. Uma classe geral de “eventos de jogo” pode ajudar a inserir uma camada adicional de abstração entre os dois. Isso pode conectar o locutor e o ouvinte sem que eles tenham conhecimento direto um do outro.
Neste exemplo, usaremos uma classe GameEvents estática para simplificar. Entretanto, em um cenário de produção do mundo real, é melhor dividi-lo em classes menores e especializadas por função, como UIEvents, GameStateEvents, HealthEvents, InventoryEvents, etc.
Por exemplo, você pode criar eventos estáticos para sair do aplicativo, mostrar uma tela de interface do usuário ou carregar uma cena. Ao tornar esses eventos estáticos, eles podem ser acessados de qualquer parte do seu aplicativo.
Por exemplo, você pode criar GameEvents como visto no exemplo abaixo.
O GameEvent estático fica entre o transmissor original e o ouvinte como intermediário. Modificações no destinatário ou no remetente têm menor probabilidade de impactar o outro.
Consequentemente, a atualização do código tem menos efeitos colaterais inesperados. Armazenar suas definições de eventos em um único local também as torna mais fáceis de gerenciar.
Embora os GameEvents estáticos sejam eficazes, eles podem não ser muito acessíveis aos seus designers de jogos. Como são estáticos, eles devem ser definidos dentro do código e não são serializáveis nativamente no Editor.
Para um sistema mais amigável ao Editor, considere implementar eventos baseados em ScriptableObjects.
using UnityEngine;
using System;
public static class GameEvents
{
public static Action ExitApplication;
public static Action HomeScreenShown;
public static Action<float> LoadProgressUpdated;
}
CANAIS DE EVENTOS REPASSAM SINAIS ENTRE EMISSORES E OUVINTES.
Configurando canais de eventos
Eventos baseados em ScriptableObject oferecem uma alternativa gráfica aos eventos estáticos. Embora ambos tenham funções semelhantes, os ScriptableObjects tendem a ser amigáveis ao designer porque aparecem no Inspetor.
Como eles retransmitem um sinal de um transmissor para um ouvinte, você pode pensar neles como “canais de eventos”, que são análogos a uma transmissão de uma torre de rádio.
Qualquer ScriptableObject com o seguinte pode funcionar como um canal de eventos:
- Um delegado (como UnityAction ou System.Action): Isso notifica os assinantes e passa dados como parâmetros.
- Um método de captação de eventos: Este método público invoca o delegado.
Você pode configurar qualquer número de canais de eventos para determinar vários aspectos da jogabilidade.
UnityAction e System.Action são ambos delegados. Você pode usar um ou ambos os tipos em seu projeto.
O UnityAction cria uma experiência mais amigável ao artista. Caso contrário, use o delegado System.Action.
Abaixo você encontrará um exemplo de um VoidEventChannelSO do projeto. Este é um evento baseado em ScriptableObject que não passa nenhum parâmetro.
Aqui usamos uma UnityAction chamada OnEventRaised e expomos um método público RaiseEvent.
using UnityEngine;
using UnityEngine.Events;
[CreateAssetMenu(menuName = "Events/Void Event Channel",
fileName = "VoidEventChannel")]
public class VoidEventChannelSO : DescriptionSO
{
[Tooltip("The action to perform")]
public UnityAction OnEventRaised;
public void RaiseEvent()
{
if (OnEventRaised != null)
OnEventRaised.Invoke();
}
}

CRIE CANAIS DE EVENTOS NO PROJETO.
Criando os ativos do canal de eventos
Crie o recurso de canal de evento no projeto para usá-lo. Você pode usar o menu Criar ou duplicar um ativo existente.
Renomeie cada ativo e use o campo de descrição para identificar cada ativo ScriptableObject. Lembre-se de que cada canal de evento existe como um ativo em nível de projeto. Você fará referência a esses ativos em seus MonoBehaviours.
Embora seja opcional, você pode marcar os canais de eventos baseados em ScriptableObject com o sufixo _SO para diferenciá-los de outros ScriptableObjects que transportam dados (que têm o sufixo _Data).
Pastas e convenções de nomenclatura podem ajudar a manter seu projeto organizado. Você vai querer personalizá-los de acordo com as necessidades do seu projeto. Leia Criar um guia de estilo C# para mais informações.

ATRIBUA O CANAL DE EVENTOS NO INSPETOR.
Levantamento de eventos
Qualquer objeto na sua cena agora pode referenciar o canal de eventos e chamar o evento usando o método RaiseEvent . Por exemplo, veja o exemplo MonoBehaviour com um método TriggerEvent abaixo.
No Inspetor, o ativo ScriptableObject precisa ser atribuído ao campo m_EventChannel . Quando algo invoca TriggerEvent, o evento é executado. Qualquer coisa que esteja ouvindo recebe uma notificação.
Esse mecanismo adiciona muita interatividade ao seu aplicativo de jogo. Cada módulo ou sistema gera um evento (por exemplo, o sistema de entrada registra um pressionamento de tecla, uma bola colide com uma parede, etc.). Como resposta, algo mais reage a isso.
public class EventRaiser: MonoBehaviour
{
[SerializeField]
private VoidEventChannelSO m_EventChannel;
public void TriggerEvent()
{
m_EventChannel.RaiseEvent();
}
}

O GERENTE DE JOGO OUVE CERTOS CANAIS DE EVENTOS E TRANSMITE EM OUTROS.
Ouvindo eventos
Para configurar um ouvinte, um MonoBehaviour ou outro componente precisará assinar o evento OnEventRaised do canal de eventos. Normalmente isso acontece emOnEnable, como no exemplo abaixo.
Quando o canal de eventos gera um evento, o método HandleEvent é executado em resposta. Esse mecanismo pode ser usado para vários propósitos, como reproduzir sons ou efeitos, modificar configurações, etc., dependendo do contexto do evento.
No projeto PaddleBallSO , é assim que configuramos o loop principal do jogo. O GameManager escuta um conjunto de canais de eventos e depois transmite em outro. Isso permite que diferentes sistemas enviem mensagens entre si sem necessariamente ter dependências diretas.
Por fim, cancele a inscrição do evento OnEventRaised no método OnDisable para evitar erros ou vazamentos de memória.
public class EventListener: MonoBehaviour
{
[SerializeField]
private VoidEventChannelSO m_EventChannel;
private void OnEnable()
{
m_EventChannel.OnEventRaised += HandleEvent;
}
private void OnDisable()
{
m_EventChannel.OnEventRaised -= HandleEvent;
}
private void HandleEvent()
{
Debug.Log("Event received");
}
}

INTERATIVIDADE SEM CÓDIGO CONFIGURADA NO INSPETOR
Adicionando um ouvinte sem código
Se você estiver trabalhando com designers, talvez seja interessante fornecer a eles um script pré-configurado de uso geral que possa escutar um evento. Isso permitirá que eles criem interações de jogo sem um programador.
O VoidEventChannelListener é um exemplo disso. Este componente gera um UnityEvent quando recebe um sinal de um canal de eventos. Basta adicionar o VoidEventChannelListener a um GameObject e definir o canal de eventos e a lógica do UnityEvent.
Um designer pode então criar protótipos de lógica orientada a eventos com apenas algumas configurações no Inspetor.
Por exemplo, o prefab GameOverSounds escuta o canal de eventos GameOver_SO . Depois de receber esse evento, ele reproduz um som no AudioSource fornecido por meio do m_Response UnityEvent.
A classe VoidEventChannelListener também inclui um atraso útil para ajustar o tempo de cada resposta.
Com um pouco de prática, esta é uma maneira simples de criar interações entre seus diferentes sistemas e módulos.

CANAIS DE EVENTOS MARCADOS PARA ENVIO E RECEBIMENTO
Como os canais de eventos ajudam
Como existem no nível do projeto, os canais de eventos são acessíveis globalmente. Isso permite que eles conectem qualquer objeto na Hierarquia de Cena e persistam durante carregamentos de cena.
Qualquer objeto pode atuar como um transmissor ou ouvinte – é apenas uma questão de como ele interage com o canal de eventos. Isso lhe dá bastante flexibilidade no envio de mensagens.
Observação: É uma boa prática indicar no Inspetor se o canal é para envio ou recebimento. Use o HeaderAttribute para fazer isso.
Um benefício adicional do uso de eventos no nível do projeto é que eles geralmente podem substituir a necessidade de um singleton. Os canais de eventos estão disponíveis globalmente, então eles podem conectar qualquer coisa com qualquer coisa. Deixe que eles controlem sistemas do jogo, como câmeras, missões, saúde e conquistas, tudo sem criar dependências desnecessárias.
Além disso, como uma arquitetura baseada em eventos é executada somente quando necessário, ela é mais otimizada do que os métodos de atualização do MonoBehaviour.
A assinatura da função de um evento base
Esta classe VoidEventChannelSO só funciona para eventos que não precisam de parâmetros. Muitas vezes, o evento gerado precisa de uma carga adicional de dados para ser significativo.
Por exemplo, se você estiver enviando um evento que causa dano em um sistema de saúde, talvez você queira passar um valor para o alvo, quanto dano enviar, que tipo de dano, etc.
Você pode alterar a assinatura da função do seu evento base para tornar o canal de eventos mais adequado para isso. O projeto define um GenericEventChannelSO para essa finalidade. Veja o exemplo abaixo.
Esta é uma classe abstrata com um único parâmetro genérico. Você derivará outros canais de eventos a partir dele. Eles podem então passar um único parâmetro, como float, int ou bool.
Assim como o VoidEventChannelSO, o GenericEventChannelSO apresenta uma UnityAction chamada OnEventRaised. Entretanto, desta vez a ação carrega um parâmetro do tipo T.
Objetos externos invocarão o método público RaiseEvent correspondente. Se o evento tiver ouvintes, ele será executado ao passar um parâmetro fornecido.
public abstract class GenericEventChannelSO<T>: DescriptionSO
{
public UnityAction<T> OnEventRaised;
public void RaiseEvent(T parameter)
{
if (OnEventRaised == null)
return;
OnEventRaised.Invoke(parameter);
}
}
Criando canais de eventos concretos
Agora você só precisa derivar canais de eventos concretos de GenericEventChannelSO e preencher o valor para T.
Além do atributo CreateAssetMenu usual, não há necessidade de detalhes explícitos de implementação.
Criar um canal de eventos que carrega um float, FloatEventChannelSO, é simples. Dê uma olhada no exemplo de código abaixo.
É simples assim! Use este fluxo de trabalho para criar fluxos de trabalho adicionais para BoolEventChannelSO, IntEventChannelSO, etc.
Se você precisar de mais de um parâmetro como carga útil, defina classes genéricas adicionais (por exemplo, GenericEventChannelSO<T,U>, GenericEventChannelSO<T,U,V>, etc.) conforme necessário.
[CreateAssetMenu(menuName = "Events/Float EventChannel", fileName = "FloatEventChannel")]
public class FloatEventChannelSO : GenericEventChannelSO<float> {}

SEQUÊNCIA DE EVENTOS QUANDO A BOLA ATINGE UM GOL DE PONTUAÇÃO
Juntando tudo
A ideia é dividir o aplicativo em partes menores e mais modulares. Estabelecer limites claros ao redor deles evita que essas partes se entrelacem com dependências e ajuda a evitar código espaguete.
Componentes que não têm conhecimento direto de objetos externos não podem manipular algo que não deveriam. Em vez disso, eles são forçados a enviar e receber mensagens por meio de canais de eventos.
Você pode ver como isso funciona se traçar uma pequena sequência do jogo de paddle ball. Por exemplo, vamos imaginar o que acontece quando uma bola colide com um ScoreGoal:
O componente ScoreGoal registra uma colisão. Após detectar a Bola, ele gera um evento no canal de eventos GoalHit_SO . Isso passa o ID do jogador que pontuou.
O canal de eventos notifica o GameManager, que em resposta gera outro canal de eventos chamado PointsScored_SO. Isso também passa o ID do jogador.
Este canal notifica o ScoreManager, que incrementa a pontuação (armazenada em um objeto separado) e atualiza os componentes da interface do usuário. Em seguida, ele passa as pontuações de ambos os jogadores pelo canal de eventos ScoreManagerUpdated_SO .
Como resposta, o objetivo ScoreObjective_SO verifica se um jogador atingiu a pontuação alvo.
Se uma condição de vitória for alcançada, o jogo termina. Caso contrário, o GameManager reinicia a rodada e a bola volta ao jogo.
À primeira vista, pode parecer muito trabalho extra aumentar um valor de pontuação em um ponto. No entanto, a intenção é isolar todas as peças envolvidas: A Bola, o ScoreManager, o GameManager, o ObjectiveManager, etc.
Cada parte do aplicativo tem uma certa autonomia, o que torna cada uma delas mais fácil de testar. Adicionar novos sistemas não precisa interromper a lógica existente. Na verdade, a jogabilidade original pode ser completamente alheia a eles.
Imagine que você queira adicionar efeitos secundários, como sons e animações, para acompanhar o processo de pontuação. Você pode criar novos componentes que ouçam os eventos certos e respondam apropriadamente. A lógica subjacente e o fluxo do jogo podem permanecer inalterados, mesmo quando você adiciona novos sistemas.
Lembre-se de que o mantra na programação SOLID é “aberto para extensão, fechado para modificação”. Você quer ter a capacidade de adicionar novas funcionalidades ao seu software sem precisar alterar o código existente. Usar canais de eventos como esse proporciona escalabilidade.

O SCRIPT DO EDITOR PODE AJUDAR A DEPURAR EVENTOS.
Eventos de depuração
A arquitetura orientada a eventos facilita a depuração e a manutenção. Partes menores são mais fáceis de testar, não importa se você está escrevendo testes unitários automatizados com o Unity Test Framework ou apenas solucionando problemas informalmente. Isso permite que você se concentre em um problema específico e faça testes isolados.
O script do Editor personalizado pode ajudar aqui. O PaddleBallSO demonstra algumas ferramentas que ajudam a rastrear o fluxo do seu aplicativo ao usar canais de eventos:
- A maioria dos canais de eventos no projeto PaddleBallSO mostra uma lista de ouvintes no Inspetor. Clique no nome de cada ouvinte para destacá-lo na Hierarquia.
- Um botão RaiseEvent personalizado pode invocar um evento simulado à vontade (usando o valor padrão de T se estiver carregando uma carga útil). Enquanto o aplicativo estiver em execução, basta acioná-lo manualmente com um único clique.
Ao solucionar problemas de canais de eventos, selecione o ativo ScriptableObject. Teste o evento manualmente conforme necessário. O Inspetor pode orientá-lo sobre os objetos que podem estar escutando. Selecione os ouvintes que você deseja inspecionar com mais detalhes.
Se você rotulou os canais de eventos com HeaderAttritute, poderá rastrear vários eventos para entender o fluxo da lógica.

Mais recursos do ScriptableObject
Esperamos que os canais de eventos e a arquitetura orientada a eventos possam beneficiar seus projetos novos e futuros.
Leia mais sobre padrões de design com ScriptableObjects em nosso e-book técnico, Crie arquitetura de jogo modular no Unity com ScriptableObjects. Você também pode descobrir mais sobre os padrões comuns de design de desenvolvimento do Unity em Melhore seu código com padrões de programação de jogos.