Concrete Logo
Hamburger button

Como desenvolver seu próprio jogo – Parte 3

  • Blog
  • 28 de Março de 2014
Share

Movimentos e colisões

Um pequeno disclaimer antes de continuarmos. Por uma questão de empolgação, esqueci de colocar a licença no código no repositório. Isso foi corrigido, e o código está licenciado usando a licença do MIT. O código no repositório a partir desta parte já contará com a informação de licença para todas as classes.

Bem-vindo de volta. Vamos começar o post de hoje (veja aqui a primeira e a segunda parte desta série) listando o código de como movimentar os dois jogadores. Caso alguém não tenha conseguido completar alguma das partes e nem acessado o repositório, vamos deixar todos sincronizados.

No final do simpleInitApp() temos:

E no método onAnalog() do analogListener temos:

Com esse código, o jogador da esquerda se move para cima e para baixo com as teclas “R” e “F” e o jogador da direita, com as teclas “O” e “K”. Repare que eu mudei a tecla de “cima” do jogador da direita desde o final do meu último post. No repositório da terceira parte, já está com o código atualizado.

Organização

Antes de continuarmos, acho válido fazermos uma pausa para falar um pouco sobre como organizar o código no jMonkeyEngine. A maioria dos tutoriais sobre jMonkey vai colocar praticamente todo o código em métodos do Application ou em sub-classes privadas. Apesar de facilitar o andamento do tutorial, fica sempre a impressão de que esse é o jeito de se fazer as coisas, e essa forma é um tanto quanto bagunçada.

Então, vamos começar a organizar um pouco melhor as coisas criando controles para darmos vida aos nossos objetos. Como nas partes anteriores, primeiro vamos criar o código necessário e depois fazemos uma rodada de explicação sobre os detalhes.

Até o momento, essa deve ser a estrutura de classes que temos no nosso projeto:

jmonkey_parte3_1.png

Clicando com o botão direito do mouse em “mygame”, vamos selecionar “New” e depois “Java Package…”

Na interface que abre, basta digitar “controle”, deixando o campo “Package Name” com o valor “mygame.controle”, conforme imagem abaixo:

jmonkey_parte3_2

Ao clicar em “Finish”, teremos um novo pacote para nossas classes. Os controles que iremos criar estarão todos dentro desse pacote.

O primeiro controle vai comandar os movimentos da bola. Para isso, vamos clicar com o botão direito do mouse em “mygame.controle” e selecionar o item “New”. Depois, “New Control…”. Pode ser que essa opção não esteja disponível, então, devemos clicar em “New” e depois “Other…”. Na lista de categorias, vamos clicar em “JME3 Classes” e, em “File Types”, selecione “New Control”, conforme a imagem a seguir:

jmonkey_parte3_3

Ao clicar em “Next”, teremos as opções de preencher os dados do novo controle. Precisamos apenas alterar o nome para “ControleBola” e clicar em “Finish”.

A classe criada terá uma série de métodos vazios para implementarmos. Neste momento, porém, vamos ignorar praticamente todos e focar só no que é necessário para atingirmos nosso objetivo.

Logo abaixo da declaração da classe, vamos remover os comentários que o jMonkey adiciona e declarar as variáveis que vamos utilizar, juntamente com um construtor e implementando o método controlUpdate para utilizar esses valores, deixando o início da classe conforme o código abaixo:

E, no final do método simpleInitApp() do nosso Application(main.java):

Se colocarmos o nosso projeto para executar agora, teremos algo muito semelhante ao gif abaixo:

jmonkey_parte3_4

Ou seja, teremos uma bola que se desloca, mas que não colide com nada. Se deixarmos o jogo correr, ela simplesmente irá seguir até o infinito.

Ainda não temos o que gostaríamos, mas já posso explicar uma dinâmica importante antes de continuar. Nosso jogo passa por quatro fases distintas: criação, atualização, renderização e finalização. As etapas de atualização e renderização são repetidas várias vezes consecutivas. Se olharmos o código na classe “Main”, tudo o que colocamos em simpleInitApp() é o código de inicialização e esse método é executado apenas uma vez durante o ciclo de vida do jogo. Mais para baixo temos o simpleUpdate(), que é o método executado na etapa de “atualização” acima. No nosso exemplo, ele está vazio, então, como é que a bola se mexe?

Se checarmos o código na classe ControleBola, veremos que lá existe um método chamado controlUpdate(). Esse método também faz parte da etapa de “atualização” e é executado a cada frame, antes de renderizarmos. Poderíamos ter posto esse código que movimenta a bola também no simpleUpdate se assim desejássemos (e é o que a maioria dos tutoriais acaba fazendo), mas optamos por fazê-lo de forma diferenciada.

