Simplicidade e poder em jogos para celu lares para celu

Propaganda
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
Download