Concrete Logo
Hamburger button

Como desenvolver seu próprio jogo? – Parte 5

  • Blog
  • 23 de Abril de 2014
Share

Movimentação dos jogadores e pontuação

Esperamos que estejam gostando da série até aqui. Se você chegou agora, pode ver a primeira parte neste link, a segunda parte aqui, a terceira aqui e a quarta neste link. Agora, estamos começando a chegar ao fim e com isso teremos uma boa base sobre desenvolvimento de jogos. Com o avanço da série, podemos começar a falar sobre assuntos e técnicas mais interessantes, que assumem uma carga de conhecimento para serem mais facilmente explicadas. Portanto, fiquem conosco, e vamos completar mais essa etapa.

Acertando os “pulos”

Conforme falamos no post anterior, em alguns momentos notamos que nossos jogadores simplesmente “pulam” enquanto estamos controlando eles. É mais difícil de notar, mas a bola também pula neste exato momento. Só que como ela se movimenta independente, é mais difícil de reparar. Nossa atenção foca no objeto que nós controlamos e que não se comporta como esperamos.

Vamos, então, corrigir o problema substituindo o código do método main da classe Main.java pelo seguinte código:

Agora, ao executar o jogo, veremos que não temos mais os pulos. Então, como acertamos o problema sem ter alterado nenhuma linha de código referente aos movimentos dos objetos em si? Na verdade, a alteração que fizemos está indiretamente relacionada com todos os movimentos que temos no jogo.

Conforme falamos nos artigos anteriores, todos os movimentos (seja da bola ou dos jogadores) são influenciados pelo tpf, que é o tempo necessário para renderizar um frame. O problema dos pulos se devia ao framerate, que ficava tão alto que o valor do float informado como tpf ficava pequeno demais. Com um float muito pequeno, surge o problema de arredondamento de valores. O que acaba acontecendo é que para framerates muito altos, o valor do tpf era arredondado para cima, para que ele não fosse exatamente zero. E esse arredondamento aliado a framerates muito altos causava os pulos que vimos.

Limitando o movimento dos jogadores

Agora, chegamos a mais um ponto problemático do jogo: os jogadores podem sair da área muito facilmente. Para resolver esse problema, vamos limitar o deslocamento vertical deles fazendo as seguintes alterações, todas elas na classe Main:

Primeiro, movemos a declaração do frustumSize, que hoje é declarado dentro do método initCam. Vamos transformá-lo em variável de instância, movendo a declaração para a linha logo anterior ao método main. Depois, criamos um novo método com o seguinte código:

Por fim, chamamos esse código dentro do método onAnalog do analogListener. Temos dois blocos de if: cada um controla os movimentos para um jogador. Após esses blocos, devemos checar os limites para o jogador que foi movimentado, ficando o código do onAnalog assim:

Essas mudanças já são suficientes para limitarmos a movimentação dos jogadores dentro da área da câmera. Este é um jeito pouco convencional e nada genérico de fazer esse controle, mas serve aos propósitos do nosso exemplo. Existem formas genéricas de se fazer o controle para qualquer objeto, porém isso envolveria uma série de cálculos e uma série de APIs que não vimos. Então, às vezes vale resistir à tentação e fazer do jeito simples e objetivo.

Marcando pontuação

Para marcarmos a pontuação, vamos refatorar o código atual que temos e reorganizar um pouco as coisas. E aproveitar para apresentar uma nova estrutura de controle de estado que o jMonkey disponibiliza.

Primeiro, vamos seguir os passos que fizemos para criar novos controles, mas dessa vez,vamos criar um Application State:

1) Criamos um novo pacote chamado “mygame.estado” (conforme fizemos anteriormente na Parte 3)

2) Clicamos com o botão direito no pacote criado e selecionamos “New/Other…”

3) Na lista de categorias, selecionamos JME3 Classes, e no tipo de arquivo, New AppState:

jmonkey_parte5_1

4) Clicamos em Next

5) Damos o nome de EstadoPontuacao

6) Clicamos em Finish

Conforme o nome sugere, este estado será responsável por contabilizar a pontuação no jogo. Não só iremos contabilizar os valores por meio deste estado, como também vamos deixar sob responsabilidade dele a criação das zonas de pontuação. Uma característica importante de programar a aplicação utilizando estados independentes é que podemos filtrar o que os estados enxergam da nossa aplicação, passando apenas o necessário para que o trabalho dele seja realizado. Vamos botar para funcionar, depois explicamos os detalhes.

