Ponto V!

Home Java Java 2D O fantasma do Pacman
Vinícius Godoy de Mendonça
O fantasma do PacmanImprimir
Escrito por Vinícius Godoy de Mendonça

Nesse artigo, vamos resolver o desafio proposto no artigo anterior, que era de desenhar o fantasma do Pacman. E aproveitaremos para explicar diferentes técnicas comuns de pintura.

A classe Ghost

Vamos criar uma nova classe, chamada Ghost, que conterá as instruções de pintura do nosso fantasma. O primeiro passo, naturalmente, é colocar um método paint nessa classe. A pintura será dividida em duas etapas:

a) A pintura do corpo, usando um GeneralPath;
b) A pintura dos olhos, usando Elipses.

Portanto, nosso método paint pode ficar ridiculamente simples, restringindo-se à:

public void paint(Graphics g) {    
   drawBody((Graphics2D) g);
   drawEyes((Graphics2D) g);
}

E onde fica a cópia do contexto gráfico? Bem, vamos garantir que nem drawBody, e nem drawEyes alterem esse contexto.

Definindo a forma do corpo

Vamos desenhar o corpo do fantasminha. Começamos criando um novo GeneralPath, e usando o método moveTo() para posicioná-lo no ponto inicial de pintura. Iniciará pela lateral esquerda, próximo a cabeça, indo em direção a base do fantasma. Desenharemos ali uma linha reta. Iniciaremos na posição (0,35). Baixamos um pouco em y, pois é necessário ter espaço para a cabeça. Desceremos até a posição (0,80), já que também é bom sobrar um espaço para os “pés”. O fantasma terá dimensão aproximada de (80,100).

GeneralPath gp = new GeneralPath();
        
//Lateral esquerda
gp.moveTo(0, 35);
gp.lineTo(0, 80);

O próximo passo é pintar a base. Ela é ondulada, portanto, usaremos curvas quadráticas para desenhá-la. Desenharemos uma curva para baixo, de largura 20, outra para cima de largura 10. O ponto de controle será exatamente no centro das curvas, e tem altura 15 para a curva para baixo, e 5 para a curva para cima. Lembre-se, o primeiro parâmetro é o ponto de controle.

gp.quadTo(15, 95, 20, 80);
gp.quadTo(25, 75, 30, 80);
gp.quadTo(40, 95, 50, 80);
gp.quadTo(55, 75, 60, 80);
gp.quadTo(70, 95, 80, 80);

O esquema abaixo mostra os dois primeiros pontos (ampliados em quatro vezes), note que a primeira coordenada é onde o lineTo terminou:

base

Desenhamos novamente a lateral direita, agora, simplesmente traçando uma linha até a posição 80, 35. Assim, ela fica no mesmo tamanho da linha da direita. Então, desenhamos o topo.

Não faremos a cabeça do fantasma como uma meia circunferência. Vamos deixá-la ligeiramente mais inclinada para o lado que o fantasma olha, no nosso caso, o esquerdo. Para isso, usaremos uma curva cúbica ao invés de uma curva quadrática. O desenho da curva é interpolado entre os dois pontos de controle, portanto, a curva dificilmente os intercepta. Por isso, a posição dos pontos de controle ficará um pouco fora do espaço coordenado em y.

//Lateral direita
gp.lineTo(80, 35);
        
//Topo
gp.curveTo(70, -5, 8, -5, 0, 30);

O esquema abaixo mostra a curva do topo. Note que, embora os pontos de controle estejam antes da coordenada 0, a curva ainda estará dentro do espaço coordenado positivo:

topo

Finalmente, chamamos closePath(). Esse comando não seria estritamente necessário, já que terminamos o desenho no exato local em que começamos, mas como queremos garantir uma figura fechada, é bom chamá-lo, nem que seja por motivos de documentação.

Pintando o corpo

Agora que já definimos o desenho do corpo, chega a hora de fazer a pintura propriamente dita. Para dar um efeito arredondado, usaremos uma pintura gradual, a partir de uma cor selecionada pelo usuário (por padrão vermelho), até um tom um pouco mais escuro dessa mesma cor. Criaremos a cor como uma propriedade da classe Ghost (simplesmente adicione a propriedade color e os seus gets e sets).

Para a pintura, usaremos o GradientPaint. Se você seguiu nossos tutoriais de Java2D até aqui, isso deve ligar um alerta: o GradientPaint irá alterar o contexto gráfico, portanto, é hora de chamar o createGraphics(). Nesse caso, não farei isso. Apenas para demonstrar outra forma de manter o contexto gráfico inalterado, vamos apenas guardar uma referência ao método de Paint antigo numa variável, e restaurá-lo ao final.

