Pong com Slick2D em Java

Neste artigo, vamos aprender a utilizar uma simples biblioteca para jogos 2D chamada Slick2D. Esta biblioteca é bastante simples de usar e abstrai o programador das partes mais aborrecidas na implementação básica de um jogo, tais como a implementação do ciclo principal de jogo e comunicação com o sistema operativo.

Vou explicar o código da minha implementação, assim como os conceitos fundamentais no desenvolvimento de qualquer jogo. Para começar vamos estudar a estrutura fundamental de qualquer aplicação interactiva.

Um jogo, num nível mais abstracto, é uma aplicação que recebe e responde a eventos. Esses eventos podem ser externos (teclas pressionadas, movimento do rato) ou internos (por exemplo num jogo de carros, detectar a colisão contra uma parede). Em termos de código, isto traduz-se em algo semelhante a:

bool gameIsRunning = true;

while( gameIsRunning ) {
    update(); 

    render();
    if( userWantsToQuit() )
        gameIsRunning = false;
}

Enquanto a variável que indica se o jogo está a decorrer (gameIsRunning) o ciclo while é sempre executado. Em cada ciclo, actualizamos o estado do jogo e desenhamos o mundo para ecrã. Se o utilizador entretanto fechar a janela ou carregar numa tecla pré-definida para o efeito, a função userWantsToQuit retorna verdadeiro, actualizamos a variável de saída, e o ciclo acaba.

É fundamental que percebam este conceito, pois é a base de qualquer jogo. Numa aplicação mais complexa, existem mais pormenores a ter em atenção, mas para este jogo, a biblioteca abstrai grande parte do essencial.

Para começar, vamos criar um novo ficheiro em Java, importar a biblioteca Slick. Podem obter a biblioteca no site oficial localizado em http://slick.cokeandcode.com/. Eu vou utilizar a biblioteca no ambiente NetBeans IDE, mas não devem ter problemas com qualquer outro ambiente de desenvolvimento para Java.

Para dar uso da biblioteca Slick2D vamos criar uma classe para representar o jogo. Eu, num momento de grande originalidade, chamei a classe Game. Temos também de definir alguns métodos na nossa classe, que serão chamados pela biblioteca nos momentos apropriados.

package mongo;

import org.newdawn.slick.*;

public class Game extends BasicGame {
    public Game(String title) {
        // Para implementar
    }

    public void init(GameContainer gc) {
        // Para implementar
    }

    public void update(GameContainer gc, int delta) { 
        // Para implementar
    }

    public void render(GameContainer gc, Graphics g) {
        // Para implementar
    }

    public static void main(String[] args) {
        // Para implementar
    }
}

A classe principal vai extender a classe BasicGame, disponibilizada pela biblioteca. Esta é a estrutura base para qualquer jogo desenvolvido com a biblioteca Slick2D. Agora temos de escrever o código para iniciar a biblioteca e começar o ciclo principal de jogo.

public static void main(String[] args) {
    AppGameContainer app = new AppGameContainer(new Game(title));
    app.setDisplayMode(width, height, fullscreen); 
    app.setTargetFrameRate(fpslimit); 
    app.start();
}

Este código cria uma nova instância da aplicação e passa a classe correspondente ao jogo no constructor. O título pretendido (que queremos que apareça na janela) é passado como argumento do constructor da classe do jogo. Depois iniciamos a janela com a largura e altura desejada, e escolhemos se a janela vai ocupar o ecrã todo. Para finalizar, executamos o método que vai iniciar o ciclo principal do jogo. Como podem ver no código, os valores foram definidos como variáveis estáticas da classe, para facilitar futuras alterações.

static int width = 640;
static int height = 480;
static boolean fullscreen = false;

static String title = "Mongo";
static int fpslimit = 60;

Entretanto já temos definida a estrutura básica de uma aplicação, e se executarem a aplicação deve ser mostrada uma janela sem conteúdo. Para mostrar o conteúdo temos de implementar os métodos correspondentes ao ciclo de actualização e desenho da aplicação.

Vamos então pensar como modelar o jogo do Pong. Este jogo foi um dos primeiros video jogos e consiste numa simulação de um campo (de ténis de mesa), onde uma bola é atirada de um lado para o outro. O jogo é normalmente jogado por 2 jogadores, mas a nossa versão vai também ter uma opção onde o segundo oponente é controlado pelo computador.

Para representar as duas raquetes, podemos utilizar imagens, mas para facilitar a implementação vamos utilizar dois rectângulos. Para a bola, vamos utilizar um círculo. A biblioteca Slick2D já disponibiliza estas primitivas geométricas.

Circle ball;
Rectangle paddlePlayer;

Rectangle paddleCPU;