Vamos migrar tudo o que é referente aos objetos: controle, pontuacaoEsquerda e pontuacaoDireita para o novo estado que criamos. As variáveis de instância são migradas como variáveis de instância e as instruções que estavam no simpleInitApp são migradas para dentro do método initialize. Existem algumas informações não disponíveis no EstadoPontuacao, em particular no nosso exemplo, rootNode. Porém, no método initialize recebemos a referência para a aplicação que teve o nosso estado anexado e então usamos essa referência para puxar da aplicação os dados necessários. Para variáveis de instância do novo estado, temos:

e dentro do método initialize ficamos com o seguinte código:

super.initialize(stateManager, app);

Com esse código no lugar, precisamos apenas anexar o estado ao nosso aplicativo para que ele seja utilizado. No final do método simpleInitApp da calsse Main adicionamos o código:

Feitas essas alterações, temos o comportamento exatamente igual ao que tínhamos anteriormente. Note apenas que, no código anterior, a referência à bola não foi passada diretamente do código da classe Main, mas pegamos o rootNode e buscamos o objeto pelo nome que queremos. Se temos a mesma funcionalidade que tínhamos anteriormente, qual a vantagem de termos feito essas alterações?

Uma vantagem óbvia que temos agora é que podemos declarar novas variáveis, novos métodos e novos objetos referentes à pontuação sem poluir a classe principal da nossa aplicação. Mais adiante discutiremos mais algumas vantagens e como podemos usufruir da possibilidade de anexar e remover essas classes dinamicamente.

Vamos, então, preparar a nossa classe para termos a exibição de pontuação na tela para nossos jogadores. Ao final do método initialize da classe EstadoPontuacao, vamos adicionar as linhas de código abaixo:

E criamos o método preparaHUD, com o seguinte código:

Para que o código funcione corretamente, precisamos declarar duas novas variáveis de instância:

Por fim, no método simpleInitApp da classe Main, adicionamos a linha:

Executando o código anterior, teremos algo semelhante à imagem abaixo:

jmonkey_parte5_2

A primeira coisa a ser notada é que excluímos aquela quantidade enorme de informações da tela. Isso foi feito com a última alteração que fizemos no método simpleInitApp. Existe um método semelhante para excluir a exibição do FPS também.

O jogo está exatamente como antes, mas agora temos uma área de pontuação exibida para cada jogador. Um detalhe importante é que se jogarmos agora, mesmo fazendo pontos, eles ainda não serão marcados. Vamos chegar nesse ponto mais adiante. Toda a mudança que fizemos se concentra no método preparaHUD da classe EstadoPontuacao.

O código ali presente é extremamente simples. Criamos um BitmapText, um Node com algumas propriedades direcionadas para renderização de texto na tela. Definimos o tamanho que queremos para o texto que vamos exibir, definimos a sua cor, o valor inicial e, por fim, a posição na tela. No código que apresentamos, a pontuação da esquerda está em 1/4 da tela, a partir do canto esquerdo, e a 10% de distância do topo. A pontuação da direita está em 1/4 da tela a partir do canto direito.

Note que nós utilizamos o guiNode da aplicação para anexar as pontuações. O guiNode é configurado para exibição de uma tela 2D em um nível de visualização acima do rootNode. Nesse modo de visualização 2D, as coordenadas dos componentes e a posição dos elementos são dependentes do tamanho da tela, em pixels, conforme a ilustração abaixo demonstra:

jmonkey_parte5_3

As coordenadas começam no canto inferior esquerdo e vão aumentando para cima e para direita até que, no canto superior direito, elas possuem o valor da resolução da tela (ou janela) atual. Com essas informações, dá para ter uma ideia de como renderizar alguns elementos básicos na interface do usuário. Uma particularidade dessa forma que utilizamos é que ela é boa para fazer coisas simples, como colocar textos ou desenhar imagens . Para termos uma interface mais complexa, podemos ir criando os objetos e texturas conforme necessário, ou utilizar o Nifty GUI integrado ao jMonkey.

Agora que temos a infraestrutura para exibir a pontuação dos nossos jogadores, vamos colocar o código para efetivamente marcar essa pontuação.

