Ponto V!

Home Java Java 2D A primeira animação
Vinícius Godoy de Mendonça
A primeira animaçãoImprimir
Escrito por Vinícius Godoy de Mendonça

No artigo passado, vimos como definir o loop principal de nosso jogo de maneira consistente. Neste, veremos como desenhar uma bolinha, que rebate se movimenta pela tela, rebatendo em seus cantos.

A classe Ball

Vamos começar pensando na própria bola. Como já falamos em artigos anteriores, a bola terá duas fases:

  • Atualização da sua lógica: Que faz a movimentação da bola, e testa se ela colidiu com os cantos da tela;
  • Pintura: O desenho da tela.

Analisemos então cada uma dessas fases.

A atualização da bola

A bola deve se movimentar numa velocidade constante. Como explicado no artigo Animação Baseada em Tempo, a fórmula física que define uma velocidade, seja ela angular ou linear é a seguinte:

velocidade = distância / tempo

Essa fórmula é bem natural. Dizemos que um carro se locomove a 60 quilômetros (distância) por hora (tempo). Também devemos definir uma velocidade para nossa bola, mas vamos adotar uma unidade mais conveniente que os km/h: os pixels / s.

Para utilizarmos a fórmula, seria necessário conhecermos o tempo entre dois updates. Esse calculo não é uma tarefa da bola (já que esse tempo deverá ser o mesmo para todos os sprites durante um update), portanto, iremos apenas recebê-lo como parâmetro.

E qual é a lógica de atualização da bola?

a) Somamos a velocidade do deslocamento lateral no eixo x;
b) Somamos a velocidade de deslocamento vertical no eixo y.

Se a bola colidir com um dos cantos da tela, invertemos a velocidade, e corrigimos a posição da bola (já que ela pode ter saído da tela).

Percebemos então que a bola tem os seguintes atributos:

  1. x: a posição no eixo x;
  2. y: A posição da bola no eixo y.
  3. vx: Uma velocidade no eixo x;
  4. vy: Uma velocidade no eixo y;
  5. SIZE: Um tamanho. Como é uma bola, a altura e a largura serão iguais;
  6. screenWidth: A informação da largura da tela
  7. screenHeight: A informação sobre a altura da tela.

A lógica do movimento da bola em código, fica assim:

@Override
public void update(long time) {
    //O tempo é em milis. Para obter em segundos, precisamos dividi-lo por 1000.        
    x += (time * vx) / 1000;
    y += (time * vy) / 1000;
    
    checkCollision();
}

Note que multiplicamos a velocidade pelo tempo transcorrido em segundos. Assim, se nossa velocidade é 20, mas se apenas meio segundo se passou, o deslocamento nesse update será de apenas 10. A divisão por 1000 ocorre porque o tempo na variável time é dado em milisegundos, não segundos.

E o método checkCollision? Vamos pensar em como funciona a colisão da bola. Existem apenas 4 pontos na tela onde a bola pode colidir, veja no diagrama:

Locais de possível colisão

Esses pontos são:

a) No lado esquerdo da tela, quando x <= 0;
b) No lado direito da tela, quando x, somado ao largura da bola, for maior que a largura da tela;
c) No topo da tela, quando y <= 0;
d) Na base da tela, quando y, somado a altura da bola, for maior que a altura da tela.

Quando essa colisão ocorrer, devemos reposicionar a bola no interior da tela, e então inverter a velocidade em x ou y, de acordo com o local onde a bola colidiu. Em código, temos o seguinte:

private void checkCollision()
{
    //Testamos se a bola saiu da tela
    //Se sair, recolocamos na tela e invertemos a velocidade do eixo
    //Isso fará a bola "quicar".        
    if (x < 0) { //Lateral esquerda
        vx = -vx;
        x = 0;
    } else if ((x+SIZE) > screenWidth) { //Lateral direita
        vx = -vx;
        x = screenWidth - SIZE;
    }
    
    if (y < 0) { //topo
        vy = -vy;
        y = 0;
    } else if (((y+SIZE) > screenHeight)) { //baixo
        vy = -vy;
        y = screenHeight - SIZE;
    }
}

E esta é a fase de atualização da bola.

A pintura da bola

Vamos apenas pintar uma bola vermelha na tela, usando antialiasing. O código é bem parecido com o que já vimos em artigos anteriores, vejam:

@Override
public void draw(Graphics2D g2d) {
    Graphics2D g = (Graphics2D) g2d.create();
    g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
    g.setColor(Color.RED);
    g.fill(new Ellipse2D.Float(x, y, SIZE, SIZE));
    g.dispose();
}

