Cursos / Jogos Digitais / Inteligência Artificial para Jogos / Aula

arrow_back Aula 05 - Máquina de Estados Finitos

4. Gerenciando eventos

A Máquina de Estados Finitos que você desenvolveu até agora possui apenas dois estados e uma transição. A transição é realizada no objeto SoccerPlayer, quando o BoxCollider2D dele entra no CircleCollider2D da bola, fazendo-o mudar do estado de ir em direção da bola para o estado de não fazer nada. Na saída do estado de “ir em direção à bola”, a ação “chutar” é realizada. Veja, então, que essa não é uma situação complexa. Os estados e as transições são fáceis de gerenciar. Agora, imagine um cenário mais complexo, com um maior número de estados e de transições. E que nele tenha o documento de Game Design indicando duas situações: o jogador fazendo gol e “saindo pra galera”, e em seguida voltando à posição inicial para cobrar outro pênalti, e ele não fazendo o gol, errando o chute, e voltando à posição inicial, mas sem comemorar.

Imaginou? Vá para a melhor das situações, que é a de o jogador fazer gol. Afinal, você quer ver a comemoração do jogador, não é mesmo? :) O Vídeo 01 ilustra como colocar essa comemoração em prática. Nele, você notará que na execução apresentada, a ação do goleiro será retirada de forma que todos os chutes realizados pelo batedor sejam gol.

play_arrow Vídeo 02 - Exemplo de cobrança de pênalti com comemoração do jogador

O esquema da MEF da Figura 04 apresenta uma possível máquina de estados para o cenário proposto. Como você vai gerenciar todos esses eventos? Onde colocar os testes para chamar as transições? Veja a seguir.

Diagrama de estados mais completo do cobrador de pênaltis

Como o diagrama da Figura 02 representa o comportamento do jogador cobrador de pênalti, os estados apresentados são próprios do jogador. Porém, se você prestar atenção nos eventos que geram as transições, há tanto eventos internos, próprios do jogador, quanto eventos externos, que dependem de outros elementos do jogo. Por exemplo, “término da comemoração” é algo interno. O próprio estado pode gerenciar quando o jogador terminará sua comemoração. Mas saber se a bola que ele chutou foi gol ou não depende de outros objetos. Você vai distinguir esses dois tipos de eventos porque eles possuem mecanismos diferentes.

Os eventos internos são mais fáceis de gerenciar. Como eles dependem apenas do próprio estado, as transições podem ser realizadas nos métodos de atualização do estado. Por exemplo, o estado de comemoração pode ser implementado como no Código 10. A comemoração implementada faz o jogador ir em uma direção aleatória durante meio segundo, depois muda para outra direção aleatória durante meio segundo e assim por diante, realizando esse processo seis vezes (count > 5).

A classe CelebrateState possui três atributos:

  • Time: gerencia o tempo do jogador quando ele estiver indo em uma direção;
  • Count: gerencia o número de vezes que o jogador mudará de direção;
  • Target: define a direção em que o jogador está indo.

A variável count será incrementada sempre que o tempo de meio segundo for ultrapassado, e quando seu valor for maior que cinco, você fará a transição para o próximo estado, BackToInitialPosState.

Se o estado de comemoração só depende dele, por outro lado, as transições do estado de aguardar o resultado do chute dependem, exclusivamente, de outros objetos. Como você irá, então, implementar isso?

Suponha que você coloque testes na atualização do estado “aguardar o resultado do chute”, verificando se a bola chutada foi gol ou não. A princípio, não há mal algum em fazer isso. Pode-se averiguar, a cada atualização do estado, se o círculo envoltório da bola se sobrepõe à caixa envoltória do gol. Se isso ocorrer em algum momento, é porque foi gol. E se o chute não resultar em gol? Você precisará fazer algo similar, testando a cada atualização se o círculo envoltório da bola se sobrepõe ao círculo envoltório do goleiro. E se isso acontecer em algum momento, é porque o goleiro segurou a bola.

Bom… mas imagine isso com os 22 jogadores em campo, todos ansiosos para saber se o chute resultou em gol ou não.Se você implementar dessa forma, terá as mesmas verificações 22 vezes, consumindo um tempo desnecessário do processador. Você precisará, então, inverter o processo de verificação. Em vez de cada um testar se a bola se sobrepõe a outros elementos, você fará com que apenas a bola execute esse teste. Porém, os demais objetos que estejam interessados em saber do gol precisam se “cadastrar” para serem informados sobre o evento. Caso a bola constate que um gol ocorreu, ela passará essa informação a todos os objetos previamente cadastrados.

