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

arrow_back Aula 05 - Máquina de Estados Finitos

3. MEF usando blocos de comando

A essência de um comportamento baseado em estado é fazer com que, a cada atualização do personagem, as ações que ele executa sejam referentes ao que ele se encontra. Ou seja, a ação que está sendo executada é baseada no estado atual. Se o personagem altera seu estado, no laço de atualização do jogo, uma ação diferente passa a ser executada. Além disso, muitas vezes é importante você ter ações associadas ao momento em que o personagem entra em um estado e ações associadas ao momento que ele sai. Por exemplo, na cobrança de pênalti, você poderia ter os seguintes estados: “Correndo para chutar” e “Não fazer nada”. Durante o “Correndo para chutar”, a ação do jogador seria apenas a de ir na direção da bola. O chute seria a ação executada quando o jogador saísse desse estado, indo para o de “Não fazer nada”.

A Figura 02 ilustra essa descrição, mostrando que a ação “chute” é realizada no momento da transição de um estado para o outro.

Diagrama de estados do cobrador de pênaltis

Uma estratégia para implementar essa ideia é reservar um atributo do personagem para armazenar o estado em que ele se encontra e, a cada atualização dele, executar um bloco de comandos diferente para cada respectivo estado. Se o personagem tiver apenas dois possíveis estados, o atributo pode ser uma variável booleana, tal como feito na aula anterior. Porém, se houver mais de dois possíveis estados para o personagem, você precisará então de um atributo que possa assumir mais valores.

Um mecanismo simples, mas bastante utilizado em jogos, é usar uma enumeração para delimitar os possíveis estados que o atributo pode assumir. Durante a atualização do personagem, um bloco de comandos é associado a cada possível estado, ou através de um switch ou de um if…else. O Código 01 mostra como essa abordagem pode ser implementada usando três possíveis estados.

O que faltou apresentar no código 01 é em que momento o método ChangeState() é chamado, ou seja, quando os eventos geram as transições de estado. Há basicamente duas possibilidades: ou se registra um evento, por exemplo um associado ao collider do personagem ou relacionado a uma mensagem que ele possa receber de outro objeto, ou se testa se o evento ocorreu durante a atualização do personagem (FixedUpdate()).

Serão apresentados mais detalhes sobre o gerenciamento de eventos na última seção dessa aula. De qualquer forma, você pode ver aqui como a captura do evento pode ocorrer. O Código 02 ilustra duas formas de captura de evento. No caso, quando o personagem colidir com algum objeto que possua um collider que é um gatilho (trigger), esse evento será capturado e o método OnTriggerEnter2D() é chamado. Nesse momento, se o personagem estiver no estado A, ele é alterado para o B. Por sua vez, durante a atualização do personagem no estado B, se uma determinada condição for verdadeira, ele passa para o A.

Para ficar mais claro, aplique o código genérico descrito anteriormente no contexto do jogo. Para começar, os estados que você tem são o de correr atrás da bola, que se chama de SeekBallState e o de não fazer nada (pois o jogador já chutou a bola e não precisa mais sair correndo atrás dela), que se chama de IdleState.

A classe IdleState implementa um estado que “não faz nada”. Parece estranho implementar algo para não fazer nada, não é? Mas os desenvolvedores de jogos aprenderam com os mestres zen-budistas que não fazer nada, às vezes, é essencial.

Muitos jogos possuem esse estado para seus personagens. Por exemplo, imagine um jogo de plataforma em que os personagens estão dispostos em várias partes do cenário. Porém, eles só vão atuar quando o jogador estiver à sua vista. Enquanto ele não se aproximar do personagem, em que estado ele vai estar? Em meditação profunda, como os mestres ensinaram. Da mesma forma, você vai colocar o jogador em estado de idle após sair do SeekState.

No vídeo 01 abaixo, você encontrará uma situação similar à que foi descrita.

Vídeo 01 - Cena do jogo Dark Souls
Fonte: VITO VENUE. Dark souls defeating a black knight. 2011. Disponível em: https://www.youtube.com/watch?v=5VnK6SY3ZTU. Acesso em: 04 abr. 2018.

Por enquanto, não se preocupe, pois em breve, você incrementará ainda mais o comportamento do jogador, definindo outros estados, por exemplo, um será para o jogador “sair pra galera” depois de fazer o gol. No momento, dois já são suficientes.

Para os dois estados (SeekBallState e IdleState), é necessário adaptar o método de atualização e o de captura de colisões existentes, além de criar o ChangeState(). Para organizar melhor o novo código, passe o código de “chute da bola” implementado na aula anterior para um método próprio chamado ShotBall(), encontrado no Código 03 abaixo.

Porém, imagine como seria o seu código se você adotasse esse esquema em um personagem que precisasse ter 20 possíveis estados. Caso seu código necessite de um ajuste, como resolver um bug, por exemplo, por favor, não peça para um amigo dar manutenção! Provavelmente, você terá um "código spaghetti" tão difícil de dar manutenção, que tal demanda pode ser capaz de abalar laços de amizades. Em vez de perder um amigo, é melhor procurar uma solução alternativa.

Há também um outro problema associado à solução apresentada. Imagine que você tenha dois comportamentos que são diferentes, mas que possuem muitos estados em comum. Da forma em que está, as partes do código referentes aos estados comuns precisam ser duplicadas em cada comportamento. Isso dificulta ainda mais a manutenção porque uma simples alteração precisa ser replicada em vários arquivos. Novamente, mais um motivo para não incomodar seu amigo!

Enfim, a implementação apresentada é uma solução adequada quando se sabe que o número de estados é pequeno e que os estados não são reusados em diferentes comportamentos. Um jogo no estilo PacMan estaria de bom tamanho para usar essa estratégia nos seus fantasmas. Porém, se você adotar essa estratégia em um jogo com mais estados, prepare-se para ter dor de cabeça. Nesse caso, é melhor usar uma solução que será apresentada na seção seguinte.

Versão 5.3 - Todos os Direitos reservados