Game loop y Animación de un objeto

En este tutorial veremos como hacer que un círculo se mueva sobre nuestro lienzo. Esta animación se consigue pintando el círculo en una posición y luego borrando y pintando el círculo en una posición cercana. El efecto logrado es un círculo en movimiento.

 

Posición del círculo

Como mencionamos antes cada vez que pintamos debemos definir la posición (x,y) donde dibujaremos en este caso el círculo. Para que el círculo se mueva debemos modificar la posición (x,y) cada cierto tiempo y volver a pintar el círculo en la nueva posición.

En nuestro ejemplo mantendremos en dos propiedades llamadas "x" e "y", la posición actual de nuestro círculo. También creamos un método moveBall() que incrementará en 1 tanto a "x" como a "y" cada vez que es llamado. En el método paint dibujamos un circulo de 30 pixeles de diámetro en la posición (x,y) dada por las propiedades antes mencionadas "g2d.fillOval(x, y, 30, 30);".

Game loop

Al final del método main iniciamos un ciclo infinito "while (true)" donde repetidamente llamamos a moveBall() para cambiar la posición del circulo y luego llamamos a repaint() que fuerza al motor AWT a llamar al método paint para repintar el lienzo.

Este ciclo o repetición se conoce como "Game loop" y se caracteriza por realizar dos operaciones:

  1. Actualización (Update): actualización de la física de nuestro mundo. En nuestro caso nuestra actualización esta dada tan solo por el método moveBall() que incrementa las propiedades "x" e "y" en 1.
  2. Renderizado (Render): aquí se dibuja según el estado actual de nuestro mundo reflejando los cambios realizados en el paso anterior. En nuestro ejemplo este renderizado esta dado por la llamada a repaint() y la subsecuente llamada a paint realizada por el motor AWT o más específicamente por el Hilo de cola de eventos.
package com.edu4java.minitennis2;

import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import javax.swing.JFrame;
import javax.swing.JPanel;

@SuppressWarnings("serial")
public class Game extends JPanel {

	int x = 0;
	int y = 0;

	private void moveBall() {
		x = x + 1;
		y = y + 1;
	}

	@Override
	public void paint(Graphics g) {
		super.paint(g);
		Graphics2D g2d = (Graphics2D) g;
		g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
				RenderingHints.VALUE_ANTIALIAS_ON);
		g2d.fillOval(x, y, 30, 30);
	}

	public static void main(String[] args) throws InterruptedException {
		JFrame frame = new JFrame("Mini Tennis");
		Game game = new Game();
		frame.add(game);
		frame.setSize(300, 400);
		frame.setVisible(true);
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		
		while (true) {
			game.moveBall();
			game.repaint();
			Thread.sleep(10);
		}
	}
}

Al ejecutar el código anterior obtendremos:

Analizando nuestro método paint

Como mencionamos en el tutorial anterior este método se ejecuta cada vez que el sistema operativo le indica a Motor AWT que es necesario pintar el lienzo. Si ejecutamos el método repaint() de un objeto JPanel lo que estamos haciendo es decirle al Motor AWT que ejecute el método paint tan pronto como pueda. La llamada a paint la realizará el Hilo de cola de eventos. Llamando a repaint() logramos que se repinte el lienzo y así poder reflejar el cambio en la posición del circulo.

	@Override
	public void paint(Graphics g) {
		super.paint(g);
		Graphics2D g2d = (Graphics2D) g;
		g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
				RenderingHints.VALUE_ANTIALIAS_ON);
		g2d.fillOval(x, y, 30, 30);
	}

La llamada a "super.paint(g)" limpia la pantalla, si comentamos esta línea podemos ver el siguiente efecto:

La instrucción "g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)" suaviza los bordes de las figuras como se puede ver en el siguiente gráfico. El círculo de la izquierda es sin aplicar ANTIALIAS y el de la derecha aplicando ANTIALIAS.

Analizando la concurrencia y el comportamiento de los hilos

Cuando se inicia la ejecución del método main sólo existe un hilo en ejecución. Esto se puede ver colocando un breakpoint en la primera línea del método main.

Si agregamos un breakpoint en la línea game.repaint() y en la primera línea del método paint y a continuación oprimimos F8 (Resume: ordena que continúe la ejecución hasta el final o hasta que encuentre el próximo breakpoint) obtendremos:

En la vista de la izquierda podemos ver que se han creado cuatro hilos de los cuales dos están detenidos en breakpoints. El Thread main está detenido en la línea 40 en la instrucción game.repaint(). El thread AWT-EventQueue está detenido en el método paint en la línea 22.

Si seleccionamos el thread AWT-EventQueue en la vista Debug y oprimimos F8 repetidamente (2 veces) veremos que no se detiene más en el metodo paint. Esto es porque el sistema operativo no ve motivo para solicitar un repintado del lienzo una vez inicializado.

Si oprimimos F6 (avanza la ejecución del hilo sólo una línea), esta vez sobre el thread main, veremos que el método paint es vuelto a llamar por el thread AWT-EventQueue. Ahora sacamos el breakpoint del método paint, oprimimos F8 y volvemos a tener sólo detenido el thread main.

La siguiente animación nos muestra que pasa en el lienzo cada vez que oprimimos resume (F8) repetidamente. Cada llamada a moveBall() incrementa la posición (x,y) del círculo y la llamada a repaint() le dice al thread AWT-EventQueue que repinte el lienzo.

Por último analicemos la línea "Thread.sleep(10)" (la última instrucción dentro del "Game loop"). Para esto comentamos la línea con // y ejecutamos sin debug. El resultado es que no se pinta el círculo en el lienzo. ¿Por qué pasa esto? Esto es debido a que el thread main se apodera del procesador y no lo comparte con el thread AWT-EventQueue que entonces no puede llamar al método paint.

"Thread.sleep(10)" le dice al procesador que el thread que se está ejecutando descanse por 10 milisegundos lo que permite que el procesador ejecute otros threads y en particular el thread AWT-EventQueue que llama al método paint.

Me gustaría aclarar que en este ejemplo la solución planteada es muy pobre y sólo pretende ilustrar los conceptos de "game loop", threads y concurrencia. Existen mejores formas de manejar el game loop y la concurrencia en un juego y las veremos en los próximos tutoriales.

<< Anterior Siguiente >>