Esse mecanismo descrito é outro padrão de projeto chamado de Observer (você já viu dois nessa disciplina: Singleton e State) e é amplamente utilizado para implementar sistemas baseados em eventos. Por exemplo, têm-se duas aplicações de sistemas baseados em eventos, uma com um sistema de interface que possui menus e botões e uma que executa rotinas quando um pacote de dados chega da rede. Enfim, a maioria das aplicações reais são baseadas em eventos. Por isso, conhecer esse padrão é muito importante.

O padrão Observer pode ser implementado de duas maneiras. Em ambas, há os conceitos de “observável” e “observador”. No caso de uma aplicação com botões à espera de serem clicados pelo usuário, os botões são os elementos observáveis, e os observadores são os objetos ou sub-rotinas que se registram para serem chamados quando o evento do clique do botão ocorrer. Normalmente, registra-se observadores passando uma sub-rotina (função ou método) para o objeto observável. Pode-se ter, inclusive, várias sub-rotinas registradas para o mesmo evento no objeto observável. Quando o evento ocorrer, todas as sub-rotinas registradas serão chamadas.

Esquema ilustrando o registro de dois observadores no padrão Observer

As sub-rotinas registradas no observável são chamadas de callbacks porque você passa suas referências a fim de serem “chamadas de volta”, quando o evento ocorrer. Porém, nem toda linguagem de programação dá suporte à passagem de sub-rotinas. Nesse caso, você passa um objeto que possua um método com uma assinatura específica (pois o método será chamado quando o evento ocorrer). Mas, como garantir que o objeto a ser registrado tenha obrigatoriamente um método com assinatura específica? Fazendo com que o objeto implemente uma interface predefinida.

Felizmente, C# dá suporte à passagem de sub-rotinas, especificando a assinatura através da palavra-chave delegate. Só que melhor ainda, C# dá suporte ao registro das sub-rotinas e à chamada das sub-rotinas registradas através de um tipo de elemento chamado event (tudo a ver ). Você já utilizou o delegate e o event anteriormente, mas apenas agora todos os seus detalhes estão sendo explicados. Enquanto o delegate permite especificar a assinatura da sub-rotina que será chamada quando um evento ocorrer, ou seja, quais são seus parâmetros e seu tipo de retorno, o event permite registrar sub-rotinas (ou remover seu registro) que atendem a assinatura especificada pelo delegate e chamar todas as sub-rotinas quando for necessário.

Você pode ver o funcionamento dos mecanismos através dos códigos a seguir. Na implementação do script da bola (Código 11), foi definido um delegate chamado GoalEvent que especifica a assinatura de uma sub-rotina que não recebe nenhum parâmetro e não retorna nada (void). Em seguida, determinados dois tipos de eventos; um para ser disparado quando ocorrer o gol (OnGoal) e outro para quando o chute falhar, ou seja, quando não for gol (OnShotFailed). Quando a bola colidir com o gol (que possui a tag Goal), o evento OnGoal será disparado, chamando todas as sub-rotinas que foram registradas no evento. De maneira similar, quando a bola colidir com o goleiro (que possui a tag GoalKeeper), o evento OnShotFailed será disparado. Esses eventos foram definidos como públicos porque serão usados por outros objetos para se registrarem.

Na implementação do estado de espera pelo resultado do chute (Código 12), o construtor armazena a referência para o script da bola para que você possa registrar sub-rotinas do estado para quando os eventos OnGoal e OnShotFailed ocorrerem. Isso é realizado no momento em que o jogador entra no estado (o método Enter() é chamado). O registro é feito através do operador +=, passando os métodos OnGoal() e OnShotFailed() do estado, e que, por sinal, atendem a assinatura do delegate especificada nos eventos (ou seja, não recebe parâmetro e não retorna nada). Na saída do estado (quando método Exit() é chamado), os métodos OnGoal() e OnShotFailed() são removidos dos registros do evento. É importante removê-los porque, apesar de o objeto do estado não ser mais utilizado, ele não poderá ser liberado pelo gerenciador de memória da linguagem (vulgo coletor de lixo ou garbage collector), por ainda ter referências para ele.

Quando a linguagem de programação utilizada não possibilita passar uma sub-rotina, pode-se registrar um objeto de uma classe que implementa uma interface específica.

Bem, até agora você conheceu o básico de uma MEF, mas há inúmeras variações que podem ser exploradas... e que serão exercitadas nas atividades. Uma versão bastante utilizada é a MEF Probabilística.

Uau! Que nome bonito, não é? Máquina de Estados Finitos Probabilística!

Ela consiste apenas em fazer com que um mesmo evento durante um estado possa gerar um ou mais estados. Então, quando o personagem se encontra em um estado que possui transições para dois ou mais estados a partir do mesmo evento, cada transição poderá ter sua probabilidade associada, como ilustra a Figura 06.

Exemplo de Máquina de Estados Finitos Probabilística

Versão 5.3 - Todos os Direitos reservados