Iniciar no desenvolvimento de jogos eletrônicos, independente da plataforma ser algum dispositivo móvel, requer um conhecimento específico, devido às diferenças em relação à maioria das aplicações. Por ser um sistema computacional, herda diversas características no que diz respeito ao processo de desenvolvimento, porém possui particularidades na documentação envolvida, na definição da arquitetura, e possui truques e técnicas de programação bem particulares.

Da mesma forma, iniciar no desenvolvimento de aplicações para celulares é uma grande mudança de paradigma para os desenvolvedores e os artistas. Suar a camisa para ganhar míseros bytes no tamanho total da aplicação, seja reestruturando o código, seja simplificando as animações dos Sprites, é uma tarefa rotineira para os envolvidos na criação de jogos para celulares.

Este artigo contribui para amenizar as dificuldades do primeiro contato com a plataforma de uma forma bem prática: através da implementação de um exemplo e do comentário detalhado do código, sendo acessível aos que já desenvolveram a sua aplicação de "Alô Mundo" em J2ME, ou àqueles que simplesmente possuem algum conhecimento da linguagem Java.

Ao final, o leitor estará familiarizado com termos e jargões do meio, e terá conhecimento de técnicas e truques clássicos no desenvolvimento de jogos.

Todos os comentários de código estarão em Português. Alguns termos como frame rate, frames per second (FPS) e outros, serão aportuguesados sempre que possível, mas com a devida explicação do termo em inglês, permitindo ao leitor realizar uma pesquisa mais ampla em outras referências sobre o tema.

Porque J2ME?

A plataforma J2ME foi escolhida para exemplificar os trechos de código apenas por ser a mais difundida no mercado. Em uma rápida consulta ao site da Wireless Gaming Review, referência mundial na área de jogos para dispositivos móveis, pode-se encontrar a tabela abaixo.

PLATAFORMA QUANTIDADE
J2ME 222
BREW 42
Symbian 18
Mophun 11
Tabela 1. A primeira coluna indica a plataforma e a segunda indica o número de aparelhos cadastrados (Dados coletados em 15 de fevereiro de 2005)

Apesar disso, muitas das dicas técnicas independem da linguagem e poderão ser aproveitadas para as outras plataformas.

Porque MIDP 1.0?

MIDP ou Mobile Information Device Profile é uma especificação padrão, definida através da JCP (Java Community Process) por um grupo de mais de 50 empresas interessadas. MIDP fornece um conjunto de APIs usadas no desenvolvimento de aplicações Java para celulares e PDAs.

Apesar de MIDP 2.0 já ser uma realidade para alguns aparelhos do mercado, e possuir um pacote de classes específico para desenvolver jogos, o artigo tratará apenas do conjunto de classes de MIDP 1.0. O motivo é o mesmo usado na escolha da plataforma J2ME: viabilidade comercial. A grande maioria dos celulares do mercado ainda não suportam MIDP 2.0. Sendo assim, classes do pacote javax.microedition.lcdui.game, bem como outras funcionalidades encontradas na API MIDP 2.0, não serão consideradas.

"Haja luz", e ela apareceu

No início sempre há mesmo uma certa escuridão. Iniciar no desenvolvimento de uma nova plataforma com diversas limitações, como tamanho da tela, tamanho da aplicação, tamanho da heap e tantas outras, pode intimidar alguns iniciantes.

Para ilustrar o artigo, será criada uma aplicação de exemplo bem simples, com apenas duas classes, onde um quadrado preto de 4x4 pixels se movimenta quando o direcional é pressionado. A Figura 1 mostra o diagrama de classes com as duas classes do exemplo: GameMIDlet e GameScreen.

Diagrama de classes do exemplo. Nem todos os métodos e atributos aparecem por não serem relevantes
Figura 1. Diagrama de classes do exemplo. Nem todos os métodos e atributos aparecem por não serem relevantes

O início de toda aplicação escrita em J2ME se dá na classe abstrata MIDlet, do pacote javax.microedition.midlet e é obrigatório que uma das classes da aplicação herde de MIDlet. A classe do nosso exemplo que herdará de MIDlet será GameMIDlet. Os métodos abstratos que devem ser implementados em GameMIDlet são startApp(), pauseApp() e destroyApp().


public class GameMIDlet extends MIDlet {
  protected void startApp() {
  }
  protected void pauseApp() {
  }
  protected void destroyApp(boolean bool) {
  }
}

