Quando programamos um código utilizando Eventos, costumamos inverter a lógica de execução das ações de objetos. Em diversos momentos esse tipo de inversão é muito útil. Um ponto negativo é que acabamos por pensar num esquema mais complicado, menos intuitivo.
Esse tutorial ensinará como usar eventos. Como pano de fundo, veremos Eventos no contexto da derrota do jogador, um caso comum em que se usa eventos.
Para começar, crie um projeto 3D, crie o código a seguir, e monte um personagem e uma UI (só para mostrar uma mensagem) em cena. Vou mostrar como ficou o meu, para exemplificar.
Para ficar claro, nosso objetivo é usar um evento que exibirá a mensagem "You Lose" quando a vida do jogador atingir zero. Para isso faremos três coisas:
Adicionar uma variável de vida a capsula;
Um objeto para colidir com a capsula e diminuir sua vida;
Um código para a interface que será executado pela cápsula;
Adicionando a vida
Para manter um padrão de código, eu vou criar uma privada de variável de vida e um método para modificar essa vida.
Variáveis de vida geralmente são inteiros, pois é uma forma mais genérica, que funciona bem para exibir em formato de barra, de porcentagem, de pontos, entre mil outras formas.
E para modificar essa vida, eu vou criar um método que recebe como parâmetro o valor de modificação, assim, quando a capsula tomar dano, esse dano é enviado pelo método com valor negativo, porque vai alterar a vida negativamente, e o mesmo ocorrerá para aumentar a vida.
Coloque o código na capsula, e nosso primeiro passo está completo.
Dano na vida
Para gerar algum tipo de dano, criarei um objeto de colisão - apenas uma parede -, com um código que lança um OverlapBox, dentro de uma Coroutine, em busca do PlayerLife, e roda o método de modificação da vida, assim a gente pode ver o valor da vida caindo aos poucos.
Também, basta colocar o código na parede e essa parte já estará finalizada.
Ativando a interface
Por fim, antes de entrarmos no assunto de eventos, precisamos de um breve código para ativar a interface, com a mensagem de derrota. Você verá que, através de eventos, podemos fazer isso de várias formas diferentes, então, para começar simples, escreveremos um código que contém apenas um método que ativa o GameObject que contém o código. Colocarei esse código no Canvas, já que ele detém a interface inteira.
Eventos
Para entender eventos, é necessário dois conceitos base - Referência e Relação "1 para muitos".
Iniciaremos pela Relação "1 para muitos". Pensando no cenário que construímos, quando o jogador é derrotado, algumas coisas podem acontecer: a tela de derrota surge, os inimigos param de atacar o jogador, algum tipo de menu pode surgir por cima da tela do jogo.
Veja, a derrota do jogador pode significar a manifestação ou ativação de uma série de objetos. Essa situação exemplifica perfeitamente a relação "1 para muitos", pois um objeto (o jogador) dispara um evento ou acontecimento, que ocorreu com ele, e propaga esse evento para muitos objetos que irão reagir (quaisquer objetos interessados em tal evento).
Geralmente, quando estamos aprendendo e estudando programação para jogos, é comum pensarmos em esquemas de implementação mais simples. Por exemplo, poderíamos implementar todos esses objetos com uma referência para o jogador, e com essa referência, eles poderiam verificar a vida do jogador constantemente no Update(), mas isso não seria muito bom porque a vida do jogador estaria totalmente exposta, e se eventualmente houvesse alguma mudança nessa vida, essa mudança poderia forçar um ajuste em cada um dos códigos que observam essa vida.
Outra possibilidade seria o jogador acessar diretamente os outros objetos, e de algum modo executar os objetos necessários. Essa alternativa também é ruim, pois o programador teria que criar esse acesso para cada tipo de objeto (uma referência para telas e menus, por exemplo), e qualquer mudança a ser feita, levaria mais tempo e mais complicações para acontecer. Além disso, poderia sobrecarregar o objeto jogador com informações desnecessárias, sendo difícil sua manutenção.
Através de eventos, podemos implementar uma outra lógica de execução das ações que eu tentei explorar acima. O que faríamos é executar todas as ações dos objetos observadores da vida do jogador através de uma referência única para todas essas ações, sem que o jogador precise saber quais objetos são esses. Além disso, preservamos a privacidade dos atributos do jogador e dos objetos observadores.
É nesse sentido que entra o conceito de Referência. Para implementar o evento, basicamente criaremos uma variável especial a partir "tipo" delegate, que serve para armazenar métodos, assim como int guarda números inteiros e string guarda texto.
Variáveis criadas a partir do delegate funcionam como um ponteiro, que guarda endereço de memória de métodos, e pode executar esses métodos quando necessário. Podemos adicionar quantos métodos forem preciso, e quando executamos a variável delegate, executamos todos os presentes armazenados de uma vez só.
Então, partindo do exemplo da derrota do jogador, o código PlayerLife terá uma variável criada pelo delegate, e todos os objetos de cena, que devam reagir a derrota dele, adicionarão seus métodos nessa variável. Quando a vida do jogador chegar a zero, o jogador executará essa variável, e, por definição, executará todos os métodos que foram adicionados a ela.
Criando um delegate
Se você pensar numa variável que armazena e executa métodos, você pode se questionar sobre as infinitas possibilidades de um método, argumentos diferentes e tipos diferentes.
Pois bem, para que um método possa ser armazenado por um delegate, esse deve ter o mesmo formato que o método. Assim, precisamos primeiro indicar o formato do delegate para depois criar a variável dele. O formato consiste na assinatura do método - tipo de retorno e parâmetros.
A seguir, vemos exemplos da criação de delegate's e variáveis criadas por eles.
"Method1", "Method2" e "Method3" são delegates que eu criei, ou seja, formatos de métodos, e eles são usados para criar variáveis que armazenam métodos, no caso as variáveis "variable1" e "variable2".
Usando um delegate
Agora que já sabemos criar uma variável para armazenar os métodos necessários, podemos modificar o código PlayerLife. Como não temos nenhum tipo de troca de informação, e só precisamos rodar as reações a derrota do jogador, esse delegate será void e sem parâmetros. O chamarei de "OnLose".
Rodando o evento
Com o "OnLose" declarado, executamos os métodos vinculados a ele quando a vida do jogador chegar a zero. Podemos fazer isso dentro do ChangeLife(). Lembre-se que ele guarda na verdade referência para métodos, e, portanto, ele pode não possuir valor algum, ou seja, ser nulo, então antes de rodar quaisquer métodos, devemos verificar se ele não é nulo.
Adicionando métodos ao "OnLose"
Até aqui, então, apenas declaramos o "OnLose" e colocamos ele para rodar quando a vida do jogador chegar a zero. O próximo passo é adicionar métodos de outros objeto ao OnLose, no caso, por hora, temos apenas o método da interface.
Veja que, para isso, a interface precisa de ter acesso ao código da capsula. Para facilitar o estudo, criei uma referência estática pública do PlayerLife, mas que fique claro que é apenas para o estudo, numa abordagem profissional existem alternativas bem melhores. E também tornarei as estruturas que criamos públicas, para serem acessadas de fora.
Agora, com a referência estática, podemos acessar o PlayerLife em qualquer código. Assim, no código da interface podemos "inscrever" o método Activate() no OnLose.
Para testar, também é necessário rodar o Start(), assim, iniciaremos a interface ativada, e desativaremos depois da inscrição.
Quando você for testar, encoste a capsula na parede e veja o número de vidas caindo. Quando chegar a zero, o método da interface rodará e ela será ativada.
Aproveite todo o cenário que implementamos, e experimente criar e adicionar novos métodos de outros objetos para rodar junto com a derrota do jogador.
Para fechar
Você deve ter notado que usamos o operador de incremento "+=", a explicação é porque queríamos inscrever ou adicionar o método Activate; se tivéssemos usado o operador de atribuição "=", limparíamos todos os métodos inscritos, deixando apenas o Activate.
Outro ponto é a extrema importância de "desinscrever" métodos que não precisam ser mais rodados. Nesse caso, é comum "desinscrever" os métodos quando o objeto será destruído. Para isso usamos o operador de decremento "-=".