Ou seja, ele simplesmente pinta a bola em sua posição x e y.

Criando o local onde a bola será desenhada

Vamos criar um JFrame, que será a nossa área de pintura principal. É ele que conterá o loop principal, portanto, faremos que ele implemente a interface LoopSteps. No seu construtor, definimos o seu tamanho, a operação padrão de fechamento e adicionamos a ele um WindowAdapter, que fará o loop principal parar, caso o JFrame seja fechado.

public class BallFrame1 extends JFrame implements LoopSteps {
    private MainLoop loop = new MainLoop(this, 60);
    
    private long previous = System.currentTimeMillis();
    private Ball ball;
    
    public BallFrame1() {        
        super("Bouncing ball");
        setDefaultCloseOperation(EXIT_ON_CLOSE);        
        setSize(400, 400);  
setResizable(false); addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { //Se apertar o x, paramos o loop. loop.stop(); } }); }

Note que a inicialização da bola ainda não foi feita. Deixamos essa atividade para a fase de setup(). No caso dessa aplicação, não faria muita diferença em que fase usar. Mas o setup() ocorre após o frame visível, e geralmente no início de cada fase do jogo. Por isso, ele é o local ideal para inicializarmos nossos objetos do jogo.

Nosso método de setup cria uma nova bola, e fornece a ela o tamanho da área de pintura. Esse tamanho é igual ao tamanho do frame, descontado a largura da decoração de sua lateral (insets). Portanto, nosso método de setup fica assim:

@Override
public void setup() {
    //Subtrai a decoração da janela da largura e altura máximas
    //percorridas pela bola.
    ball = new Ball(getWidth() - getInsets().left - getInsets().right, 
            getHeight() - getInsets().top - getInsets().bottom);
}

No processamento da lógica, calcularemos quanto tempo transcorreu entre duas chamadas ao update. Note que na criação do JFrame definimos uma variável chamada time. Ela será usada para representar o momento em que o método update() foi chamado pela ultima vez..

Podemos então calcular o tempo o transcorrido subtraindo o tempo atual dessa variável. O resultado será em milissegundos. Em seguida, fazemos a atualização dos sprites do nosso jogo, e atualizamos a variável tempo, para que ela saiba o momento que nosso update terminou. Nosso método processLogics() fica assim:

@Override
public void processLogics() {
    //Calcula o tempo entre dois updates
    long time = System.currentTimeMillis() - previous;
    
    //Chama o update dos sprites, no caso, só a bola
    ball.update(time);
    
    //Grava o tempo na saída do método
    previous = System.currentTimeMillis();
}

E o método renderGraphics()? Ainda iremos utilizar a pintura da forma como vimos no Swing, ou seja, através do método paint. Portanto, esse método fica vazio. Veremos no próximo artigo uma técnica chamada pintura direta, que dispensa o método paint(), e então o renderGraphics() será utilizado.

Nosso método paint é muito parecido com o do fantasma do Pacman. Nele, criamos um contexto gráfico, definimos um novo contexto, relativo ao interior da janela, limpamos o fundo, e então chamamos a pintura de todos os sprites do nosso jogo:

@Override
public void paint(Graphics g) {        
    //Criamos um contexto gráfico que não leva em conta as bordas
    g = g.create(getInsets().right, 
               getInsets().top, 
               getWidth() - getInsets().left, 
               getHeight() - getInsets().bottom);
    //Limpamos a tela
    g.setColor(Color.BLACK);        
    g.fillRect(0, 0, getWidth(), getHeight());
if (ball != null) ball.draw((Graphics2D) g); //Desenhamos a bola g.dispose(); //Liberamos o contexto criado. }

Finalmente, o método paintScreen(), do nosso gameLoop(), apenas chama repaint(), para que o paint() seja chamado.

Iniciando o loop principal

Está tudo pronto para inicializarmos o loop principal da nossa animação. Para isso, vamos criar no JFrame um método chamado startMainLoop() responsável por essa tarefa:

public void startMainLoop()
{
    //Iniciamos o main loop
    new Thread(loop, "Main loop").start();
}

Então, no nosso método main, basta criamos o JFrame, deixa-lo visível e chamarmos o método startMainLoop():

public static void main(String[] args) {
    EventQueue.invokeLater(new Runnable() {
        @Override
        public void run() {
            BallFrame1 bf = new BallFrame1();
            bf.setVisible(true);
            bf.startMainLoop();                
        }
    });
}

Você pode baixar o código fonte de toda essa aplicação aqui. Deixei a velocidade no eixo y igual a metade dela no eixo x, para que a bola não fique simplesmente percorrendo a diagonal da tela, e faça um movimento mais interessante.

Na sua máquina, a bola poderá não se mover tão suavemente quanto deveria. Não fique muito nervoso! Ainda estamos usando a pintura controlada pelo swing, não por nós mesmos. O Swing está autorizado a ignorar alguns repaints, caso eles cheguem muito rapidamente, como é o nosso caso. E esse problema é especialmente sensível em JApplets. No próximo artigo, varemos como controlar essa pintura por nós mesmos.


Comentários (35)
  • Marco Biscaro  - Muito bom
    avatar

    Novamente, parabéns pelo belo tutorial.

    Só que tem uma coisa... na explicação da colisão da bola a coisa está meio confusa...

    A imagem está errada. Deveria ser:
    View image

    E o texto está bagunçado. Deveria ser:

    Esses pontos são:

    a) No topo da tela, quando y

  • Marco Biscaro  - Impossível
    avatar

    Esse sistema não gosta do "menor ou igual" ele simplesmente trunca o comentário só até essa parte. :0

    Apenas reveja aquele trecho com calma. :)

  • Vinícius Godoy de Mendonça  - Caramba
    avatar

    Realmente, eu devia estar dormindo ou bêbado quando fiz aquele trechinho. :0

    Ainda bem que você revisou, obrigado pelo comentário. Já está corrigido. :)