O GradientPaint exige 2 pontos e duas cores. Vamos definir esses pontos no interior do fantasma, com uma margem de 10. Isso garantirá que o degradée não iniciará e nem terminará exatamente nas bordas. Em seguida, usamos o método fill para desenhar o path previamente estabelecido e, ao final da pintura, restauramos o Paint original.

Paint oldPaint = g2d.getPaint(); 
        
GradientPaint paint = new GradientPaint(
   0, 40, color,            //Da cor definida pelo usuário 
   70, 40, color.darker());   //Até um tom um pouco mais escuro
g2d.setPaint(paint);
        
//Pintura do fantasma            
g2d.fill(gp);

O esquema abaixo, mostra essa etapa:

gradientpaint

Pintando os olhos do fantasma

No método anterior, usamos valores absolutos para desenhar o corpo fantasma. Com essa técnica, fornecemos exatamente as coordenadas onde queríamos cada pedaço do corpo, baseando sempre na posição (0,0). Usaremos uma técnica um pouco diferente para os olhos. Faremos inicialmente um método de pintura para as elipses dos olhos na posição (0,0), como se fossemos pintá-los apenas uma única vez, fora do corpo do fantasma. Depois, alteraremos o sistema de coordenadas e chamaremos esse método duas vezes, posicionando assim os olhos nos locais desejados.

O primeiro passo é definir dois objetos do tipo Ellipse2D para representar a parte branca e azul do olho. Como nosso olho se baseará na posição 0,0, esses objetos podem ser constantes em nossa classe:

private static final Ellipse2D eyeWhite = new Ellipse2D.Float(0, 0, 20, 25);
private static final Ellipse2D eyeBall = new Ellipse2D.Float(0, 5, 8, 15);

Em seguida, fazemos um método que simplesmente pinta esse olho. Aqui, alteraremos a cor do contexto gráfico, mas não faremos sua cópia. Deixaremos essa tarefa para o método que pintará os dois olhos.

private void drawEye(Graphics2D g2d) {        
   g2d.setColor(Color.WHITE);        
   g2d.fill(eyeWhite);
   g2d.setColor(Color.BLUE);
   g2d.fill(eyeBall);
}

Agora, é hora de usarmos o comando translate. O que ele faz? Ele altera o sistema de coordenadas antigo. Assim, antes de chamarmos o método drawEye, diremos que a posição (10, 25) é a nossa nova posição (0,0). Dessa forma, quando o drawEye tentar pintar o olho em (0,0), estará pintando, na realidade, em 10,25. Em seguida, afastaremos o sistema de coordenadas mais 25 pixels para a direita, para pintar o segundo olho.

private void drawEyes(Graphics2D g)
{                
    Graphics2D g2d = (Graphics2D) g.create();
        
   //Afastamos nosso sistema de coordenadas (em 0,0) 
   //para a posição 10,25 e pintamos o olho
   g2d.translate(10, 25);
   drawEye(g2d);
        
   //Afastamos mais 25 pixels em x
   //Ou seja, a posição (0,0) agora é (35,35).
   //Pintamos o olho novamente.
   g2d.translate(25, 0);
   drawEye(g2d);
        
   g2d.dispose();
}

Além do método translate, também encontramos os métodos rotate, scale e shear, para fazer transformações de coordenadas. Ou podemos definir uma matriz de transformação diretamente, através do método setTransform(), passando a ele um AffineTransform.

Poderíamos pintar os dois olhos diretamente, como fizemos com o corpo? Certamente.

Entretanto, essa forma evita duplicação de código, e, mais importante do que isso, mostra como fazemos para gerar figuras independente de sistemas coordenados. Com ela, podemos criar bancos de imagens, e só usar os métodos translate para as posicionar no local final, dentro do jogo. Os métodos de pintura ficam mais simples, já que não precisam calcular offsets em x e y, que por padrão, serão sempre baseados na origem (0,0).

Note que, como nosso fantasma também foi desenhado com base num sistema em (0,0), poderemos usar essa mesma técnica para posiciona-lo no tabuleiro do jogo de pacman.

O frame

Iremos desenhar o fantasma num JFrame padrão. Existe apenas um detalhe a ser tratado aqui, no método paint. A coordenada (0,0) do JFrame refere-se ao canto superior esquerdo, sem qualquer pintura ou decoração. Ou seja, se pintarmos lá diretamente, parte do nosso desenho ficará embaixo a borda do JFrame. Felizmente as classes de janelas fornecem um método chamado getInsets(). Ele fornece as dimensões da decoração da janela. Isso nos permite usar o método create() da classe Graphics para criar um contexto gráfico cujas coordenadas caiam exatamente sobre a área pintável do JFrame:

public void paint(Graphics g) {    
   //Criamos um contexto gráfico com a área de pintura restrita
   //ao interior da janela.
   Graphics2D clip = (Graphics2D) g.create(getInsets().left, 
      getInsets().top, 
      getWidth() - getInsets().right, 
      getHeight() - getInsets().bottom);
        
   //Pintamos o fundo do frame de preto
   clip.setColor(Color.BLACK);
   clip.fill(clip.getClipBounds());
        
   //Pintamos o fantasma
   ghost.paint(clip);    
   clip.dispose();
}

O esquema abaixo demonstra o que foi feito. Destaquei nele as dimensões indicadas pelos insets e, em pontilhado, deixei a nova área de pintura calculada:

insets

Note que novamente, alteramos o sistema de coordenadas básico. A única diferença aqui é que também definimos a área de clipping, ou seja, as coordenadas máximas e mínimas que podem ser desenhadas. Qualquer coisa pintada fora desses pontos será sumariamente descartada pelo Java 2D.

Retoques finais

Se você conseguiu exibir o código até aqui, verá que a forma do fantasma ficou serrilhada. Como último retoque, vamos pedir para o Java2D fazer anti-aliasing. Essa é uma técnica que mistura gradualmente a cor das bordas com a do fundo, eliminando a aparência serrilhada e adicionando qualidade ao desenho. Use o anti-alias com cuidado, pois ele pode tornar a pintura um processo bastante caro em termos de processamento.

Alteraremos o método paint, da classe ghost, fornecendo ao contexto gráfico um Rendering Hint. Como o nome indica, as “dicas” de desenho representam um pedido ao Java 2D, que ele pode atender ou não. A decisão final dependerá da capacidade do hardware de vídeo. O novo método paint da classe ghost fica assim:

public void paint(Graphics g) {    
   Graphics2D g2d = (Graphics2D) g.create();
   g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        
   drawBody(g2d);            
   drawEyes(g2d);
        
   g2d.dispose();
}

Como resultado final, temos o seguinte fantasminha:

blinky

Conclusões finais

Vimos nesse artigo como usar o Java2D para desenhar o fantasma do Pacman. Conhecemos as técnicas de pintura direta, usada no corpo, e pintura relativa. Vimos como definir uma área de desenho restrita ao interior da janela de pintura, ao invés de usar a área padrão, que inclui as bordas e a decoração.

Você também pode observar que usamos o método create() da classe Graphics sem piedade. Esse método é muito eficiente, e é muito mais fácil entender um programa de pintura, se mantivermos o contexto gráfico inalterado. Entretanto, exploramos uma forma de manter o contexto sem necessariamente usar o método create() que, embora muito mais trabalhosa, também é válida.

Por fim, vimos como adicionar qualidade ao desenho final através da técnica de anti-aliasing.

Você pode baixar o código fonte da solução final aqui.