O jMonkeyEngine segue um padrão de composição ao invés de extensão. Esse padrão defende que ao invés de fazermos o que estamos acostumados em OOP e criarmos uma hierarquia de classes, com atributos herdados, devemos na verdade criar uma série de controles independentes, extremamente especializados no que eles vão fazer (no nosso caso, mover a bola) e adicionarmos esses controles conforme o necessário aos nossos objetos. Foi exatamente isso que fizemos no exemplo atual: criamos um controle cuja única responsabilidade é controlar a velocidade e direção da bola.

Para cuidar da colisão, podemos criar um controle novo ou utilizar o que já temos para fazer isso. Ambas as soluções são válidas, mas optamos por criarmos um novo controle, que será anexado aos outros objetos na cena.

Os leitores mais atentos repararam que as variáveis do ControleBola foram declaradas como públicas ao invés de privadas com métodos de acesso. Para que fique claro, o método sugerido oficialmente é com métodos de acesso, porém, para brevidade e facilidade de leitura, vamos quebrar essa convenção no momento.

Seguindo o mesmo procedimento descrito anteriormente, vamos criar um novo controle no pacote “mygame.controle”, chamado ControleColisao.

O início do código da classe fica:

Na nossa classe Main, precisamos anexar instâncias dos controles aos jogadores, e isso é feito no final do método simpleInitApp(), logo após a associação do controle da bola:

Se executarmos o nosso jogo agora, veremos que a bola se desloca lentamente para a direita e é refletida no jogador da direita. Ela passa a se deslocar para a esquerda e é refletida no jogador da esquerda.

Como isso acontece? Primeiro, vamos olhar os valores que atribuímos ao controle no simpleInitApp. Lá, dizemos que o eixo de reflexão é Vector3f.UNIT_X.mult(-1.0f). Ou seja, -1 no sentido do eixo X, ou ainda (-1,0,0). No nosso ângulo de visão, o X está na horizontal da câmera.

A lógica real está dentro do controle ControleColisao, no método controlUpdate. Na primeira linha do método, verificamos se a bola está na região de nosso controle. Mais especificamente, se o volume que ela ocupa no “mundo” faz intersecção com o volume que ocupamos no “mundo”. O objeto ao qual o controle está associado é identificado pela variável spatial. No nosso caso, o volume ocupado é exatamente igual ao objeto renderizado. Porém, isso normalmente não é verdade. Uma tática bem comum é representarmos objetos complexos (naves espaciais, por exemplo ) utilizando objetos mais simples, como cubos ou esferas.

Na segunda e terceira linha, pegamos um controle que conhecemos que o objeto bola tem (neste caso, o ControleBola) e interagimos com ele. Na quarta linha, refletimos a direção da bola no eixo -X, conforme falamos anteriormente. Essa é uma das formas de usarmos a composição de controles para atingirmos o nosso objetivo. O problema dessa estratégia é que, apesar de termos controles trabalhando de forma independente em objetos diferentes, os dois controles ainda estão muito acoplados. No próximo post vamos cuidar desse detalhe.

Lembrando que, conforme falamos anteriormente, o método controlUpdate de cada instância de cada controle é chamado a cada frame que é renderizado. Portanto, é importante garantir que esses métodos não realizem nenhum tipo de operação excessivamente pesada, senão prejudica o framerate e acaba atrapalhando o jogo.

Como informação, vale ressaltar a criação dos controles. Você pode ter reparado que declaramos dois controles do tipo ControleColisao, com os mesmos parâmetros, e associamos cada um a um jogador diferente. Nesse caso em particular, não poderíamos “otimizar” e declarar uma só instância e associar aos dois jogadores? A resposta é “não”. Como vimos, os controles têm acesso a uma variável chamada spatial, que referencia o objeto ao qual estão associados. Esse valor é atribuído quando o controle é associado e fica cacheado na instância do controle. Portanto, é necessária uma instância do controle para cada objeto na nossa cena.

Para você que conseguiu nos acompanhar até aqui, parabéns! Estamos começando agora a ver as partes mais interessantes da engine e de como fazer algumas coisas básicas, do jeito certo. Na próxima parte, vamos melhorar o movimento da bola, fechar a área de jogo (afinal, a bola vai começar a ser defletida não só no eixo X) e preparar as áreas de pontuação atrás de cada jogador.

Para quem quiser, o código dessa parte está disponível no repositório parte três. Até a próxima!