Inteligência computacional para Jogos com GamePlayKit em Swift

Parte 1: A máquina de estados por trás do pacman

Renata Faria
CocoaHeads Recife
6 min readJul 26, 2020

--

Photo by Daniel Cheung on Unsplash

Olá a todos!

Se você leu esse título e pensou que seria sobre CoreML, está enganado. Hoje vamos nos aventurar em conceitos muito utilizados no mercado de jogos. Esses conceitos não são exclusivos de Swift, eles são utilizados em vários lugares, inclusive em engines como Unity e Unreal. Por isso é interessante entender os conceitos para então mergulhar na prática. Ao fim você vai ver como eles vão fazer muito mais sentido do que usar uma IA tão sofisticada lançadas pela Apple nos últimos anos.

Sendo assim, falarei sobre dois conceitos: máquina de estados (State Machine) e árvore de decisão/comportamento (decision or behaviour tree). Explicarei a teoria e depois como aplicar elas em Swift.

Parte 1 — Máquina de estados: Esse post

Parte 2 — Árvore de decisão: Em breve ~

Óbvio, existe muito mais que isso, então vou deixar links para vocês consultarem depois ❤️

Máquina de Estados (Finite State Machine)

De onde tudo surgiu?

Na verdade isso vale para grande parte das teorias computacionais em jogos: Apesar de ser afirmação meio óbvia, tudo veio da computação! Vários problemas que existiam em jogos já aconteceram antes, em outros cenários. Com a máquina de estados não foi diferente, esse conceito dá pra ser utilizando em múltiplos campos da computação, de hardware até software. Inclusive já vi até quem use o framework de jogos que vou apresentar hoje para coordenar requisições web, que louco né?

O princípio é simples: você precisa de no mínimo dois estados, estes se alternam dependendo de cada situação, ou condição. Um exemplo fácil de se ver isso é uma lâmpada, que possui dois estados, um aceso e um apagado. Mais tarde voltaremos a esse conceito….

Mas e em jogos?

No inicio, os jogos eram pequenos. Um simples if ou else funcionava, talvez um switch para casos mais complexos. Com jogos se tornando cada vez maiores e complicados, tornou-se insustentável não utilizar técnicas. E quem começou com tudo isso? Eu não sei bem ao certo, mas um dos pioneiros foi, sem dúvidas, o pac-man:

Se você está tentando encontrar quando o famoso personagem amarelo troca de estados, você está procurando no lugar errado. Geralmente aplicamos inteligência computacional em coisas que não estão sobre nosso domínio, seja npcs, inimigos ou até seções do jogo. Logo, tudo nos leva aos fantasminhas! Sim, é lá que a máquina de estados está. Se parar pra observar no video acima, vai perceber os estados que eles possuem, mesmo que implementados de formas diferentes: caçando, fugindo e dispersando. Esses estados são trocados dependendo de certas condições no jogo. Por exemplo, o estado de fugindo é acionado após o pacman coletar a bolinha de energia.

Na prática…

Vamos imaginar que estamos em 1979 e fomos contratados pra desenvolver Pacman, porém em Swift (e já existia swift em 1979? 👀). Antes de iniciarmos o desenvolvimento vamos planejar nosso software. Um estado da máquina de estados é normalmente representada por circulos. Sendo assim, vamos desenhar eles:

No inicio do jogo os fantasmas estão se dispersando, ou seja, cada um vai de forma aleatória para os cantos da tela. Esse modo dura pouco, até que é trocado pelo caçar, onde os fantasmas são programados, cada um do seu jeito, a seguir o jogador. O tempo de caçar tem um timer, quando este acaba, o modo de dispersar é ativado. E assim fica nesse zig-zag entre os estados de Dispersar e Caçar. Até que então o Pacman alcança uma das bolas de energia que o dá super poderes, fazendo dos fantasmas seres medrosos: mudam de comportamento (ou estado) para fugindo. Pra facilitar esse post, o fantasma só irá fugir se estiver Caçando. Se ele estiver disperçando, o efeito não irá valer.