O ciclo de vida de cada MIDlet é controlado pelo sistema de gerenciamento da aplicação (application management system ou AMS), um aplicativo do próprio celular responsável por baixar, instalar, executar, e remover os aplicativos instalados e outros recursos.É muito importante conhecer o ciclo de vida de um MIDlet, pois é nele que encontramos alguns erros recorrentes dos iniciantes.

No momento em que o usuário seleciona o MIDlet para executar, o AMS cria uma instância do MIDlet inicialmente no estado de Pausado. O AMS muda então o MIDlet para o estado de Ativo e notifica essa transição através de uma chamada ao método startApp(). Do mesmo modo, os métodos pauseApp() e destroyApp() avisam ao MIDlet quando ele for pausado ou destruído, respectivamente.

Desenvolvedores iniciantes costumam dar pouca atenção ao ciclo de vida do MIDlet, esquecendo de tratar corretamente as mudanças de estado. Por exemplo, o MIDlet pode ser suspenso – pausado pelo AMS – quando uma chamada ou mensagem SMS é recebida. Os desenvolvedores têm a obrigação de tratar situações como essas, retornando à execução, após uma interrupção, sem nenhuma alteração no seu comportamento.

Ciclo de vida de um MIDlet
Figura 2. Ciclo de vida de um MIDlet

Um exemplo do que pode ocorrer em um jogo:

  1. O usuário joga um jogo bem dinâmico, como uma corrida de carros.
  2. O telefone toca e o AMS notifica o MIDlet com uma chamada ao método pauseApp().
  3. O desenvolvedor não se preocupou em criar uma variável de controle para pausar o jogo, então a execução contínua durante a ligação.
  4. O carro colide várias vezes, o que causa perda de energia e pontos.
  5. A ligação é encerrada e o AMS notifica o MIDlet com uma chamada ao método startApp().
  6. O controle do jogo retorna ao usuário, porém em um estado diferente do que ele deixou.
Nota: Chamadas ao método pauseApp() não paralisam a execução do MIDlet, apenas mudam o estado para Pausado. Por isso existe a necessidade de um controle por parte do desenvolvedor, como está claro no item 3. Uma sugestão é criar a flag isPaused e verificar se ela é falsa antes de executar o método update(), impedindo as atualizações no estado de Pausado.

A base do jogo

