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

arrow_back Aula 05 - Máquina de Estados Finitos

3. MEF usando blocos de comando

3.1 MEF usando o padrão de projeto State

Na aula passada, você viu rapidamente o que são padrões de projeto. Inclusive, o padrão de projeto Singleton, não foi? Pois bem, nesta seção, será apresentado um outro padrão de projeto, chamado State. Como o nome diz, tem tudo a ver com estados.

Um padrão de projeto é uma solução generalizada para um problema recorrente. No caso do Singleton, o problema recorrente é quando se precisa que uma classe tenha uma única instância e que ela seja facilmente acessível pelos demais objetos da aplicação. Quando você se deparar com essa necessidade em algum momento da programação, não precisa mais perder tempo tentando achar uma solução adequada. Use o padrão Singleton e está resolvido.

Da mesma forma, sempre que for necessário que um objeto altere seu comportamento quando alterações internas ocorrerem (percebeu uma relação com o que está sendo estudado nessa aula?), você não precisa mais perder tempo procurando uma solução. Basta usar o padrão State para esse problema recorrente. Simples assim! O que você precisa, agora, é saber como esse padrão funciona e como ele é implementado.

Primeiramente, o padrão State separa o comportamento do objeto. Nesse caso, é como se o comportamento não fizesse parte do personagem. Ele apenas o utiliza, ou seja, o personagem “possui” um comportamento. Quando o estado do personagem é alterado, o que ocorre é que ele deixa de “possuir” o comportamento atual e passa a “possuir” um outro. Faz sentido, não é mesmo?

A implementação dessa estratégia, na qual personagem e comportamento encontram-se separados, passa pelo uso de mecanismo de composição de classes (associação). Com essa separação, é possível reutilizar o mesmo comportamento em diferentes personagens. Porém, como no Unity se trabalha com C#, que é uma linguagem de tipagem estática, para que o atributo de associação do personagem ao comportamento possa assumir diferentes estados, eles precisam ser do mesmo tipo. Por essa razão, em C#, o padrão State usa também os mecanismos herança de classes e polimorfismo.

Diagrama de classe ilustrando a estrutura do padrão de projeto <span class='italico'>State</span>

Observe a Figura 03, ela apresenta um diagrama de classe de um padrão de projeto State genérico com três possíveis estados. Character é a classe representando o personagem (ou seu comportamento). Essa classe possui o atributo state da classe State, e métodos de atualização Update() e de mudança de estado ChangeState(). A classe State, por sua vez, possui declarações de métodos para implementar as ações a cada atualização do personagem (Update()), bem como para quando o personagem entrar (Enter()) e sair do estado (Exit()). Porém, State é uma classe abstrata, ou seja, não há objetos dela propriamente ditos. Ela deve, entretanto, possuir subclasses que não são abstratas (State_A, State_B e State_C). Estas, sim, podem possuir objetos. Cada uma dessas subclasses representa um possível estado e, portanto, implementa suas próprias ações de atualização, entrada e saída do estado.

Como as classes mencionadas são subclasses de State, seus objetos são também do tipo State e podem, portanto, ser associados ao atributo state do personagem. Assim, o que o método Update() de Character faz é chamar o método Update() do estado em que o personagem se encontra. Quando o atributo state for alterado, o Update() chamado será outro e, portanto, o comportamento também será outro. Por fim, o que o método ChangeState() de Character faz é chamar os métodos Exit() e Enter() do antigo e do novo estado, respectivamente. Veja o código 04 a seguir.

O Código 04 mostra como a atualização e a mudança de estado são implementadas. Essa parte do código da classe Character não muda, independentemente do tipo de estado, pois o que muda é o objeto associado ao atributo state. Se o objeto for da classe State_A, o método Update() chamado será dessa classe. Se o objeto for da classe State_B, o Update() será dela, e assim por diante.

Talvez seja difícil entender todo o mecanismo apenas descrevendo-o, não é mesmo? Então, aplique o padrão no exemplo do cobrador de pênalti para que você possa compreender melhor a ideia.

Da mesma forma que na implementação da MEF usando blocos de comandos, nessa implementação você terá também dois possíveis estados. O comportamento do jogador será composto, portanto, de uma classe abstrata para generalizar todos seus possíveis estados e duas subclasses para especificar cada possível estado. Denominadas de SoccerState a classe abstrata, e SeekState e IdleState as subclasses para ir em direção à bola e não fazer nada, respectivamente.

Você deve estar se perguntando por que irá implementar a SoccerState como uma classe abstrata e não uma interface. O fato é que, da mesma forma que o personagem possui uma referência para seu estado (através do atributo state), o estado também precisa ter uma referência para o personagem, de forma que ele possa consultar seus atributos e executar seus métodos.

Há duas opções para que o estado tenha a referência do personagem; ou o estado a recebe a cada chamada de seus métodos Update(), Enter() e Exit(), ou você a armazena em um atributo. Escolha a última opção e vá, portanto, guardar a referência para o personagem em um atributo. Você deve saber que interfaces possuem apenas definições de métodos para que as classes possam implementar. Sendo assim, o estado não pode ser uma interface, mas uma classe abstrata. Os métodos precisam ser, entretanto, virtuais para que o polimorfismo possa funcionar. O Código 05 apresenta a classe implementada.

Note que os métodos Update(), Enter() e Exit() são virtuais. Isso permite que você tenha uma implementação default para cada método sem precisar que as subclasses sejam obrigadas a implementá-los. No caso, sua implementação default não faz nada (o bloco de comandos está vazio).

A classe SeekState deve derivar da SoccerState e implementar os métodos necessários para detalhar seu comportamento nesse estado, ou seja, durante a atualização Update() e na saída do estado Exit(). Durante a atualização, ele atualiza o alvo do jogador e o envia em direção à bola através do comportamento de navegação Seek. Na saída do estado, ele manda o jogador chutar a bola.

O Código 06 apresenta o código resultante. Lembre-se de que onwer é um atributo herdado da classe SoccerState e possui a referência para o jogador.

O estado zen-budista de “não fazer nada” (IdleState) é implementado de forma similar ao SeekState. Porém, no seu método de atualização, ele deve ir parando as ações anteriores. Como o jogador estava correndo no estado anterior devido ao comportamento de navegação Seek, ele não pode simplesmente parar de um momento para o outro, pois há uma inércia envolvida. A atualização deve, portanto, aplicar uma força de navegação para pará-lo. Sua implementação, apresentada no Código 07, usa o comportamento de navegação Stop(), que não foi introduzido na aula de Comportamentos de navegação, mas cujo conceito é bastante simples. O que ele faz é reduzir, a cada atualização, 1/4 da velocidade multiplicando o inverso da velocidade atual por 3/4. Quando a velocidade atingir uma velocidade mínima, você retornará como força o inverso de sua velocidade atual, fazendo com que o jogador pare completamente.

Por fim, falta implementar as alterações necessárias na classe SoccerPlayer. Você precisa inicialmente retirar a enumeração SoccerState, uma vez que agora você tem uma classe com esse nome, e o atributo state será uma instância da classe. Consequentemente, os acessos ao atributo precisam ser alterados, como no método Reset() que, em vez de inicializar o estado do jogador com uma constante da enumeração, inicializa o estado atribuindo-lhe uma instância da classe SeekState. O Código 09 mostra a nova versão da classe SoccerPlayer.

Versão 5.3 - Todos os Direitos reservados