As primeiras alterações são na classe ControlePontuacao. Vamos adicionar uma nova variável de instância para o nosso controle:

Dentro do método controlUpdate, fazemos a marcação de que houve a colisão dentro do if existente, deixando o bloco de código conforme abaixo:

Com isso, cada vez que houver uma colisão com um dos nossos controles de pontuação, ele ficará marcado. Vamos mudar, então, o nosso estado que controla a pontuação para usar essa informação. Declaramos duas variáveis de instância para mapear a pontuação de cada um dos nossos jogadores:

E atualizamos o método update, para utilizar essas variáveis e garantir a renderização delas na interface do usuário:

Se executarmos agora o nosso jogo, vamos reparar que cada vez que a bola alcança a área de marcação a pontuação é atualizada e a bola é resetada. Vale ressaltar que existem várias formas de atingirmos o mesmo resultado, porém, utilizamos ao máximo a infraestrutura que o jMonkey nos oferece, sem termos sacrificado nem complicado as nossas funcionalidades.

Antes de continuarmos porém, vale implementarmos mais um método do EstadoPontuacao, deixando o método cleanup com o seguinte código:

Esse é o método que desfaz o que fizemos quando inicializamos o nosso estado. Ele é chamado quando removemos o nosso estado da aplicação. Isso será importante para o próximo post da série.

Ajeitando o visual

Vamos aproveitar que estamos com um jogo já razoavelmente funcional e vamos ajeitar a renderização dos nossos objetos. No método simpleInitApp da classe Main, vamos comentar a linha que define a renderização de wireframe do nosso material.

Essa pequena mudança já traz uma visão totalmente diferente para o nosso jogo:

jmonkey_parte5_4

Na linha de baixo, vale mudarmos a cor dos nossos objetos, para ficar mais fiel ao Pong original:

E, por fim, vamos retirar os marcadores de pontuação da tela, tornando-os invisíveis. Vamos manter o material em amarelo exatamente da forma como está, apenas para facilitar a identificação de algum problema no futuro. O que vamos fazer é um pouco diferente. No final do método initialize, da classe EstadoPontuacao, colocamos a seguinte linha:

Após essas atualizações, temos o seguinte:

jmonkey_parte5_5

Podemos reparar que a marcação da pontuação continua funcionando normalmente. Então, o que o CullHint influencia?

O CullHint indica para a geometria se ela deverá ou não ser escondida (culled) da renderização. O comportamento padrão é herdar do nó pai e na falta de um nó pai é ser dinâmico, ou seja, se tiver alguma parte da geometria dentro do espaço de renderização da câmera, ela estará visível. Se ela estiver totalmente fora da câmera, ela não será renderizada.

Quando indicamos um valor diferente para o CullHint, podemos forçar a renderização ou não de uma geometria. Forçar a renderização costuma ser uma ferramenta de depuração. No nosso caso, forçamos que a geometria fique invisível. Ela continua existindo, recebe todos os seus eventos de colisão e update, mas não recebe eventos de renderização. Poderíamos até mesmo retirar o material associado a essa geometria (matDebug) que não teríamos mais nenhum erro na hora de tentar rodar o jogo.

Vale ressaltar que fizemos a alteração do CullHint no nó de controle, que agrega as duas geometrias. Essa alteração funciona, pois o comportamento padrão é de herança entre os nós.

Antes de terminarmos, vamos acertar mais um pequeno detalhe no nosso jogo. O título da tela sempre aparece como “jMonkey Engine 3.0”. Vamos personalizar um pouco mais. Na classe Main, no método main, vamos adicionar a linha a seguir, logo após o limitador de FPS:

Executando o jogo agora, veremos que o título da janela mudou. Como sempre, temos todo o código dessa etapa disponível no repositório.

Como vocês devem ter percebido, estamos chegando ao fim da série. Já temos um jogo funcional de acordo com o clássico Pong. Nos próximos passos, vamos controlar o estado do jogo adicionando uma tela de abertura e finalizando o jogo quando algum dos jogadores chegar a 10 pontos. Vamos continuar acertando algumas imagens e ícones para o jogo e, por fim, veremos como preparar versões do jogo para enviarmos a nossos jogadores, para que eles possam jogar sem precisar ter a IDE instalada. Se você tem alguma dúvida até aqui, é só deixar nos comentários. Até a próxima!