En esta serie de tutoriales enseñaré conceptos intermedios sobre java (threads, AWT, Swing, etc.) y conceptos básicos para la programación de juegos (game loop, FPS, sprite, etc). Mi idea es que estos tutoriales sirvan tanto para los que desean programar juegos, como para que la gente con un nivel de principiante o intermedio en java pueda aprender y afianzar conceptos de programación java en general, de forma divertida.
Juegos en JavaHasta hace poco tiempo casi todos los juegos profesionales se desarrollaban en C o C++. Esto ha cambiado, hoy día hay grandes juegos realizados completamente en java. La industria de los juegos para móviles esta creciendo y java es el lenguaje para programar en Android. Android debe ser ya el sistema operativo para smart phones más usado en este momento. Por otro lado, juegos como Minecraft, tienen millones de usuarios a pesar de iniciarse como proyecto de un sólo programador, sin el apoyo de una gran empresa.
Espero, con esta serie de tutoriales, motivarlos a entrar en el mundo de la programación java y en particular de la programación de juegos.
El juego: Mini TenisEn esta serie de tutoriales desarrollaremos desde cero una versión de uno de los primeros juegos que alcanzaron popularidad en la era de la informática.

Este juego no pretende ser el próximo éxito en ventas sino tan sólo una plataforma desde donde enseñar y quien sabe si inspirar a algún programador principiante a convertirse en el próximo exitoso en el mundo de la informática
Índice- Nuestro primer gráfico: JFrame, JPanel, método paint
- Animación de un objeto en movimiento
- Sprites
- Eventos. Capturando las entrada por teclado
- Agregando el sprite raqueta
- Detección de colisiones
- Agregando sonido a nuestro juego
- Creando una clase Sound para nuestro juego
- Agregando puntuación y aumentando la velocidad
- Creando archivo jar ejecutable y qué es la máquina virtual de java
- Descargar el código fuente de minicraft y configurarlo en eclipse
- Agregar vista de mapa a Minicraft
Programación de juegos: JFrame, JPanel, método paintPara dibujar algo necesitamos una superficie donde pintar. Esta superficie o lienzo (Canvas en inglés) donde pintaremos nuestro primer ejemplo es un objeto JPanel. Así como un lienzo necesita un marco para sostenerse, nuestro JPanel estará enmarcado en una ventana modelada por la clase JFrame.
JFrame: La VentanaEl siguiente código crea una ventana con titulo "Mini Tennis" de 300 pixels por 300 pixels. La ventana no será visible hasta que llamemos setVisible(true). Si no incluimos la última línea "frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)", cuando cerremos la ventana el programa no terminará y seguirá ejecutándose.
package com.edu4java.minitennis1;
import javax.swing.JFrame;
public class Game {
public static void main
(String[] args
) { frame.setSize(300, 300);
frame.setVisible(true);
frame.
setDefaultCloseOperation(JFrame.
EXIT_ON_CLOSE); }
}
Si ejecutamos obtendremos:

