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 (7)
  • 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.

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