  • pedro  - frame
    avatar

    eu queria saber, se voce pode me ensinar como receber os campos digitados em uma frame(obs.: frame com radio, textfield, combox), para outra frame em uma tabela, e com o botao imprimir..imprimindo os dados digitados, tipo um cadastro

    eu ja tenho a frame criada e a outra frame com a tabela.

    valew...

  • Vinícius Godoy de Mendonça
    avatar

    Sim, mas não aqui. Poste sua dúvida no GUJ.

  • Rodrigo  - adicionar canvas em applet
    avatar

    olá pessoal...
    tô com uma dúvida aki: como faço pra adicionar uma canvas tipo essa com a animação das bolas dentro de um JDesktopPane que está dentro de um Applet?
    é que dentro do JDesktopPane vai ficar ums botões que eu vou implementar ações depois, e a tela que vai correr a animação vai ficar dentro do DesktopPane, desde já agradeço a ajuda, abraços.

  • Vinícius Godoy de Mendonça
    avatar

    Você deve então fazer um filho de JComponent, e implementar a lógica no método paintComponent.

    Depois, basta adicionar esse componente como um outro qualquer, dentro do layout manager do seu desktopane.

  • allan
    avatar

    Cara consegui fazer a animação, legal. Mas agora se eu quiser colocar uma bolinha controlável via teclado como devo fazer? Onde colocar o keylistiner? e o tempo que é usado para calcular o o processamento da pintura?

  • Vinícius Godoy de Mendonça
    avatar

    Sim, seria usando o KeyListener mesmo. Basta registra-lo no frame principal. Os eventos então atualizar a posição da bolinha.

    Dê uma olhada nesse tutorial do Coke and Code:
    http://www.cokeandcode.com/info/tut2d.html

  • Ytrio Salmito  - Método "setRenderingHint"
    avatar

    Fiquei com dúvida de qual a diferença de fazer uma animação usando "RenderingHint" ou não!. isso deixa a animação melhor ??!
    influência de que forma na exibição da imagem na tela ?!

  • Vinícius Godoy de Mendonça
    avatar

    Com os rendering hints você pode adicionar coisas como suavização de serrilhado (anti-aliasing), melhorias na forma de calcular as cores, algorítmos de mais qualidade para ampliar ou reduzir imagens e até formas melhores de desenhar textos.

    Tudo isso melhora a qualidade da animação, porém, aumenta o custo de processamento.

  • Ytrio Salmito
    avatar