Con estas pocas instrucciones obtenemos una ventana que se puede maximizar, minimizar, cambiar de tamaño con el ratón, etc. En realidad cuando creamos un objeto JFrame iniciamos un motor que maneja la interfaz de usuario. Este motor se comunica con el sistema operativo tanto para pintar en la pantalla como para recibir información del teclado o el ratón. Llamaremos a este motor "Motor AWT" o "Motor Swing" ya que está compuesto por estas dos librerías. En las primeras versiones de java solo existía AWT y luego se agregó Swing. Este Motor utiliza varios hilos de ejecución.
¿Qué es un hilo o thread en java?Normalmente un programa se ejecuta línea tras línea por un solo procesador en una sola línea o hilo de ejecución. El concepto de hilo (en ingles Thread) permite a un programa iniciar varias ejecuciones concurrentes. Esto es como si existieran varios procesadores ejecutándo al mismo tiempo sus propias secuencias de instrucciones.
Aunque los hilos y la concurrencia son herramientas muy potentes puede traer muchos problemas como que dos hilos accedan a las mismas variables de forma conflictiva. Es interesante considerar que dos hilos pueden estar ejecutando el mismo código de un método a la vez.
Podemos pensar que un hilo es un cocinero preparando un plato leyendo una receta de cocina. Dos hilos concurrentes serían como dos cocineros trabajando en la misma cocina, preparando cada uno un plato leyendo cada uno una receta o también podrían estar leyendo la misma receta. Los conflictos surgen por ejemplo cuando los dos intentan usar una sartén al mismo tiempo.
Motor AWT e Hilo de cola de eventos - Thread AWT-EventQueue-0El Motor AWT inicia varios Hilos (Threads) que podemos ver en la vista Debug si iniciamos la aplicación con debug y vamos a la perspectiva Debug. Cada hilo es como si fuera un programa independiente ejecutándose al mismo tiempo que los otros hilos. Más adelante veremos más sobre hilos, por lo pronto solo me interesa que recuerden el tercer hilo que vemos en la vista Debug llamado "Thread [AWT-EventQueue-0]" este hilo es el encargado de pintar la pantalla y recibir los eventos del teclado y el ratón.
JPanel: El lienzo (Canvas en inglés)Para poder pintar necesitamos donde y el donde es un objeto JPanel que incluiremos en la ventana. Extenderemos la clase JPanel para poder sobrescribir el método paint que es el método que llamará el Motor AWT para pintar lo que aparece en la pantalla.
package com.edu4java.minitennis1;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.Ellipse2D;
import javax.swing.JFrame;
import javax.swing.JPanel;
@SuppressWarnings("serial")
public class Game2
extends JPanel {
@Override
g2d.fillOval(0, 0, 30, 30);
g2d.drawOval(0, 50, 30, 30);
g2d.fillRect(50, 0, 30, 30);
g2d.drawRect(50, 50, 30, 30);
}
public static void main
(String[] args
) { frame.add(new Game2());
frame.setSize(300, 300);
frame.setVisible(true);
frame.
setDefaultCloseOperation(JFrame.
EXIT_ON_CLOSE); }
}
El método paint recibe por parámetro un objeto Graphics2D que extiende de Graphics. Graphics es la vieja clase usada por AWT que ha sido reemplazada por Graphics2D que tiene más y mejor funcionalidad. El parámetro sigue siendo de tipo Graphics por compatibilidad pero nosotros siempre utilizaremos Graphics2D por lo que es necesario crear una variable g2d: "Graphics2D g2d = (Graphics2D) g;". Una vez que tenemos g2d podemos utilizar todos los métodos de Graphics2D para dibujar.
Lo primero que hacemos es elegir el color que utilizamos para dibujar: "g2d.setColor(Color.RED);". Luego dibujamos unos círculos y cuadrados.
Posicionamiento en el lienzo. Coordenadas x e yPara dibujar algo dentro del lienzo debemos indicar en que posición comenzaremos a pintar. Para esto cada punto del lienzo tiene una posición (x,y) asociada siendo (0,0) el punto de la esquina superior izquierda.

El primer circulo rojo se pinta con "
g2d.fillOval(0, 0, 30, 30)": los primeros dos parámetros son la posición (x,y) y luego se indica el ancho y alto. como resultado tenemos un circulo de 30 pixeles de diámetro en la posición (0,0).
El circulo vacío se pinta con "
g2d.drawOval(0, 50, 30, 30)": el la posición x=0 (pegado al margen izquierdo) y la posición y=50 (50 pixeles más abajo del margen superior) pinta un circulo de 30 pixeles de alto y 30 de ancho.
Los rectángulos se pintan con "
g2d.fillRect(50, 0, 30, 30)" y "
g2d.drawRect(50, 50, 30, 30)" de forma similar a los círculos.
Por último "
g2d.draw(new Ellipse2D.Double(0, 100, 30, 30))" pinta el ultimo circulo usando un objeto Ellipse2D.Double.
Existen muchísimos métodos en Graphics2D. Algunos los veremos en siguientes tutoriales.
¿Cuándo el motor AWT llama al método paint?El motor AWT llama al método paint cada vez que el sistema operativo le informa que es necesario pintar el lienzo. Cuando se carga por primera vez la ventana se llama a paint, si minimizamos y luego recuperamos la ventana se llama a paint, si modificamos el tamaño de la ventana con el ratón se llama a paint.
Podemos comprobar este comportamiento si ponemos un breakpoint en la primer línea del método paint y ejecutamos en modo debug.

