La animacion basicamente consiste en una rapida secuencia de imagenes que crean la ilusion de movimiento. Como ejemplo animaremos una estrellita en nuestro lienzo de tres maneras diferentes, para analizar el funcionamiento de:
- Los timer Swing
- Los timer standard
- Los Thread (hilos de ejecucion)
Swing timer
En el primer ejemplo usaremos el Timer Swing para crear la animacion. Este es el metodo mas sencillo, pero tambien el menos eficaz para animar objetos en java.
Esta es la clase principal para el codigo de ejemplo:
SwingTimerEx.java
package com.codbas; import java.awt.EventQueue; import javax.swing.JFrame; public class SwingTimerEx extends JFrame { public SwingTimerEx() { initUI(); } private void initUI() { add(new Lienzo()); setResizable(false); pack(); setTitle("Estrellita"); setLocationRelativeTo(null); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); } public static void main(String[] args) { EventQueue.invokeLater(() -> { SwingTimerEx ex = new SwingTimerEx(); ex.setVisible(true); }); } }
SetResizable () establece si se puede cambiar el tamaño del marco.
El método pack () hace que el tamaño de esta ventana se ajuste al tamaño y diseños preferidos de sus elementos secundarios. Observar que el orden en que se llaman estos dos métodos es importante. SetResizable () cambia las inserciones del marco en algunas plataformas; llamar a este método después del método pack () podría conducir a resultados incorrectos (la estrella no entraría con precisión en el borde inferior derecho de la ventana).
En la clase Lienzo moveremos la estrellita desde la esquina superior izquierda a la esquina inferior derecha.
Lienzo.java
package com.codbas; import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Image; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import javax.swing.ImageIcon; import javax.swing.JPanel; import javax.swing.Timer; public class Lienzo extends JPanel implements ActionListener { private final int B_WIDTH = 350; private final int B_HEIGHT = 350; private final int INITIAL_X = -40; private final int INITIAL_Y = -40; private final int DELAY = 25; private Image star; private Timer timer; private int x, y; public Lienzo() { initLienzo(); } private void loadImage() { ImageIcon ii = new ImageIcon("src/resources/star.png"); star = ii.getImage(); } private void initLienzo() { setBackground(Color.BLACK); setPreferredSize(new Dimension(B_WIDTH, B_HEIGHT)); loadImage(); x = INITIAL_X; y = INITIAL_Y; timer = new Timer(DELAY, this); timer.start(); } @Override public void paintComponent(Graphics g) { super.paintComponent(g); drawStar(g); } private void drawStar(Graphics g) { g.drawImage(star, x, y, this); Toolkit.getDefaultToolkit().sync(); } @Override public void actionPerformed(ActionEvent e) { x += 1; y += 1; if (y > B_HEIGHT) { y = INITIAL_Y; x = INITIAL_X; } repaint(); } }
Se definen cinco constantes. Las dos primeras constantes son el ancho y la altura del lienzo. El tercero y el cuarto son las coordenadas iniciales de la estrella. El último determina la velocidad de la animación.
private final int B_WIDTH = 350; private final int B_HEIGHT = 350; private final int INITIAL_X = -40; private final int INITIAL_Y = -40; private final int DELAY = 25;
En el método loadImage() creamos una instancia de la clase ImageIcon. La imagen se encuentra en el directorio de los recursos del proyecto. El método getImage() devolverá el objeto Image de esta clase. Este objeto se dibujará en el lienzo.
private void loadImage() { ImageIcon ii = new ImageIcon("src/resources/star.png"); star = ii.getImage(); }
Aquí creamos una clase Swing Timer y llamamos a su método start(). Cada DELAY ms el temporizador llamará al método actionPerformed(). Para usar el método actionPerformed(), debemos implementar la interfaz ActionListener.
timer = new Timer(DELAY, this); timer.start();
La accion de dibujar el sprite se realiza en el método paintComponent(). Observar que también llamamos al método paintComponent() de la clase padre. La pintura real se delega al método drawStar().
@Override public void paintComponent(Graphics g) { super.paintComponent(g); drawStar(g); }
En el método drawStar(), dibujamos la imagen en la ventana con el uso del método drawImage().
Toolkit.getDefaultToolkit(). Sync() sincroniza la pintura en sistemas que almacenan eventos gráficos. ¡Sin esta línea, la animación podría no ser fluida en Linux!.
private void drawStar(Graphics g) { g.drawImage(star, x, y, this); Toolkit.getDefaultToolkit().sync(); }
El temporizador llama repetidamente al método actionPerformed(). Dentro del método, aumentamos los valores x e y del objeto estrella. Luego llamamos al método repaint() que hará que se llame a paintComponent().
De esta manera, pintamos regularmente el tablero y hacemos la animación.
@Override public void actionPerformed(ActionEvent e) { x += 1; y += 1; if (y > B_HEIGHT) { y = INITIAL_Y; x = INITIAL_X; } repaint(); }
Utility timer
Este metodo es muy similar al anterior. Usamos java.util.Timer en lugar de javax.Swing.Timer. Para los juegos Java en Swing, esta forma es más precisa. Esta es la clase principal:package com.codbas; import java.awt.EventQueue; import javax.swing.JFrame; public class UtilityTimerEx extends JFrame { public UtilityTimerEx() { initUI(); } private void initUI() { add(new Lienzo()); setResizable(false); pack(); setTitle("Estrellita"); setLocationRelativeTo(null); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); } public static void main(String[] args) { EventQueue.invokeLater(() -> { JFrame ex = new UtilityTimerEx(); ex.setVisible(true); }); } }
En este ejemplo, el temporizador llamará regularmente al método run() de la clase ScheduleTask.
package com.codbas; import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Image; import java.awt.Toolkit; import java.util.Timer; import java.util.TimerTask; import javax.swing.ImageIcon; import javax.swing.JPanel; public class Lienzo extends JPanel { private final int B_WIDTH = 350; private final int B_HEIGHT = 350; private final int INITIAL_X = -40; private final int INITIAL_Y = -40; private final int INITIAL_DELAY = 100; private final int PERIOD_INTERVAL = 10; private Image star; private Timer timer; private int x, y; public Lienzo() { initLienzo(); } private void loadImage() { ImageIcon ii = new ImageIcon("src/resources/star.png"); star = ii.getImage(); } private void initLienzo() { setBackground(Color.BLACK); setPreferredSize(new Dimension(B_WIDTH, B_HEIGHT)); loadImage(); x = INITIAL_X; y = INITIAL_Y; timer = new Timer(); timer.scheduleAtFixedRate(new ScheduleTask(), INITIAL_DELAY, PERIOD_INTERVAL); } @Override public void paintComponent(Graphics g) { super.paintComponent(g); drawStar(g); } private void drawStar(Graphics g) { g.drawImage(star, x, y, this); Toolkit.getDefaultToolkit().sync(); } private class ScheduleTask extends TimerTask { @Override public void run() { x += 1; y += 1; if (y > B_HEIGHT) { y = INITIAL_Y; x = INITIAL_X; } repaint(); } } }
Aquí creamos un temporizador y programamos una tarea con un intervalo específico. Hay un retraso inicial.
timer = new Timer(); timer.scheduleAtFixedRate(new ScheduleTask(), INITIAL_DELAY, PERIOD_INTERVAL);
Cada 10 ms, el temporizador llamará a este método run().
@Override public void run() { ... }
Thread
Animar objetos usando un hilo es la forma más efectiva y precisa de animación.Esta es la clase principal:
package com.codbas; import java.awt.EventQueue; import javax.swing.JFrame; public class ThreadAnimationEx extends JFrame { public ThreadAnimationEx() { initUI(); } private void initUI() { add(new Lienzo()); setResizable(false); pack(); setTitle("Estrellita"); setLocationRelativeTo(null); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); } public static void main(String[] args) { EventQueue.invokeLater(() -> { JFrame ex = new ThreadAnimationEx(); ex.setVisible(true); }); } }
En los ejemplos anteriores, ejecutamos una tarea a intervalos específicos. En este ejemplo, la animación tendrá lugar dentro de un hilo. El método run() se llama solo una vez. Es por eso que tenemos un ciclo while en el método. A partir de este método, llamamos a los métodos cycle() y repaint().
package com.codbas; import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Image; import java.awt.Toolkit; import javax.swing.ImageIcon; import javax.swing.JOptionPane; import javax.swing.JPanel; public class Lienzo extends JPanel implements Runnable { private final int B_WIDTH = 350; private final int B_HEIGHT = 350; private final int INITIAL_X = -40; private final int INITIAL_Y = -40; private final int DELAY = 10; private Image star; private Thread animator; private int x, y; public Lienzo() { initLienzo(); } private void loadImage() { ImageIcon ii = new ImageIcon("src/resources/star.png"); star = ii.getImage(); } private void initLienzo() { setBackground(Color.BLACK); setPreferredSize(new Dimension(B_WIDTH, B_HEIGHT)); loadImage(); x = INITIAL_X; y = INITIAL_Y; } @Override public void addNotify() { super.addNotify(); animator = new Thread(this); animator.start(); } @Override public void paintComponent(Graphics g) { super.paintComponent(g); drawStar(g); } private void drawStar(Graphics g) { g.drawImage(star, x, y, this); Toolkit.getDefaultToolkit().sync(); } private void cycle() { x += 1; y += 1; if (y > B_HEIGHT) { y = INITIAL_Y; x = INITIAL_X; } } @Override public void run() { long beforeTime, timeDiff, sleep; beforeTime = System.currentTimeMillis(); while (true) { cycle(); repaint(); timeDiff = System.currentTimeMillis() - beforeTime; sleep = DELAY - timeDiff; if (sleep < 0) { sleep = 2; } try { Thread.sleep(sleep); } catch (InterruptedException e) { String msg = String.format("Thread interrumpido por: %s", e.getMessage()); JOptionPane.showMessageDialog(this, msg, "Error", JOptionPane.ERROR_MESSAGE); } beforeTime = System.currentTimeMillis(); } } }
El método addNotify() se llama después de que nuestro JPanel se haya agregado al componente JFrame. Este método se usa a menudo para diversas tareas de inicialización.
@Override public void addNotify() { super.addNotify(); animator = new Thread(this); animator.start(); }
Queremos que nuestro juego funcione sin problemas, a velocidad constante. Por lo tanto, calculamos el tiempo del sistema.
timeDiff = System.currentTimeMillis() - beforeTime; sleep = DELAY - timeDiff;
Los métodos cycle() y repintar() pueden tomar un tiempo diferente en varios ciclos while. Calculamos el tiempo de ejecución de ambos métodos y lo restamos de la constante DELAY. De esta manera, queremos asegurarnos de que cada ciclo se ejecute a tiempo constante. En nuestro caso, es DELAY ms en cada ciclo.
Un último apunte es la extraña forma de invocar al frame en la función main().
Indagando por internet, se encuentra rápidamente la respuesta a esta peculiar sintaxis de inicio:
El procesamiento completo de Swing se realiza en un subproceso llamado EDT (Subproceso de distribución de eventos). Por lo tanto, bloquearía la GUI si calculara algunos cálculos de larga duración dentro de este hilo.
El camino a seguir aquí es procesar su cálculo dentro de un hilo diferente, para que su GUI se mantenga receptiva. Al final, desea actualizar su GUI, que debe hacerse dentro de la EDT. Ahora EventQueue.invokeLater entra en juego. Publica un evento (su Runnable) al final de la lista de eventos Swings y se procesa después de que se procesen todos los eventos GUI anteriores.
También el uso de EventQueue.invokeAndWait es posible aquí. La diferencia es que su cadena de cálculo se bloquea hasta que se actualiza su GUI.
Por lo tanto, es obvio que esto no debe usarse desde la EDT.
Tenga cuidado de no actualizar su interfaz gráfica de usuario de Swing desde un hilo diferente. En la mayoría de los casos, esto produce algunos problemas extraños de actualización / actualización.
Todavía hay código Java que inicia un JFrame simple desde el hilo principal. Esto podría causar problemas, pero no se evita Swing. La mayoría de los IDEs modernos ahora crean algo como esto para iniciar la GUI:
public static void main(String[] args) { EventQueue.invokeLater(() -> { JFrame ex = new ThreadAnimationEx(); ex.setVisible(true); }); }