Comentários (18)
  • José Ricardo  - Desenho ou Sprites
    avatar

    Fala Vinícius.

    Uma dúvida: Quais as vantagens/desvantagens entre desenhar na mão e usar sprites?

    Valeu e parabéns pelo post

    José

  • Vinícius Godoy de Mendonça  - Vantagens e desvantagens
    avatar

    Na mão você pode fazer um programa ridiculamente pequeno, o que pode ser bastante vantajoso para quem vai fazer um download. Também é praticamente impossível copiarem os arquivos de recursos do seu jogo. Finalmente, as imagens podem ser redimensionadas à vontade, sem que haja perda de qualidade.

    A desvantagem é que na mão exige muito mais processamento.

    No caso de usar sprites prontos, a vantagem é que existem diversos editores prontos, com recursos gráficos impressionantes. E os artistas sabem usa-los.

    As imagens exigem espaço em disco, mas são rápidas e leves de serem desenhadas.

    Aí, vai de você saber o quão importante é ter um aplicativo pequeno ou não. Se for um jogo para desktop, isso dificilmente será relevante. Mas na web, você pode querer minimizar o tempo de download.

    Vamos ver em artigos futuros que com o Java2D é possível usar uma abordagem híbrida. Você desenhar na mão uma image, e usa-la como sprite em seguida. Essa abordagem tenta unir o melhor dos dois mundos.

  • Vânio Meurer  - ouch...
    avatar

    Opa, fiquei com vergonha do meu fantasminha agora! kkk
    Muito bom o post, não conhecia o antialiasing e o translate da g2d, show!

  • Vinícius Godoy de Mendonça
    avatar

    Nem precisa ter vergonha, você cumpriu o desafio numa boa. O objetivo do exercício era só praticar Java2D, não fazer um fantasma bonito.

    Mas achei que só resolver o exercício, simplesmente, seria desperdiçar o espaço de um bom artigo e o tempo de um leitor dedicado.

    Por isso, aproveitei para explicar as técnicas de desenho mais comuns, como o anti-alias, o Gradient Paint e as transformações de coordenadas, que ficaram de fora (ou em segundo plano) nos artigos anteriores.

  • Bruno Daniel Marinho  - Irado!
    avatar

    Ficou muito da ora o fantasma parece jpg, muita pratica eim.

  • Anônimo
    avatar

    Olá,


    Primeiramente parabéns pelo post, adorei a idéia de utilizar Java2D para criar os personagens de uma jogo, agora fiquei curioso em saber se isso é comum para jogos, e principalmente se tem algum jogo que use essa técnica, adoraria jogar para ver como é o desempenho.

    Abçs

    Marcelo.

  • Marcelo Souza  - Muito interessante mesmo
    avatar

    Olá,


    Primeiramente parabéns pelo post, adorei a idéia de utilizar Java2D para criar os personagens de uma jogo, agora fiquei curioso em saber se isso é comum para jogos, e principalmente se tem algum jogo que use essa técnica, adoraria jogar para ver como é o desempenho.

    Abçs

    Marcelo.

  • Vinícius Godoy de Mendonça
    avatar

    Depende muito do jogo, mas não é assim tão comum.

    Você pode usar o java 2D para desenhar arquivos com a extensão .svg, que podem ser facilmente transformados em comandos de pintura.

  • Marcelo Souza  - Fiquei muito curioso
    avatar

    Eu estava pensando que utilizando código para desenhar os personagens poe-se criar personagens (Objeto), e passar esse objeto como parâmetro para o construtor de uma classe e aí aplicar métodos que movimentassem esse personagem, mas acho que só daria pra criar um personagem que se movimentasse de um lado para o outro.
    Se criassemos um boneco ele andaria de um lado para o outro, mas não teria como fazê-lo movimentar as pernas, por exemplo.
    Sei que viajo as vezes, mas gostei bastante da idéia.

  • Vinícius Godoy de Mendonça
    avatar

    Daria sim, não seria exatamente fácil, mas daria.

  • Ricardo Lima  - Muito bom!
    avatar

    Olá, estou lendo as aulas sobre Java2D, e até agora estou gostando muito mesmo!

    Só uma dúvida quanto a criação da JFrame:
    Devido a janela encobrir parte da área "desenhável", durante a criação do meu frame, eu tenho que levar em conta esses valores a mais correto?

    Digamos que eu queira uma área desenhável de 320x320, eu preciso entao criar um JFrame de 328x347 (320 + 4 da esquerda e 4 da direita por 320 + 23 do topo e 4 da base)?

    Ou...

    Seria mais correto criar um JPanel de 320x320 e inserir dentro do JFrame este que se espande automaticamente para nao encobrir o JPanel?

    Qual seria a maneira padrão pra criação da área de pintura pra ser usada nos jogos?

  • ViniGodoy
    avatar

    O ideal é criar o JFrame maior, colocando na conta os insets. Quando usamos Java2D para jogos, não usamos outros componentes além da janela principal.

  • Ricardo Lima
    avatar

    Obrigado, a princípio eu tinha pensado dessa forma, mas achei que poderia ser um tipo de gambiarra hahaha.

    Eu consegui fazer o meu fantasminha, mas fiz ele no formato de pixel art mesmo (pra testar), deu um trabalhinho, mas achei que ficou bonito =)

    http://i.imgur.com/LbkJEZ0.png

  • ViniGodoy
    avatar

    Ficou legal. :)

  • samuel gomes de sá  - informações
    avatar

    cara é muito legal ! mas eu queria entender como é que eu implemento na prática o código entendeu ?

  • ViniGodoy
    avatar

    Leia o artigo e vê. No final, tem até o link para baixar o código inteiro.

  • Victor Fortunato  - curva de noventa graus
    avatar

    Muito legal mesmo, você já me ajudou muito, e muito obrigado! Agora eu queria saber como que faz para uma curva ser um ângulo reto. Uma curva de 90º como se fosse um "L". E também se tinha como fazer ao menos 2 curvas dessas em uma mesma linha. Obrigado!

  • Fasto  - Linhas curvas
    avatar

    Boa Tarde ViniGodoy, estou com dificuldade em fazer linhas curvas usando a class CubicCurve, não consigo compriender os valores que são inserido.

    Podes fazer um exemplo implementando o grafico do senx

Escrever um comentário
Your Contact Details:
Gravatar enabled
Comentário:
[b] [i] [u] [url] [quote] [code] [img]   
:angry::0:confused::cheer:B):evil::silly::dry::lol::kiss::D:pinch::(:shock:
:X:side::):P:unsure::woohoo::huh::whistle:;):S:!::?::idea::arrow:
Security
Por favor coloque o código anti-spam que você lê na imagem.
LAST_UPDATED2  

Busca

Linguagens

Twitter