Para mover a bola pelo ecrã, temos de representar a sua velocidade. Estas quantidades são normalmente representadas por vectores de duas dimensões. A biblioteca Slick2D possui também uma biblioteca de matemática com estas primitivas implementadas. Vamos definir algumas variáveis para a velocidade e para armazenar as pontuações obtidas por cada jogador.

Vector2f ballVelocity;

int scorePlayer;
int scoreCPU;

Já temos, então, tudo o que precisamos para guardar o estado de jogo. Agora falta implementar os vários métodos que ficaram em branco. Vamos começar com o método de iniciação, que é chamado pela biblioteca quando o jogo é iniciado. Este método tem um parâmetro do tipo GameContainer, através do qual podemos aceder a outras classes que oferecem vários serviços, como o sistema de input, contexto dos gráficos 2D, sistema de som, etc.

public void init(GameContainer gc) {
    gc.getInput().enableKeyRepeat();

    paddlePlayer = new RoundedRectangle(5, height/2, 10, 80, 3);
    paddleCPU = new RoundedRectangle(width-15, height/2, 10, 80, 3);
    ball = new Circle(width/2, height/2, 6);
    ballVelocity = new Vector2f(-3, 1);
}

Neste métodos vamos ligar suporte para repetição de teclas (assim enquanto uma tecla estiver pressionada, o Slick2D envia sempre eventos de input). Depois iniciamos as variáveis de jogo declaradas anteriormente. Os construtores das figuras recebem os tamanhos e posições na janela. As duas raquetas são criadas na esquerda e na direita, e depois a bola é criada no centro do ecrã, com velocidade horizontal inicial correspondente a deslocação para a esquerda e para baixo.

Depois da iniciação estar concluída, falta implementar a actualização e a renderização do jogo.

public void render(GameContainer gc, Graphics g) {
    g.fill(paddlePlayer);
    g.fill(paddleCPU);
    g.fill(ball);
}

O código cima é muito simples e só desenha as figuras que iniciámos para representar os jogadores e a bola. Com o código escrito até agora, o jogo já pode ser iniciado e são desenhadas todas as figuras do jogo. Falta então adicionar vida ao jogo, com a função de simulação/actualização (o método update).

O parâmetro delta é usado para que a simulação do jogo seja independente da velocidade de renderização. Vejam o link nos recursos que explica diferentes implementações de game loops. Como este jogo é muito simples, vamos ignorar o delta.

Primeiro vamos pensar no que pode acontecer no Pong. Se o utilizador carregar nas teclas temos de mover o rectângulo para cima ou para baixo, dependendo da tecla que foi pressionada.

if(gc.getInput().isKeyDown(Input.KEY_UP)) {
    if(paddlePlayer.getMinY() > 0) 
        paddlePlayer.setY(paddlePlayer.getY() - 10.0f);
} else if(gc.getInput().isKeyDown(Input.KEY_DOWN)) {
    if(paddlePlayer.getMaxY() < height) 
        paddlePlayer.setY(paddlePlayer.getY() + 10.0f);
}

Temos também de actualizar a posição da bola com a respectiva velocidade.

ball.setLocation(ball.getX()+ballVelocit y.getX(), ball.getY()+ballVelocity.getY()); 

Agora vamos detectar as colisões que podem acontecer quando a bola bate nos limites do campo de jogo. Em termos horizontais, se a bola sai do ecrã pela esquerda ou pela direita, temos de trocar a velocidade horizontal e actualizar as pontuações.

if(ball.getMinX() <= 0) {
    ballVelocity.x = -ballVelocity.getX();
    scoreCPU++;
} 

if(ball.getMaxX() >= width) {
    ballVelocity.x = -ballVelocity.getX();
    scorePlayer++; 
}

Agora em termos verticais, só temos de trocar a velocidade na componente Y do vector da velocidade.

if(ball.getMinY() <= 0)
    ballVelocity.y = -ballVelocity.getY(); 

if(ball.getMaxY() >= height)
    ballVelocity.y = -ballVelocity.getY();

Resta detectar colisões quando a bola bate nas raquetes. Basta trocar a velocidade na componente do X.

if(ball.intersects(paddlePlayer) || ball.intersects(paddleCPU)) {
    ballVelocity.x = -ballVelocity.getX();
}

Experimentem correr o jogo e já devem ter algo que se assemelha ao jogo do Pong. Mas temos uma falha grande. A raquete do oponente não é simulada pelo computador. Vamos adicionar ao jogo, para podermos jogar contra o computador. Na minha implementação a “inteligência artificial” é muito simples. A raquete do oponente segue sempre a posição vertical da bola, tornando impossível vencer o computador:

float posY = ball.getCenterY() - paddleCPU.getHeight()/2;
paddleCPU.setY(posY);

Muito simples! O algoritmo pode ser melhorado através do uso de números aleatórios. Experimentem modificar o jogo e adicionar novas funcionalidades.

Recursos

 

Publicado na edição 25 (PDF) da Revista PROGRAMAR.