Game API Simplicidade e poder em jogos para celu la O mercado de jogos para dispositivos móveis apresenta tendências de enorme crescimento. Segundo um estudo do IDC, são previstos mais de 70 milhões de jogadores até 2007 – dez vezes o número de 2002. A programação de jogos para pequenos aparelhos apresenta características bem distintas em relação aos para PCs ou consoles de videogames. O poder de processamento, o tamanho da tela, a quantidade de memória e a largura de banda disponível são muito mais limitados, por tanto é necessário que os jogos sejam planejados considerando essas características. Por outro lado, as restrições podem ser vistas como vantagens para o programador independente. Jogos sendo desenvolvidos para celulares atualmente são comparáveis aos de antigos videogames, como o primeiro Nintendo (8 bits) ou o Super Nintendo, de 16 bits. Costumam ser bem mais simples do que os jogos para PCs ou consoles atuais, cuja criação muitas vezes exige investimentos de milhões de dólares, equipes de dezenas de pessoas e anos de trabalho. No caso de jogos para celulares, equipes de tamanho reduzido, geralmente com cinco pessoas ou menos, conseguem desenvolver jogos em alguns meses – e, claro, com um orçamento muito menor. J2ME e MIDP 2.0 Uma vantagem de desenvolver jogos para celulares é poder tirar proveito da plataforma J2ME. Por ser um padrão aberto, o Java 2 Micro Edition não requer autorização dos fabricantes de aparelhos, como acontece para o caso de videogames. Com J2ME, também não ocorre uma prática comum na indústria de jogos para PCs: o licenciamento de engines de jogos (como o do Quake) a preços altíssimos para servirem de base para outros que seguem o mesmo estilo. Além disso, a distribuição dos jogos é facilitada, pois, ao contrário do que acontece no mercado tradicional de games, em que os principais canais de distribuição são as lojas de varejo, as aplicações para celulares são vendidas diretamente ao consumidor, pela internet através de portais, ou pela rede das operadoras. Mais especificamente, o MIDP 2.0, a nova versão do perfil J2ME direcionado principalmente para celulares, é um passo fundamental para uma maior padronização da programação Java para dispositivos wireless. Antes dos avanços do MIDP 2.0, os fabricantes se viam forçados a oferecer recursos adicionais criando extensões suportadas apenas pelos seus modelos de celulares – perdia-se assim uma das características principais de Java: a portabilidade. Na nova versão do MIDP, várias funcionalidades úteis para o desenvolvimento de jogos foram acrescentadas, como, por exemplo, o suporte à transparência de pixels. Um destaque foi a inclusão da Game API, que serve como fundação para a criação de jogos, permitindo um melhor uso das capacidades gráf icas nativas do aparelho e trazendo assim maior desempenho. Game API: visão geral Figura 1. Classes principais para jogos 42 Java Magazine • Edição 10 As cinco classes do pacote javax.microedition. lcdui.game (veja a Figura 1) estendem as capacidades do MIDP para jogos 2D de diversas formas significativas. Um dos conceitos principais da Game API é permitir que o conteúdo da tela seja composto por layers (imagem de fundo, detalhes do cenário, personagens etc.), representadas por objetos Layer. Uma layer pode ser qualquer elemento gráfico, e tem como características posição, tamanho e visibilidade. As duas subclasses concretas de Layer são Sprite e TiledLayer. Sprite é utilizada para compor uma animação a partir de vários quadros, oferecendo também métodos para movimentação e giro, além de detecção de colisões. A classe TiledLayer possibilita a criação de uma imagem grande através da composição de partes menores, o A API de jogos do MIDP 2.0 fornece uma infra-estrutura sólida para a criação de games para dispositivos J2ME ares Vanessa Sabino que é bastante útil para a construção de cenários ou fundos (backgrounds), como veremos adiante. A classe LayerManager controla o desenho de um grupo de layers, e GameCanvas suporta o ciclo básico do jogo, implementando mecanismos para obter o estado das teclas, manipular o buffer gráfico e enviar imagens para a tela. Preparação e exemplo Como mostrado no artigo “Projeto de jogos wireless” (Edição 3), o primeiro passo para criar um jogo é realizar o projeto conceitual, definindo suas características. No exemplo deste artigo, criaremos um jogo do tipo “Pac Man”, que, como a maioria dos leitores lembrará, compreende um labirinto em que o personagem controlado pelo usuário deve recolher todas as “bolinhas” e evitar o contato com os fantasmas – isto quando não estiver sob efeito das “bolinhas de poder” localizadas nos cantos do labirinto, que o tornam temporariamente invencível. O exemplo é mostrado em execução na Figura 2. Para criar o jogo você precisará de um kit de desenvolvimento para MIDP 2.0. Pode começar utilizando o J2ME Wireless Toolkit 2.0 (WTK) da Sun (veja links), que possui ferramentas para gerar arquivos JAR e JAD, além de emuladores para testar as aplicações. Vale a pena também obter os kits das fabricantes de celulares, que incluem emuladores para aparelhos específicos. O processo de desenvolvimento de jogos MIDP com o WTK é igual ao de qualquer outro MIDlet (veja o quadro “Executando o exemplo”). As imagens utilizadas no jogo devem ser copiadas para o diretório <J2ME_HOME>/apps/PacMan/ res (onde <J2ME_HOME> é o diretório raiz do W TK ), e o código fonte para <J2ME_HOME>/apps/PacMan/src. O código do exemplo está dividido em dois pacotes. Em jm.pacman estão a classe PacManMIDlet, chamada pelo gerenciador de aplicações do celular, e encarregada de inicializar e finalizar a aplicação; e PacManCanvas, a classe principal do jogo, Executando o exemplo P ara executar o exemplo, após fazer o download do site da Java Magazine, crie um projeto na KToolBar do Wireless Toolkit e clique no botão New Project; digite “PacMan” em Project Name e “jm.pacman.PacManMIDlet” em MIDlet Class Name (como na figura). Será mostrada uma janela com as informações do arquivo MANIFEST.MF; mante- nha os valores sugeridos e clique em OK. Isto criará uma estrutura de diretórios em <J2ME_HOME>/apps/PacMan. Descompacte o ZIP dentro deste diretório, lembrando de manter a estrutura de pastas. Para compilar e pré-verificar as classes, clique em Build; para testar a aplicação em um dos emuladores disponíveis, clique em Run. Figura2.PacManrodandonoemuladordoJ2ME Wireless Toolkit responsável por criar todos os elementos gráficos, organizar sua disposição na tela, verificar eventos e executar a lógica do jogo. No pacote jm.pacman.sprites estão PacManSprite e FantasmaSprite, as classes de sprites dos personagens do jogo. Ambas herdam de MovimentoSprite, que implementa os métodos básicos de movimentação dos sprites. Por fim, as classes Bolinha e PoderSprite representam os dois tipos de bolinhas. Sprites Iniciaremos a construção do jogo pelos sprites. No nosso exemplo, a maioria dos elementos que sofrem algum tipo de ação – o personagem, os fantasmas e as bolinhas de poder – foi criada como sprites, o que simplifica e torna mais claro o código. Apenas as bolinhas verdes foram construídas pelo método tradicional (renderizando a Edição 10 • Java Magazine 43 Game API imagem diretamente no objeto Graphics com drawImage()). Isso evita a criação de 52 objetos praticamente iguais diferenciados apenas pela posição – o que seria um desperdício de memória. O personagem do PacMan aproveita bastante os recursos da classe Sprite.����� Por exemplo, para dar mais vida ao jogo, criamos uma animação simples abrindo e fechando sua boca conforme se movimenta; para obter esse efeito, a imagem do sprite foi criada com dois quadros representando cada estado do sprite (veja a Figura 3). No construtor da classe PacManSprite, a primeira linha chama o construtor da superclasse (Sprite), passando como parâmetros a imagem (um arquivo PNG), além da largura e a altura de cada quadro: super(image, frameWidth, frameHeight); Dessa forma, na imagem de tamanho 32x16 é determinado que o primeiro quadrado de 16x16 pixels (a região entre os pixels de coordenadas [0,0] e [15,15]) é o primeiro quadro da animação; o segundo quadro compreende a área entre os pixels [16,0] e [32,32]. Para alternar entre os quadros, é chamado o método nextFrame(). Se houvesse um maior número de quadros, seria possível escolher uma seqüência diferente da apresentada na imagem, utilizando o método setFrameSequence(); para exibir um quadro específico, chama-se setFrame(). Por padrão, qualquer posicionamento de gráficos é feito a partir da origem da tela – o ponto 0x0, localizado no canto superior esquerdo. Para utilizar o centro da imagem como referência, no construtor do personagem foi chamado o método defineReferencePixel(), fazendo ���������������� com que a imagem seja rotacionada em torno do seu centro (portanto, sem deslocamentos). Para girar o sprite, usamos o método setTransform() e uma das oito constantes de direção, correspondentes a giros múltiplos de 90 graus (da imagem normal e da espelhada). Por exemplo, para fazer o PacMan apontar para cima, deve-se girá-lo 270 graus no sentido horário, da seguinte maneira: setTransform(TRANS_ROT270);//CódigoemPacManSprite Apesar de só estarem disponíveis quatro direções, se forem criados quadros adicionais representando outras orientações, fica fácil apontar o sprite para qualquer direção, ou fazer um giro mais suave. Observe também que as transformações não são cumulativas; sempre é adotada como referência a imagem original. A movimentação do sprite também é simples, usando o método move(), que recebe como parâmetros a quantidade de pixels Imagens em pedaços U sando TiledLayer, a imagem original (parte de cima da ilustração ao lado) é quebrada em partes, que são indexadas e usadas para montar o cenário do jogo (paredes). O índice de cada parte é especificado em um array de mapeamento como no código abaixo, onde zeros indicam espaços vazios: final���� int[] ��� mapa = { 1, 7, 7, 7, 7, 7, 7, 7, 7, 2, 10, 9, 0, 0, 5, 6, 0, 0, 8, 20, 10, 13, 17, 0, 10, 20, 0, 17, 14, 20, 10, 0, 0, 0, 0, 0, 0, 0, 0, 20, 10, 3, 7, 7, 0, 0, 7, 7, 4, 20, 10, 13, 17, 17, 0, 0, 17, 17, 14, 20, 10, 0, 0, 0, 0, 0, 0, 0, 0, 20, 10, 3, 7, 0, 10, 20, 0, 7, 4, 20, 10, 19, 0, 0, 15, 16, 0, 0, 18, 20, 11, 17, 17, 17, 17, 17, 17, 17, 17, 12 }; for� (���������������������������������� int������������������������������� i = 0; i < mapa.length; i++) { int� coluna = i % 10; int linha = (i - coluna) / 10; fase.setCell(coluna, linha, mapa[i]); } 44 Java Magazine • Edição 10 Figura3.Aimagembaseparaumspriteédivididaem quadros que irão compor a animação na horizontal e na vertical que o sprite deve ser deslocado; os valores são positivos nos sentidos para baixo ou para direita e negativos nos sentidos inversos. O mecanismo de detecção de colisões é outro ponto forte da classe Sprite. O método collidesWith() está disponível em três versões sobrecarregadas. Ele verifica colisões com outro sprite, com imagens dentro de uma TiledLayer, ou com um objeto Image em uma posição especificada. Na classe responsável pela lógica do jogo (PacManCanvas) são usadas as três versões de collidesWith(): •pacMan.collidesWith(fantasma[f], true) //Sprite •pacMan.collidesWith(parede, true) //TiledLayer •pacMan.collidesWith(bolinhas.imagem, bolinhas.x[i],bolinhas.y[i], true) //Image O último parâmetro determina se deve ou não ser considerada a transparência do pixel. Como toda imagem ocupa um espaço retangular, definir o parâmetro como Cenários animados A classe TiledLayer inclui métodos para realizar a animação de células, o que permite montar cenários animados. Como exemplo, vamos criar um cenário com moinhos cujas pás giram em sincronia. O cenário foi montado a partir de sete partes (Figura Q1). As partes com índices 5, 6 e 7 serão alternadas para simular o giro. Um array foi criado com esses índices para simplificar a alternância entre células no ciclo da animação: int[] quadros = { 5, 6, 7 }; FiguraQ1.Imagembaseparaa montagemdocenário:apenas7 partes são utilizadas Primeiramente obtemos um índice que identifica a animação, através do método createAnimatedTitle(), que recebe o índice da imagem inicial a ser mostrada: indiceAnimacao = this.createAnimatedTile(5); O índice retornado é sempre um número negativo (lembre-se que índices zero representam espaços vazios e os positivos, pedaços da imagem base). Esse índice é então atribuído às células animadas, correspondentes às pás, fazendo-se chamadas ao método setCell() (observe que as posições passadas correspondem às indicadas na Figura Q2): FiguraQ2.Cenárioanimado(os índicesindicamascoordenadas das células com animação) setCell(1, 1, indiceAnimacao); setCell(2, 2, indiceAnimacao); setCell(3, 1, indiceAnimacao); true permite trabalhar com a forma real da imagem, não considerando um “contato” como colisão a não ser que dois pixels opacos (não-transparentes) estejam sobrepostos. Aproveitando a transparência, no nosso exemplo foi possível desenhar as paredes com um número reduzido de células (veja adiante). Pode-se também definir uma área específica de colisão, através do método defineCollisionRectangle(). TiledLayer A classe TiledLayer, como citado, é utilizada para construir uma imagem a partir de diversos pedaços, ou “ladrilhos” (tiles) menores. No exemplo, uma imagem de 160x32 pixels serviu como base para uma região completa de 160x160. Criamos o TiledLayer como uma tabela de 10x10. No Em seguida, o quadro atual da animação é alterado con­­tinuamente, chamando-se setAnimatedTile(): setAnimatedTile(indiceAnimacao, quadros[posicao]); O código abaixo mostra a implementação da animação: import javax.microedition.lcdui.Image; import javax.microedition.lcdui.game.TiledLayer; public class MoinhoLayer extends TiledLayer { private final int[] quadros = { 5, 6, 7 }; private int posicao = 0; private int indiceAnimacao; public MoinhoLayer(Image imagem) { super(5, 5, imagem, 32, 32); final int[] mapa = { 0, 0, 0, 0, 0, 0, 5, 0, 5, 0, 0, 3, 5, 3, 0, 1, 2, 4, 2, 1, 1, 1, 2, 1, 1 }; for (int i = 0; i < mapa.length; i++) { int coluna = i % 5; int linha = (i - coluna) / 5; setCell(coluna, linha, mapa[i]); } inicializarPas(); } private void inicializarPas() { indiceAnimacao = this.createAnimatedTile(5); setCell(1, 1, indiceAnimacao); setCell(2, 2, indiceAnimacao); setCell(3, 1, indiceAnimacao); } public void girarPas() { posicao = (posicao + 1) % 3; setAnimatedTile(indiceAnimacao, quadros[posicao]); } } Para animar o cenário durante a execução do jogo, simplesmente chamamos cenario.girarPas() dentro do método run(). seu construtor são definidas as quantidades de colunas e de linhas, a imagem base, e como ela deve ser dividida (no caso, em quadrados de 16x16): Imageimagem=Image.createImage(“/imagens/parede.png”); TiledLayerfase=newTiledLayer(10,10,imagem,16,16); A imagem é então automaticamente indexada de 1 a 20 (correspondendo aos vinte quadrados de 16x16 que cabem na imagem de 160x32), seguindo a ordem da esquerda para a direita e de cima para baixo. Observe que aqui, diferentemente da classe Sprite, a numeração dos pedaços não começa de zero, que é utilizado para indicar células em branco ou vazias. Veja no quadro “Imagens em pedaços” como é feito o mapeamento no código. LayerManager A classe LayerManager facilita a renderização de objetos Sprite e TiledLayer.���������� Com ela, em vez do programador chamar o método paint() de cada objeto individualmente, ele simplesmente os adiciona ao LayerManager e depois chama o método paint() desta classe,� que fica responsável por desenhar as regiões corretas de cada layer na ordem determinada (veja a Figura 4). A ordem em que as layers são renderizadas influencia como são mostradas, pois seu índice está relacionado à z-order, ou seja, as layers de índice mais baixo ficam mais “perto” do usuário, aparecendo na frente daquelas de índice mais alto. Estão disponíveis dois métodos para a inserção de layers: append, que recebe como parâmetros apenas o objeto e insere-o no Edição 10 • Java Magazine 45 Game API final da “pilha” de layers; e insert, que recebe, além do objeto, o índice em que deve ser inserido. Por exemplo: layerManager.insert(parede, 0); layerManager.insert(pacMan, 1); Após colocar todos os índices na ordem, é chamado o método setCell() para cada um, def inindo que par te da imagem será mostrada em cada célula. Também é possível trabalhar com grupos de células consecutivas, utilizando o método fillCells(), ou até mesmo fazer a animação de células não-adjacentes, através de um mecanismo que utiliza números negativos para representar os pedaços de imagem que serão alternados. Veja um exemplo no quadro “Cenários animados” (página anterior). Ao chamar o método paint(), também é definido a partir de qual coordenada será mostrado o conteúdo das layers. Dessa forma fica fácil reservar uma região da tela para exibir a pontuação, o número de vidas e outras informações, renderizadas independentemente das layers. Outra característica interessante da classe LayerManager é permitir a rolagem da tela (scrolling). Em vez de montar apenas a parte do cenário a ser exibida, pode-se definir uma layer maior, contendo uma região mais ampla ou até a fase inteira. No nosso PacMan, o labirinto poderia não estar limitado ao tamanho da tela, por exemplo. A rolagem é implementada utilizando o conceito de View Window, que determina a região do LayerManager que deve ser desenhada. O método setViewWindow() recebe como parâmetro a posição e o tamanho da região visível, que são geralmente alterados quando varia a posição do personagem controlado pelo jogador. Ciclo básico e GameCanvas Essencialmente, todo jogo segue um ciclo básico, executado continuamente até que o jogo seja interrompido (veja a Figura 5). Primeiro é verificada a entrada do usuário, depois atualizado o estado do jogo de acordo com essa entrada e com a Figura 5. Ciclo básico de jogos lógica implementada; em seguida o buffer gráfico é atualizado e enviado para a tela; e o ciclo recomeça. A parte principal da lógica de um jogo baseado na Game API é implementada em uma subclasse de GameCanvas. Descendente de javax.microedition.lcdui.Canvas, esta classe abstrata acrescenta funcionalidades específicas para jogos, como a capacidade de verificar teclas e recursos para manipulação de um buffer gráfico (off-screen buffer). No código do construtor da subclasse de GameCanvas, logo na chamada ao construtor da superclasse, deve ser informado se os eventos normais das teclas de jogo devem ser ignorados, evitando chamar listeners desnecessariamente. No restante do construtor, costuma-se inicializar os demais elementos gráficos e adicioná-los ao LayerManager. Por exemplo: public PacManCanvas() throws IOException { super(true); //Para ignorar eventos das teclas de jogo pacMan = criarPacMan(); layerManager = new LayerManager(); layerManager.append(pacMan); } Figura4.DiversaslayerssãoinseridasnoLayerManager,criandoocenáriocompletoquandocombinadas 46 Java Magazine • Edição 10 (Note que estamos mostrando trechos simplif icados do código do exemplo. Consulte o download do artigo para as versões completas). Em se guida , é utiliz ado o méto do São apenas algumas possibilidades de extensões – isso para um exemplo simples como o mostrado aqui. Listagem 1. Implementação do ciclo do jogo public void run() { // Obtém o objeto no qual será renderizada a tela Graphics g = getGraphics(); //Ciclo básico while (executando) { // Atualiza a posição do PacMan e dos fantasmas movimenta(); // Verifica colisões verificaPosicaoParede(); verificaBolinha(); verificaPoder(); verificaFantasma(); // Se tiverem acabado as bolinhas, reinicializar if (qtdBolinhas == 0) { reinicializaFase(); } // renderiza a tela renderiza(g); } } Conclusões A Game API proporciona produtividade e versatilidade para o desenvolvimento de jogos para celulares. Através dela, o desenvolvedor pode se concentrar no conceito dos jogos sem se preocupar com detalhes de baixo nível da implementação gráfica. Com suporte nativo a sprites e layers, combinado a um hardware compatível, é possível atingir nos jogos MIDP 2.0 uma qualidade gráfica que já está sendo comparada com a do GameBoy. Considerando-se ainda outras vantagens de Java, como penetração no mercado e segurança, o MIDP 2.0 torna-se uma alternativa muito forte para a criação de games para dispositivos móveis. Listagem 2. Renderização dos gráficos private void renderiza(Graphics g) { // Define a cor base (branco) g.setColor(0xffffff ); // Desenha um retângulo para “limpar” a tela g.fillRect(0, 0, largura, altura); // Define a posição em que será desenhado o jogo, centralizando-o // caso a tela seja maior do que 160x160 int x = (largura - 160) / 2; int y = (altura - 160) / 2; bolinhas.paint(g, x, y); layerManager.paint(g, x, y); } int keyStates = getKeyStates(); if ((keyStates & LEFT_PRESSED)!= 0) { pacMan.esquerda(); } O ciclo do jogo é implementado dentro do método run(), pois será executado em uma thread independente. Antes de iniciar o ciclo em si, deve ser obtido o objeto Graphics, utilizado como buffer. Dentro do laço while (Listagem 1)������������������������� , que será executado até o jogo terminar, são chamados os métodos referentes à lógica do jogo e à renderização dos gráficos (Listagem 2). NométodostartApp()daclassePacManMIDlet é instanciada a classe GameCanvas e iniciada java.sun.com/products/midp Mobile Information Device Profile java.sun.com/products/j2mewtoolkit J2ME Wireless Toolkit // Envia os dados para a tela flushGraphics(); getKeyStates() para verificar as teclas que estão pressionadas naquele momento ou que foram pressionadas pelo menos uma vez desde a última chamada do método. Para identificar as teclas, basta comparar as constantes que representam cada uma isoladamente com o retorno desse método, utilizando o operador &. Veja um exemplo: www.nintendo.com/systems ConsolesdaNintendo,incluindooNintendo Entertainment System e o SuperNES a thread responsável pelo ciclo do jogo: canvas = new PacManCanvas(); canvas.start(); Extensões O exemplo deste artigo explorou apenas o básico da criação de jogos com o MIDP 2.0 e a Game API. Uma extensão importante seria o acréscimo de efeitos sonoros, usando os recursos da Mobile Media API (veja o artigo “Multimídia no celular”, na Edição 2). E para que haja um desafio maior para os jogadores, poderíamos implementar técnicas de inteligência artificial; no exemplo, os fantasmas poderiam seguir o PacMan calculando a cada movimento o menor caminho para alcançá-lo, ou até mesmo usar algoritmos sofisticados para cooperarem visando à captura do personagem. Poderíamos também aumentar a dificuldade de acordo com o progresso do jogador, tornando os cenários mais complexos ou incrementando gradualmente a velocidade e a inteligência dos inimigos. java.sun.com/products/mmapi Mobile Media API www.forum.nokia.com/main/1,6566,050_ 20,00.html Artigossobredesenvolvimentodejogospara celulares www.jasonlam604.com/books.php Livro em PDF sobre jogos J2ME www.java.com/en/explore/mobile/games.jsp Seçãodositejava.com,comreferênciaspara exemplosdejogosemJavaparadispositivos móveis javamagazine.com.br/downloads/ jm10/jm10-vsabino-gameapi.zip Vanessa Cristina Sabino ([email protected] .br)écoordenadoradoGUJ (www.guj.com.br)eescreve umblogsobreJava(www.java. blogger.com.br).Écertificadapela Sun(SCJPeSCWCD)eprestaserviçoscomoconsultora e instrutora Java. Edição 10 • Java Magazine 47