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
- http://dev.koonsolo.com/7/dewitters-gameloop/
- http://netbeans.org/
- http://slick.cokeandcode.com/
- http://slick.cokeandcode.com/wiki/doku.php