Em todo jogo é necessário tratar os eventos de entrada do usuário, ou realizar chamadas gráficas para desenhar na tela do dispositivo. A base para se fazer isso é a classe abstrata Canvas, do pacote javax.mocroedition.lcdui.Canvas, e é obrigatório que uma das classes da aplicação herde de Canvas. A classe do nosso exemplo que herdará de Canvas será GameScreen. O único método abstrato que deve ser implementado por GameScreen é paint().


  protected final void paint(Graphics g) {

O jogo executa este método através de chamadas ao método repaint() e nunca deve chamá-lo diretamente. O objeto Graphics passado como argumento do método paint(), serve como interface para a tela do dispositivo. Através dele podemos desenhar primitivas gráficas, como retângulos, linhas, arcos, e escrever um texto. A origem do sistema de coordenadas está localizado no canto superior esquerdo da tela, com é mostrado na figura 3.

Sistema de coordenadas
Figura 3. Sistema de coordenadas

O tratamento dos eventos de tecla também é feito no Canvas. Quando o usuário pressiona alguma tecla do celular, o método keyPressed() é chamado.


protected void keyPressed(int keyCode) {

Da mesma forma, quando uma tecla é liberada, o método keyReleased() é chamado.


  protected void keyReleased(int keyCode) {

O Canvas possui uma implementação vazia dos métodos, e a subclasse GameScreen deve redefini-los.

Uma boa dica a respeito do método keyPressed() é nunca alterar atributos de posicionamento dos sprites no próprio método. O ideal é deixar para fazer isso no método update(), como foi feito com xSquare e ySquare, através das variáveis de controle upKeyPressed, downKeyPressed, leftKeyPressed e rightKeyPressed. Centralizar as atualizações no método update() facilitará futuras manutenções de código.


import javax.microedition.lcdui.Canvas;
import javax.microedition.lcdui.Graphics;
 
 
public class GameScreen extends Canvas {
 
  /**   * Aresta do quadrado
   */
   private final int EDGE = 10;
   
  /**   * Passo do quadrado (de quantos em quantos pixels ele se move)
   */
   private final int STEP = 5;
   /**   * Hexadecimal que representa a cor preta
   */
   private final int BLACK = 0x000000;
    /**   * Hexadecimal que representa a cor branca
   */
   private final int WHITE = 0xFFFFFF;
    
  /**
   * Indica se o direcional está pressionado para cima. 
   */
   private boolean upKeyPressed;
   
   /**
   * Indica se o direcional está pressionado para baixo.
   */
   private boolean downKeyPressed;
   
   /**
 * Indica se o direcional está pressionado para a esquerda. 
   */
   private boolean leftKeyPressed;
   
   /**
   * Indica se o direcional está pressionado para a direita. 
   */
   private boolean rightKeyPressed;
   
   /**
   * Indica se o jogo está pausado.
   */
   private boolean isPaused;
   
   /**
   * Coordenada x do canto esquerdo superior do quadrado
   */
   private int xSquare;
   
   /**
   * Coordenada y do canto esquerdo superior do quadrado
   */
   private int ySquare;
       
   /**
   * Construtor
   */
   public GameScreen() {
       
     // chamada ao construtor da superclasse Canvas
     super();
   }
    
   /**
 * Atualiza o posicionamento do quadrado caso o jogo não esteja
   * pausado
   */   
   public void update() {
       
     if(!this.isPaused()) {
       if(upKeyPressed == true) {
         this.ySquare -= this.STEP;
       }
       
       if(downKeyPressed == true) {
         this.ySquare += this.STEP;
       }
       
       if(leftKeyPressed == true) {
         this.xSquare -= this.STEP;
       }
       
       if(rightKeyPressed == true) {
         this.xSquare += this.STEP;
       }
     }
   }
    
   /**
   * Desenha a tela no objeto Graphics do jogo. Este método é chamado
   * a cada ciclo do jogo.
   */
   protected final void paint(Graphics g) {
       
     // define a cor de pintura do objeto Graphics como sendo branca
     g.setColor(this.WHITE);
    
     // pinta toda a tela de branco
     g.fillRect(0,0,g.getClipWidth(),g.getClipHeight());
       
     // define a cor de pintura do objeto Graphics como sendo preta
     g.setColor(this.BLACK);
 
     // desenha o retângulo preto na posição correta
     g.fillRect(this.xSquare,this.ySquare,this.EDGE,this.EDGE);
   }
 
   /**
 * Este método trata todos os eventos de tecla e é chamado sempre que 
   * uma tecla é pressionada.
   */
   protected void keyPressed(int keyCode) {
 
     // guarda a ação de jogo referente à tecla pressionada, ou 0 se
     // não houver
     int actionCode = this.getGameAction(keyCode);
    
     switch(actionCode) {
 
       case Canvas.UP:
         upKeyPressed = true;
         break;
 
       case Canvas.DOWN:
         downKeyPressed = true;
         break;
 
       case Canvas.LEFT:
         leftKeyPressed = true;
         break;
 
       case Canvas.RIGHT:
         rightKeyPressed = true;
         break;
     }
   }
    
   /**
 * Este método é chamado quando uma tecla pressionada é liberada. 
 * Nesse caso, ele apenas altera as variáveis de controle do
 * direcional para falso, sem fazer uso do parâmetro 'keyCode'. 
   */
    protected void keyReleased(int keyCode) {
       this.upKeyPressed  = false;
       this.downKeyPressed = false;
       this.leftKeyPressed = false;
       this.rightKeyPressed = false;
    }
    
    
   /**
   * Define o jogo como pausado ou não.
   */
   public void setPaused(boolean bool) {
     this.isPaused = bool;
   }
 
   /**
   * Retorna se o jogo está pausado.
   */
   public boolean isPaused() {
     return this.isPaused;
   }
 
Listagem 1. GameScreen.java

O coração do jogo

O coração de todo jogo é o laço principal do jogo ou laço do jogo (game loop). Como na maioria dos aplicativos interativos, um jogo executa até que o usuário decida parar. Cada ciclo é como o batimento cardíaco do jogo. O laço principal de um jogo em tempo real, como os jogos de ação, está muito relacionado à atualização e redesenho da tela. Se cada execução do laço principal está associada a um evento de hardware chamado em um intervalo fixo de tempo, é necessário que todo o processamento, para cada atualização da tela, seja executado durante tal intervalo, ou o jogo terá uma variação na taxa de quadros por segundo (frame rate). Esse controle também pode ser feito no código da aplicação com uma “Técnica de Controle de Velocidade”, tradução incidental para a conhecida Throttle Technique.

Exemplo simples utilizando a Técnica de Controle de Velocidade:


while(theGameIsNotOver) {
 
  Qual o tempo do sistema neste momento?
 
  trata as entradas digitadas pelo jogador
  atualiza os elementos do jogo (Sprites, variáveis de controle, etc.)
  desenha tudo na tela
  
  Se ainda não passou o tempo mínimo para garantir uma taxa de quadros 
  por segundo constante, pausar a execução até atingir a taxa.
}

import javax.microedition.lcdui.Display;
 
import javax.microedition.midlet.MIDlet;
 
 
public class GameMIDlet extends MIDlet implements Runnable {
 
  /**
   * Tempo que cada ciclo do jogo deve durar em milisegundos.
   */
  private static int GAME_DELAY = 50;
 
  /**
   * Instância única do objeto Display para o MIDlet
   */
  public static Display display;
 
  /**
   * Canvas do jogo.
   */
  private GameScreen gameScreen;
 
  /**
   * Este método é chamado logo quando o AMS cria o MIDlet e sempre que a
     * aplicação retorna de uma interrupção (SMS, chamada telefônica, etc.)
   */
  protected void startApp() {
 
    // só entrará neste 'if' na primeira chamada do método, pois nas
    // próximas o objeto gameScreen será sempre diferente de null
    if(this.gameScreen == null) {
                 
      // guarda a instância única do objeto Display
      GameMIDlet.display = Display.getDisplay(this);
 
      // cria a instância do Canvas do jogo
      this.gameScreen = new GameScreen();
    
      // define gameScreen como o objeto visível na tela do
      // dispositivo
      GameMIDlet.display.setCurrent(this.gameScreen);
    
      // executa o método run() depois que terminar o ciclo de
      // pintura, iniciando o laço principal do jogo
      GameMIDlet.display.callSerially(this);
 
    } else {
 
      // retira o jogo do estado de pausa
      this.gameScreen.setPaused(false);
    }
  }
 
  protected void pauseApp() {
 
    this.gameScreen.setPaused(true);
  }
 
  protected void destroyApp(boolean unconditional) {
 
    this.gameScreen = null;
  }
 
  public void run() {
 
    long difference;
    long initialTime;
   
    // Qual o tempo do sistema neste momento?
    // guarda o tempo inicial do processo de pintura da tela
    initialTime = System.currentTimeMillis();
 
    try {
    
      // atualiza os elementos do jogo (Sprites, variáveis de
      // controle, etc.)
      this.gameScreen.update();
 
      // redesenha toda a tela do jogo
      this.gameScreen.repaint();
         
    } catch (Exception e) {  
      e.printStackTrace();
    }
 
    // guarda o tempo para processar a atualização da tela
    difference = System.currentTimeMillis() - initialTime;
      
    // verifica se o tempo decorrido é menor que o necessário
    if(difference < GAME_DELAY) {
      try {
         
        // dorme o tempo necessário para obter o 'frame rate'
        // desejado
        Thread.sleep(GAME_DELAY - difference);
        
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }    
      
    // executa o método run() novamente depois que terminar o ciclo de
    // pintura, dando continuidade ao laço principal do jogo
    GameMIDlet.display.callSerially(this);
  }
}
Listagem 2. GameMIDlet.java

As classes que implementam a interface Runnable devem possuir um método sem argumentos chamado run(). No caso em questão, o método run() é usado para criar o laço principal do jogo através da seguinte chamada: GameMIDlet.display.callSerially(this). O método callSerially(Runnable r) faz uma chamada ao método r.run() do objeto Runnable r passado como referência, após terminar todo o ciclo de pintura.

Explicando melhor, quando a chamada GameMIDlet.display.callSerially(this) é executada em startApp(), a seguinte sequência de fatos ocorrem:

  1. Se houver alguma chamada pendente ao método repaint(), o método paint() será chamado e retornará, encerrando o ciclo de pintura atual.
  2. O método run() de GameMIDlet será então chamado, pois GameMIDlet passou sua própria instância como referência para o método callSerially().
  3. Como dentro do método run() de GameMIDlet também existe uma chamada para GameMIDlet.display.callSerially(this), todo o processo se repete, a partir do item 1., formando assim o laço do jogo.

Conclusão

Através da criação do MIDlet e do Canvas do jogo, e da implementação da “Técnica de Controle de Velocidade” utilizando a interface Runnable , o leitor terá um esqueleto de código reusável, próprio para escrever diversos jogos. Basta apenas que remova o código dos métodos paint() e update().