Es interesante ver que el método paint es ejecutado por el Hilo de cola de eventos (Thread AWT-EventQueue) que como indicamos antes es el encargado de pintar la pantalla.
Game loop y Animación de un objetoEn 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írculoComo 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 loopAl 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:
- 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.
- 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.
[/list]
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
super.paint(g);
g2d.fillOval(x, y, 30, 30);
}
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();
}
}
}
Al ejecutar el código anterior obtendremos:
Analizando nuestro método paintComo 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
super.paint(g);
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 hilosCuando 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.
Sprites - Velocidad y direcciónCada objeto que se mueve en la pantalla tiene características propias como la posición (x,y), la velocidad y la dirección en que se mueve, etc. Todas estas características se pueden aislar en un objeto que llamaremos Sprite.
Velocidad y direcciónEn el tutorial anterior logramos que la pelota (el círculo) se moviera hacia abajo y a la derecha a un píxel por vuelta en el Game Loop. Cuando llegaba al limite de la pantalla la pelota seguía su curso desapareciendo del lienzo. Lo que haremos a continuación es que la pelota rebote en los limites del lienzo cambiando su dirección.
package com.edu4java.minitennis3;
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;
int xa = 1;
int ya = 1;
private void moveBall() {
if (x + xa < 0)
xa = 1;
if (x + xa > getWidth() - 30)
xa = -1;
if (y + ya < 0)
ya = 1;
if (y + ya > getHeight() - 30)
ya = -1;
x = x + xa;
y = y + ya;
}
@Override
super.paint(g);
g.fillOval(x, y, 30, 30);
}
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();
}
}
}
En el código anterior se agregaron dos propiedades "xa" y "ya" que representan la velocidad en que se mueve la pelota. Si xa=1, la pelota se mueve hacia la derecha a un píxel por vuelta del Game Loop y si xa=-1, la pelota se mueve hacia la izquierda. Similarmente ya=1 mueve hacia abajo y ya=-1 mueve hacia arriba. Esto lo logramos con las líneas "
x = x + xa" e "
y = y + ya" del método moveBall().
Antes de ejecutar las instrucciones anteriores verificamos que la pelota no salga de los márgenes del lienzo. Por ejemplo cuando la pelota alcance el margen derecho o lo que es lo mismo cuando (x + xa > getWidth() - 30) lo que haremos es cambiar la dirección del movimiento sobre el eje x o lo que es lo mismo asignar menos uno a xa "
xa = -1".
private void moveBall() {
if (x + xa < 0)
xa = 1;
if (x + xa > getWidth() - 30)
xa = -1;
if (y + ya < 0)
ya = 1;
if (y + ya > getHeight() - 30)
ya = -1;
x = x + xa;
y = y + ya;
}
Cada sentencia if limita un borde del lienzo.
Crear el Sprite Ball (pelota en inglés)La idea es crear una clase llamada Ball que aisle todo lo referente a la pelota. En el siguiente código podemos ver como extraemos todo el código referente a la pelota de la clase Game2 y lo incorporamos a nuestra nueva clase Ball.
package com.edu4java.minitennis3;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import javax.swing.JFrame;
import javax.swing.JPanel;
@SuppressWarnings("serial")
public class Game2
extends JPanel {
Ball ball = new Ball(this);
private void move() {
ball.move();
}
@Override
super.paint(g);
ball.paint(g2d);
}
Game2 game = new Game2();
frame.add(game);
frame.setSize(300, 400);
frame.setVisible(true);
frame.
setDefaultCloseOperation(JFrame.
EXIT_ON_CLOSE);
while (true) {
game.move();
game.repaint();
}
}
}
El Sprite Ball necesita que le envíen una referencia al objeto Game para obtener los limites del lienzo y así saber cuando debe cambiar de dirección. En el método move() de la clase Ball se llama a
game.getWidth() y
game.getHeight().
package com.edu4java.minitennis3;
import java.awt.Graphics2D;
public class Ball {
int x = 0;
int y = 0;
int xa = 1;
int ya = 1;
private Game2 game;
public Ball(Game2 game) {
this.game= game;
}
void move() {
if (x + xa < 0)
xa = 1;
if (x + xa > game.getWidth() - 30)
xa = -1;
if (y + ya < 0)
ya = 1;
if (y + ya > game.getHeight() - 30)
ya = -1;
x = x + xa;
y = y + ya;
}
g.fillOval(x, y, 30, 30);
}
}
Si ejecutamos Game2 obtendremos el mismo resultado que si ejecutamos la versión anterior Game. La conveniencia de esta separación del código referente a la pelota en una clase de tipo Sprite se vuelve más obvia cuando incluimos la raqueta mediante un nuevo Sprite en un próximo tutorial.