    Valeu vinícios Godoy (;
    é que eu tou fazendo um jogo utilizando o JAVA2D ai quero fazer com o melhor design possível.
    Outra dúvida,como eu faço pra o JAVA ignorar o fundo de um imagem ??.Precisa coloca o fundo como transparênte o a uma forma de lida com a imagem ela com o fundo de qualquer cor ??!

  • Vinícius Godoy de Mendonça
    avatar

    É melhor desenhar o fundo como transparente mesmo. A linguagem não tem um suporte muito fácil a Color Keying (uma cor específica para transparência).

  • Daybson Paisante  - Transformação de Sprites Animados
    avatar

    Gostaria de saber se podem me ajudar em um problema:

    Possuo um sprite sheet animado, o qual exibo apenas um frame por vez.
    Desejo aplicar transformações sobre a imagem, mas o

    Código:
    Graphics2D

    não possui um método

    Código:
    draw()

    que aceite como argumentos apenas uma área da imagem E um objeto AffineTransform.

    A sobrecarga do método

    Código:
    draw()

    que aceita isso permite apenas desenhar todo o sprite sheet, e a sobrecarga que aceita desenhar uma área do sprite sheet não aceita um

    Código:
    AffineTransform

    .

    Como contornar isso?

  • ViniGodoy
    avatar

    Você chama dois métodos. Primeiro apenas o setTransform, que vai alterar a transformação, e depois o drawImage, que vai desenhar a imagem usando a transformação previamente definida.

  • Daybson Paisante
    avatar

    OK Vinícius! Funcionou bem! Obrigado!!!

    Agora uma dúvida:

    Meu sprite possui posições (x, y) e uma velocidade de descolamento (x, y). Qual a melhor maneira de locomovê-lo pela tela?

    Até hoje tenho incrementado sua posiçao(x, y) com a velocidade (x, y), sem aplicar um translate(velX(), velY()), por exemplo.

    Qual a diferença entre locomovê-lo "brutamente" como tenho feito e por translações?

    Grato!

  • Vinícius Godoy de Mendonça
    avatar

    As transformações tem as seguintes vantagens:
    - Permitem representar movimentos relativos (por exemplo, uma torre que gira em cima de um tanque);
    - Facilitam a implementação de uma câmera no jogo, inclusive com recursos como girar e dar zoom;
    - Permitem que o jogo possa ser rescalonado facilmente.

    Independente de ter ou não a transformação, você ainda terá variáveis no Sprite para representar sua posição. As transformações são um recurso de desenho, não da lógica do jogo.

  • Daybson Paisante
    avatar

    Mas Vinícius, se eu movimentar meu Sprite apenas por translações, e usar um getTranslaceX() para atualizar minha posição double x, eu não posso realizar outra transformação, pois está alterando completamente o valor retornado de getTranslaceX().

    Como faria para aplicar translate, rotate, shear e scale ao mesmo tempo, e depois atualizar a posição de (x, y) por getTranslaceX() ..Y() de forma confiável, por exemplo ?

  • Ricardo Lima
    avatar

    Estou com dificuldade em fazer o JFrame se ajustar ao tamanho correto levando em conta os Insets, pelo fato deles mudarem de SO para SO.

    Pois se eu chamo o getInsets() pra fazer esse ajuste dentro do construtor no método setSize() do JFrame, o getInsets() retorna 0 independente que seja left, right, top ou bottom...

    Eu quero deixar de forma que eu deixe a tela no tamanho de 400 x 400 por exemplo e o JFrame seja construido ja se ajustando com os insets sem que eu precise digitar manualmente a tamanho da tela + os insets manualmente.

  • Daniel  - Dúvida
    avatar

    Boa noite Vinicíus! O acompanho desde o GUJ e fico feliz por ver que suas publicações estão fazendo sucesso! Gosto muito de seus tutoriais tanto que estou utilizando-os como referência em parte de meu tcc, mais especificamente, a parte de animação. A propósito, me deparei com uma duvida gostaria de tirá-la se possivel. Estou com um frame principal que contém barra de menu, barra de ferramentas e um splitpane composto por um desktopPane a direita e um painel na lateral esquerda da tela. Este desktopPane é onde ficará minha animação, porém quando inicio a animação que fiz baseada na de seu tutorial ela simplesmente retira todo o conjunto de paineis do frame ficando somente a animação visivel. Será que tem como eu executar a animação apenas no espaço desejado do frame, mantendo os demais componentes intáctos, ou seja, a barra de menu e de ferramentas? Isso está ocorrendo pq estou usando o método paint ao invez do paintComponent?

    Desde já obrigado vini e parabéns!

  • ViniGodoy
    avatar

    Oi.

    Se você sobrescreveu o JFrame, com certeza. Se quer uma animação apenas numa porção da tela, faça o seguinte:
    a) Crie um filho da classe JComponent;
    b) Sobrescreva o método paintComponent no lugar do paint;
    c) Sobrescreva o getPreferredSize() para retornar o tamanho que esse componente tem que ter preferencialmente;
    d) Adicione esse componente no local desejado;

  • Daniel  - Dúvida animação em parte da tela
    avatar

    Boa noite Vini! Primeiramente quero agradecer pelos esclarecimentos, fiz as devidas correção, porém surgiu uma dúvida: após a criação da classe filha JComponent, teu método paintComponent e o getPreferreSize() sobrescritos a etapa de adicionar em meu JDesktopPane é feita no método renderGraphics()? Caso afirmativo, preciso fazer uso dos comandos removeAll(), validate() para remoção do estado anterior ou o simples fato de se estanciar a classe filha JComponent() para meu DesktopPane fará a atualização do painel com o efeito de animação como o demonstrado em teu tutorial?

    Mais uma vez, agradeço!

    Daniel

  • Vinícius Godoy
    avatar

    Primeiramente, é necessário esclarecer uma coisa: O JDesktopPane é um container apenas de JInternalFrames. Ele não suporta que componentes sejam diretamente colocados sobre ele. Se você estiver fazendo isso, certamente terá problemas.

    Agora, assumindo que você fez um JInternalFrame e é lá que você desenha inserir seu JComponent, o painel será adicionado pelo método add do JInternalFrame, como você faria com qualquer outro componente do Swing.

  • Daniel
    avatar

    Boa tarde vini. Agradeço pela observação, pois me alertou para um erro importante em meu programa para o tcc. Como não pretendo usar nenhuma janela interna, apenas um frame principal e alguns compontes sobre ele, então não justifica o uso do JDesktopPane. Já o subistituir por um JPanel, obrigado mesmo! Bom, contudo ainda não consegui fazer com que a animação seja executada corretamente. Esse é meu primeiro contato com animação, então estou caminhando um passo por vez kkk! Inicialmente, estou tentando fazer com que esta tua animação do tutorial ocorra dentro de um espaço restrito, no caso um JPanel para que com a compreensão desta eu prossiga com as animações em meu trabalho. Bom, após seguir suas instruções, a situação está melhorando, já consigo inserir o desenho no local adequado, porém a animação não ocorre, o que dá para notar é que o fame fica piscando sem nenhuma alteração do circulo. Será que você pode me ajudar a identificar o problema?

  • Lucas Santos  - Override
    avatar

    Sei que o artigo é meio antigo, mas o único método implementável aqui é o draw, a interface LoopSteps também não existe, foi voce que a criou? e a classe MainLoop também? pq ela não existe aqui também não, da pra da uma força ae?

  • Vinicius Godoy
    avatar

    Leia o artigo anterior, que explica como montar o loop de animação.

  • Lucas Santos  - override
    avatar

    sim sim, já vi, caramba você tem muito conhecimento com java, você usa e abusa dos melhores recursos que a linguagem tem...
    muito bom. :)

  • Anônimo  - colisão de bolhas
    avatar

    como faz para uma bolinha colidir com a outra?

  • ViniGodoy
    avatar

    É bem simples. Basta testar se a distância entre o centro de duas bolinhas é menor do que a soma de seus raios.

    No caso do Java, a classe Ellipse2D possui o método intersects, que faz automaticamente esse teste.

  • Bruno  - Intersecção
    avatar

    Quando tenho 2 bolas e quero usar o método intersects ele não permite. Não tenho maneira de ver se duas Ellipse2D se intersectam... como resolvo este problema?

  • Júnior Guedes  - Duvida
    avatar

    Como fazer para inserir mais de uma bolinha na tela?? Tentei criando várias instâncias da classe ball dentro de uma array, mas no meu caso me parece que só aparece na tela a primeira bola inserida..

  • Gustavo
    avatar

    É mais dificil do que parece, provavelmente suas bolinhas estao uma sobre a outra. Tente mudar a direção quando a bolinha é criada.

  • Júnior Guedes  - Complemento da pergunta anterior
    avatar

    P.S.: estou usando um BufferedImage para desenhar uma bola na tela.

  • Gustavo  - MouseEvent
    avatar

    Como fazer com que a bolinha inicie com um clique do mouse na tela, e a cada clicada no mouse apareça outra bolinha? kkk to ferrado...

  • gezuis  - re: Duvida
    avatar
    Júnior Guedes Escreveu:
    Como fazer para inserir mais de uma bolinha na tela?? Tentei criando várias instâncias da classe ball dentro de uma array, mas no meu caso me parece que só aparece na tela a primeira bola inserida..


    Cade seu code?

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