Entendemos os motivos das trocas, portanto vamos adicionar essas relações, ou melhor, condições para mudança de estados no nosso esquema:

Pronto! Agora que está planejado, podemos implementar em Swift. Acredite, a Apple deixou o código disso muito simples com o GamePlayKit, introduzido em 2015. Vamos usar ele. Iniciamos criando GKStates, que nada mais são do que nossos estados, que estão dentro da bolinha. Olhando na documentação da Apple, temos alguns métodos que podem ser muito úteis para trocarmos para o próximo estado. Mas antes de mergulharmos nisso, vamos criar nossos estados, que são classes que herdam do GameplayKitState:

Esses estados agora existem no nosso jogo, porém não estão inseridas em nenhuma máquina de estados. Por esse motivos, vamos instanciar uma nova e inseri-los, que nem no código abaixo:

Até agora fizemos a primeira parte do nosso esquema: a máquina + nossos estados. O que falta? Exato! A troca de estados. Para elas, utilizamos um método chamado enter(_ stateClass: AnyClass) -> Bool, onde stateClasse é a classe que iremos entrar. Porém, sabemos que no inicio do jogo os fantasmas estão se dispersando e não faz sentido já conseguirmos mudar para o estado de fugindo, por exemplo. Por isso, ao usar esse método, o framework irá nos devolver um booleano, que nos informa se a troca de estados foi feita ou não.

No entanto, com o que temos até agora, conseguiremos entrar em todos os estados, independente do estado atual. Precisamos ajeitar isso sobreescrevendo um método da classe GKState, chamado isValidNextState. Aqui iremos informar que os estados Dispersar e o Fugindo só podem mudar para o Caçando. Já o caçando pode entrar em qualquer um do dois!

O código fica assim:

E agora, que temos tudo, é simples. Basta jogarmos as condições e trocar os estados da nossa máquina. Antes disso, vou adicionar algumas coisas para conseguirmos testar nossa lógica. A primeira delas é o mais importante: a rodada, que será controlada por um timer chamado timerRodada. A cada 1 segundo, o jogo avança uma rodada. Depois vamos adicionar nossas condições principais: um timer que vai ficar trocando se o fantasma caça ou dispersa, um booleano para dar superpoderes para nosso Pacman e por fim, um timer para controlar o tempo de poder dele.. afinal, se o jogador puder ficar invencível o jogo não terá graça!

O timer da rodada vai chamar uma função chamada novaRodada que irá sortear um número para dar (ou não) poderes para o pacman. Depois irá verificar se o pacman tem superpoderes e então alterar o estado dele. Além disso, o timerTurno irá disparar a função trocarTurno que também irá alterar o estado do fantasma. Quando pacman ganha poderes, iniciamos o timer timerSuperPoder. que chama a função de desativar poderes.

Aí vai o código:

Pronto, agora com o jogo funcionando, podemos adicionar a função enter(_ stateClass: AnyClass) -> Bool citada anteriormente nos locais necessários:

Pronto, terminamos! Mas… como vamos saber que está funcionando? Existe uma função dentro do GKState chamada didEnter(from previousState: GKState?) essa função é chamada toda vez que você entrar em um novo estado. Podemos colocar la um print nos avisando que o fantasma mudou de estados:

Agora sim, podemos ver no terminal como está se comportando 😁

Terminamos a máquina de estados!

Quer conferir como ficou no seu Mac? Baixa o playground aqui!

Em breve postarei sobre a árvore de comportamentos, é tão simples quanto a máquina de estados, porém nos da maior poder de decisão e o jogo fica incrível 😉

Quer saber mais sobre Máquina de Estados? Confere esse video super bacana feito pelo Victor Vasconcelos:

--

--

Renata Faria
CocoaHeads Recife

Gerente de Produtos no AllowMe. Estudante de mestrado na Universidade Federal de Pernambuco.