Programacion de Juegos en Java
Programacion de Juegos en Java
JFrame: La Ventana
El siguiente cdigo 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
lnea "frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)", cuando cerremos la
ventana el programa no terminar y seguir ejecutndose.
package com.edu4java.minitennis1;
import javax.swing.JFrame;
usuario. Este motor se comunica con el sistema operativo tanto para pintar en
la pantalla como para recibir informacin del teclado o el ratn. Llamaremos a
este motor "Motor AWT" o "Motor Swing" ya que est compuesto por estas dos
libreras. En las primeras versiones de java solo exista AWT y luego se agreg
Swing. Este Motor utiliza varios hilos de ejecucin.
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 cdigo de un mtodo
a la vez.
Podemos pensar que un hilo es un cocinero preparando un plato leyendo una receta de
cocina. Dos hilos concurrentes seran como dos cocineros trabajando en la misma cocina,
preparando cada uno un plato leyendo cada uno una receta o tambin podran estar leyendo
la misma receta. Los conflictos surgen por ejemplo cuando los dos intentan usar una sartn
al mismo tiempo.
El Motor AWT inicia varios Hilos (Threads) que podemos ver en la vista Debug si
iniciamos la aplicacin con debug y vamos a la perspectiva Debug. Cada hilo es como si
fuera un programa independiente ejecutndose al mismo tiempo que los otros hilos. Ms
adelante veremos ms 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 ratn.
3
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 mtodo paint que es el
mtodo 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
public void paint(Graphics g) {
Graphics2D g2d = (Graphics2D) g;
g2d.setColor(Color.RED);
g2d.fillOval(0, 0, 30, 30);
g2d.drawOval(0, 50, 30, 30);
g2d.fillRect(50, 0, 30, 30);
g2d.drawRect(50, 50, 30, 30);
El mtodo paint recibe por parmetro un objeto Graphics2D que extiende de Graphics.
Graphics es la vieja clase usada por AWT que ha sido reemplazada por Graphics2D que
tiene ms y mejor funcionalidad. El parmetro 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 mtodos de Graphics2D para dibujar.
Para dibujar algo dentro del lienzo debemos indicar en que posicin comenzaremos a
pintar. Para esto cada punto del lienzo tiene una posicin (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 parmetros
son la posicin (x,y) y luego se indica el ancho y alto. como resultado tenemos un circulo
de 30 pixeles de dimetro en la posicin (0,0).
El circulo vaco se pinta con "g2d.drawOval(0, 50, 30, 30)": el la posicin x=0 (pegado al
margen izquierdo) y la posicin y=50 (50 pixeles ms abajo del margen superior) pinta un
circulo de 30 pixeles de alto y 30 de ancho.
Los rectngulos se pintan con "g2d.fillRect(50, 0, 30, 30)" y "g2d.drawRect(50, 50, 30,
30)" de forma similar a los crculos.
5
Por ltimo "g2d.draw(new Ellipse2D.Double(0, 100, 30, 30))" pinta el ultimo circulo
usando un objeto Ellipse2D.Double.
El motor AWT llama al mtodo 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 tamao
de la ventana con el ratn se llama a paint.
Es interesante ver que el mtodo paint es ejecutado por el Hilo de cola de eventos (Thread
AWT-EventQueue) que como indicamos antes es el encargado de pintar la pantalla.
En este tutorial veremos como hacer que un crculo se mueva sobre nuestro lienzo. Esta
animacin se consigue pintando el crculo en una posicin y luego borrando y pintando el
crculo en una posicin cercana. El efecto logrado es un crculo en movimiento.
Como mencionamos antes cada vez que pintamos debemos definir la posicin (x,y) donde
dibujaremos en este caso el crculo. Para que el crculo se mueva debemos modificar la
posicin (x,y) cada cierto tiempo y volver a pintar el crculo en la nueva posicin.
Game loop
Al final del mtodo main iniciamos un ciclo infinito "while (true)" donde repetidamente
llamamos a moveBall() para cambiar la posicin del circulo y luego llamamos a repaint()
que fuerza al motor AWT a llamar al mtodo paint para repintar el lienzo.
Este ciclo o repeticin se conoce como "Game loop" y se caracteriza por realizar dos
operaciones:
3. package com.edu4java.minitennis2;
4.
5. import java.awt.Graphics;
6. import java.awt.Graphics2D;
7. import java.awt.RenderingHints;
8. import javax.swing.JFrame;
9. import javax.swing.JPanel;
10.
11. @SuppressWarnings("serial")
12. public class Game extends JPanel {
13.
14. int x = 0;
15. int y = 0;
16.
17. private void moveBall() {
18. x = x + 1;
19. y = y + 1;
20. }
21.
22. @Override
7
Como mencionamos en el tutorial anterior este mtodo se ejecuta cada vez que el sistema
operativo le indica a Motor AWT que es necesario pintar el lienzo. Si ejecutamos el mtodo
repaint() de un objeto JPanel lo que estamos haciendo es decirle al Motor AWT que ejecute
8
el mtodo 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 posicin 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 lnea podemos ver el
siguiente efecto:
La instruccin "g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON)" suaviza los bordes de las figuras como se
puede ver en el siguiente grfico. El crculo de la izquierda es sin aplicar ANTIALIAS y el
de la derecha aplicando ANTIALIAS.
9
Cuando se inicia la ejecucin del mtodo main slo existe un hilo en ejecucin. Esto se
puede ver colocando un breakpoint en la primera lnea del mtodo main.
En la vista de la izquierda podemos ver que se han creado cuatro hilos de los cuales dos
estn detenidos en breakpoints. El Thread main est detenido en la lnea 40 en la
instruccin game.repaint(). El thread AWT-EventQueue est detenido en el mtodo paint en
la lnea 22.
Si oprimimos F6 (avanza la ejecucin del hilo slo una lnea), esta vez sobre el thread
main, veremos que el mtodo paint es vuelto a llamar por el thread AWT-EventQueue.
Ahora sacamos el breakpoint del mtodo paint, oprimimos F8 y volvemos a tener slo
detenido el thread main.
La siguiente animacin nos muestra que pasa en el lienzo cada vez que oprimimos resume
(F8) repetidamente. Cada llamada a moveBall() incrementa la posicin (x,y) del crculo y la
llamada a repaint() le dice al thread AWT-EventQueue que repinte el lienzo.
Por ltimo analicemos la lnea "Thread.sleep(10)" (la ltima instruccin dentro del "Game
loop"). Para esto comentamos la lnea con // y ejecutamos sin debug. El resultado es que no
se pinta el crculo 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 mtodo paint.
12
"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 mtodo paint.
Me gustara aclarar que en este ejemplo la solucin planteada es muy pobre y slo 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 prximos
tutoriales.
Cada objeto que se mueve en la pantalla tiene caractersticas propias como la posicin
(x,y), la velocidad y la direccin en que se mueve, etc. Todas estas caractersticas se pueden
aislar en un objeto que llamaremos Sprite.
Velocidad y direccin
En el tutorial anterior logramos que la pelota (el crculo) se moviera hacia abajo y a la
derecha a un pxel por vuelta en el Game Loop. Cuando llegaba al limite de la pantalla la
pelota segua su curso desapareciendo del lienzo. Lo que haremos a continuacin es que la
pelota rebote en los limites del lienzo cambiando su direccin.
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;
x = x + xa;
y = y + ya;
13
@Override
public void paint(Graphics g) {
super.paint(g);
Graphics2D g2d = (Graphics2D) g;
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g.fillOval(x, y, 30, 30);
while (true) {
game.moveBall();
game.repaint();
Thread.sleep(10);
}
}
}
En el cdigo 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 pxel
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 lneas "x = x
+ xa" e "y = y + ya" del mtodo moveBall().
Antes de ejecutar las instrucciones anteriores verificamos que la pelota no salga de los
mrgenes 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 direccin del
movimiento sobre el eje x o lo que es lo mismo asignar menos uno a xa "xa = -1".
x = x + xa;
y = y + ya;
}
14
La idea es crear una clase llamada Ball que aisle todo lo referente a la pelota. En el
siguiente cdigo podemos ver como extraemos todo el cdigo 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 {
@Override
public void paint(Graphics g) {
super.paint(g);
Graphics2D g2d = (Graphics2D) g;
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
ball.paint(g2d);
}
while (true) {
game.move();
game.repaint();
Thread.sleep(10);
}
}
}
El Sprite Ball necesita que le enven una referencia al objeto Game para obtener los limites
del lienzo y as saber cuando debe cambiar de direccin. En el mtodo move() de la clase
Ball se llama a game.getWidth() y game.getHeight().
15
package com.edu4java.minitennis3;
import java.awt.Graphics2D;
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;
}
Para leer del teclado es necesario registrar un objeto que se encargue de "escuchar si una
tecla es presionada". Este objeto conocido como "Listener" u "oyente" y tendr mtodos
que sern llamados cuando alguien presione una tecla. En nuestro ejemplo el Listener se
16
package com.edu4java.minitennis4;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import javax.swing.JFrame;
import javax.swing.JPanel;
@SuppressWarnings("serial")
public class KeyboardExample extends JPanel {
public KeyboardExample() {
KeyListener listener = new MyKeyListener();
addKeyListener(listener);
setFocusable(true);
}
@Override
public void keyPressed(KeyEvent e) {
System.out.println("keyPressed="+KeyEvent.getKeyText(e.getKeyCode()));
}
@Override
public void keyReleased(KeyEvent e) {
System.out.println("keyReleased="+KeyEvent.getKeyText(e.getKeyCode()));
}
}
}
En el constructor de la clase KeyboardExample creamos el listener y lo registramos. Para
que un objeto JPanel reciba las notificaciones del teclado es necesario incluir la instruccin
setFocusable(true) que permite que KeyboardExample reciba el foco.
public KeyboardExample() {
KeyListener listener = new MyKeyListener();
17
addKeyListener(listener);
setFocusable(true);
}
La clase MyKeyListener es la que uso para crear el objeto Listener. Este Listener imprimir
en la consola el nombre del mtodo y la tecla afectada por el evento.
@Override
public void keyPressed(KeyEvent e) {
System.out.println("keyPressed="+KeyEvent.getKeyText(e.getKeyCode()));
}
@Override
public void keyReleased(KeyEvent e) {
System.out.println("keyReleased="+KeyEvent.getKeyText(e.getKeyCode()));
}
}
Una vez registrado, cuando KeyboardExample (nuestro JPanel) tenga el foco y alguien
oprima una tecla KeyboardExample informar al objeto listener registrado. El objeto
Listener de nuestro ejemplo implementa la interfaz KeyListener que tiene los mtodos
keyTyped(), keyPressed() y keyReleased(). El mtodo keyPressed ser llamado cada vez
que una tecla sea oprimida (y varias veces si se mantiene oprimida). El mtodo
keyReleased ser llamado cuando solemos una tecla.
Los mtodos antes mencionados reciben como parmetro un objeto KeyEvent que contiene
informacin sobre que tecla se ha oprimido o soltado. Usando e.getKeyCode() podemos
obtener el cdigo de la tecla y si le pasamos un cdigo de tecla a la funcin estatica
KeyEvent.getKeyText(...) podemos obtener el texto asociado a la tecla.
Lo eventos del ratn y el teclado son controlados por el sistema operativo. El motor AWT,
en particular el thread AWT-Windows se comunica con el sistema operativo y se entera de
si hubo un evento. Cuando encuentra un nuevo evento lo coloca en la "Cola de Eventos"
para que sea atendido cuando le llegue su turno por el Thread AWT-EventQueue.
18
Clase annima
En el ejemplo anterior la clase MyKeyListener ser solo usada una vez por lo que
podramos reemplazarla por una clase annima. KeyboardExample2 muestra como sera:
package com.edu4java.minitennis4;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import javax.swing.JFrame;
import javax.swing.JPanel;
@SuppressWarnings("serial")
public class KeyboardExample2 extends JPanel {
public KeyboardExample2() {
KeyListener listener = new KeyListener() {
@Override
public void keyTyped(KeyEvent e) {
}
@Override
public void keyPressed(KeyEvent e) {
System.out.println("keyPressed="+KeyEvent.getKeyText(e.getKeyCode()));
19
@Override
public void keyReleased(KeyEvent e) {
System.out.println("keyReleased="+KeyEvent.getKeyText(e.getKeyCode()));
}
};
addKeyListener(listener);
setFocusable(true);
}
@Override
public void keyPressed(KeyEvent e) {
System.out.println("keyPressed="+KeyEvent.getKeyText(e.getKeyCode()));
}
@Override
public void keyReleased(KeyEvent e) {
System.out.println("keyReleased="+KeyEvent.getKeyText(e.getKeyCode()));
}
};
Esta instruccin tiene el mismo efecto que la anterior. Reemplaza la definicin de la clase
MyKeyListener por una clase annima que hace exactamente lo mismo.
La forma de crear un objeto desde una clase annima es reemplazar el nombre de la clase a
crear por una definicin que empieza por la interfaz a implementar seguida por () y luego
dentro de {} la definicin de la clase como hacemos normalmente.
20
Lo primero que hacemos es agregar en la clase Game una nueva propiedad llamada racquet
donde mantendremos el Sprite que maneja la raqueta. En el mtodo move() aadimos una
llamada a racquet.move() y en paint() una llamada a racquet.paint(). Hasta ahora todo es
similar al sprite Ball pero como la posicin de la raqueta responde al teclado tenemos que
hacer algo ms.
package com.edu4java.minitennis5;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import javax.swing.JFrame;
import javax.swing.JPanel;
@SuppressWarnings("serial")
public class Game extends JPanel {
public Game() {
addKeyListener(new KeyListener() {
@Override
public void keyTyped(KeyEvent e) {
}
@Override
public void keyReleased(KeyEvent e) {
racquet.keyReleased(e);
}
@Override
public void keyPressed(KeyEvent e) {
racquet.keyPressed(e);
}
});
setFocusable(true);
}
21
@Override
public void paint(Graphics g) {
super.paint(g);
Graphics2D g2d = (Graphics2D) g;
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
ball.paint(g2d);
racquet.paint(g2d);
}
while (true) {
game.move();
game.repaint();
Thread.sleep(10);
}
}
}
En el constructor de la clase game se puede ver como se registra un listener para capturar
los eventos del teclado. En el mtodo keyPressed() del listener informamos a la raqueta que
una tecla ha sido oprimida llamando a racquet.keyPressed(e). Lo mismo hacemos para
keyReleased(). Con esto el Sprite racquet se enterar cuando una tecla sea oprimida.
Veamos ahora las clases Ball y Racquet que implementan los sprites
package com.edu4java.minitennis5;
import java.awt.Graphics2D;
void move() {
22
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;
}
package com.edu4java.minitennis5;
import java.awt.Graphics2D;
import java.awt.event.KeyEvent;
Inicialmente el valor de "x" es cero lo que indica que la raqueta estar en el limite izquierdo
del lienzo. "xa" tambin est inicializado a cero, lo que hace que en principio la raqueta
aparezca esttica ya que x = x + xa no modificar "x" mientras "xa" sea cero.
Cuando alguien presione una tecla el mtodo keyPressed de Racquet ser llamado y este
pondr "xa" en 1 si la tecla presionada es la de direccin derecha (KeyEvent.VK_RIGHT)
lo que a su vez har que la raqueta se mueva a la derecha la prxima vez que se llame al
mtodo move (recordar x = x + xa). De la misma forma si se presiona la tecla
KeyEvent.VK_LEFT se mover a la izquierda.
Cuando una tecla deja de ser presionada el mtodo keyReleased es llamado y "xa" pasa a
valer cero lo que hace que el movimiento de la raqueta se detenga.
Si ejecutamos el ejemplo podemos ver como la pelota se mueve rebotando contra los
lmites y la raqueta se mueve cuando presionamos las teclas de direccin correspondientes.
Pero cuando la pelota choca con la raqueta la atraviesa pareciendo como si esta no
existiese. En el prximo tutorial veremos como hacer que la pelota rebote sobre la raqueta.
Deteccin de colisiones
En este tutorial aprenderemos como detectar cuando un sprite choca con otro. En nuestro
juego haremos que la pelota rebote contra la raqueta. Adems haremos que el juego termine
si la pelota alcanza el limite inferior del lienzo mostrando una ventana popup con el clsico
mensaje "Game Over".
24
Game Over
A continuacin vemos nuestra clase Game que es idntica a la anterior con la sola
diferencia de que se ha agregado el mtodo gameOver();
package com.edu4java.minitennis6;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
@SuppressWarnings("serial")
public class Game extends JPanel {
public Game() {
addKeyListener(new KeyListener() {
@Override
public void keyTyped(KeyEvent e) {
}
@Override
public void keyReleased(KeyEvent e) {
racquet.keyReleased(e);
}
@Override
public void keyPressed(KeyEvent e) {
racquet.keyPressed(e);
}
});
setFocusable(true);
}
@Override
public void paint(Graphics g) {
super.paint(g);
Graphics2D g2d = (Graphics2D) g;
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
ball.paint(g2d);
racquet.paint(g2d);
}
25
while (true) {
game.move();
game.repaint();
Thread.sleep(10);
}
}
}
El mtodo gameOver() lanza un popup usando JOptionPane.showMessageDialog con el
mensaje "Game Over" y un solo botn "Aceptar". Despus del popup,
System.exit(ABORT) hace que se termine el programa. El mtodo gameOver() es pblico
ya que ser llamado desde el sprite Ball cuando detecte que ha llegado al lmite inferior del
lienzo.
Colisin de Sprites
package com.edu4java.minitennis6;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.event.KeyEvent;
Otro cambio que funcionalmente no afecta pero que es una buena prctica de programacin
es la inclusin de constantes:
Como antes mencionamos el valor de posicin "y" estaba fijo en 330. Este valor es usado
tanto en el mtodo paint como en getBounds. Si queremos cambiarlo ahora slo tenemos
que cambiarlo en un slo lugar evitando el posible error que se producira si lo
cambiramos en un mtodo y en otro no.
La forma de definir una constante en java es declarando una propiedad "static final" y en
maysculas. El compilador permite usar minsculas pero el estndar dice que se deben usar
maysculas para los nombres de las constantes.
package com.edu4java.minitennis6;
import java.awt.Graphics2D;
import java.awt.Rectangle;
int xa = 1;
int ya = 1;
private Game game;
void move() {
if (x + xa < 0)
xa = 1;
if (x + xa > game.getWidth() - DIAMETER)
xa = -1;
if (y + ya < 0)
ya = 1;
if (y + ya > game.getHeight() - DIAMETER)
game.gameOver();
if (collision()){
ya = -1;
y = game.racquet.getTopY() - DIAMETER;
}
x = x + xa;
y = y + ya;
}
y = game.racquet.getTopY() - DIAMETER;
Por ltimo es el mtodo move() de la clase Ball el que usa los nuevos mtodos collision() y
gameOver() de la clase Game. El rebote al alcanzar el lmite inferior ha sido reemplazado
por una llamada a game.gameOver().
if (collision())
ya = -1;
Creando sonidos
Para crear los sonidos me tom la libertad de buscar en Google "free audio editor" y como
respuesta encontr http://free-audio-editor.com/. Tengo que decir que la versin gratis de
este producto es potente y fcil de manejar.
30
Con este editor he creado los archivos: back.wav, gameover.wav y ball.wav. En el video de
youtube pueden ver como lo hice y crearlos ustedes mismos. Tambin pueden descargar y
usar estos tres que en esta misma lnea los declaro libres de copyright. Lo que tienen que
hacer es copiar estos archivos al paquete com.edu4java.minitennis7.
Para reproducir los archivos de sonido usaremos la clase AudioClip. Crearemos objetos
AudioClip usando el mtodo esttico de la clase Applet: Applet.newAudioClip(URL url).
Este mtodo necesita un objeto URL que le indique donde est el archivo de audio que
queremos cargar para luego reproducir. La siguiente instruccin crea un objeto URL
utilizando una ubicacin en Internet:
Nosotros buscaremos nuestro archivo utilizando el classpath. Este es el sistema que usa
java para cargar las clases o mejor dicho los archivos *.class que definen las clases del
programa. Para obtener un URL desde el classpath se utiliza el mtodo getResource(String
name) de la clase Class donde name es el nombre del archivo que queremos obtener.
A continuacin vemos dos formas de como conseguir el URL del archivo "back.wav" que
est en el mismo paquete que la clase SoundTest o lo que es lo mismo en el mismo
directorio donde esta el archivo SoundTest.class.
Tanto "SoundTest.class" como "new SoundTest().getClass()" nos dan un objeto class que
tiene el mtodo getResource que queremos usar.
He creado la clase SoundTest con el slo propsito de mostrarles como trabaja AudioClip y
no es necesaria para nuestro juego. A continuacin se muestra el cdigo fuente de
SoundTest completo:
package com.edu4java.minitennis7;
import java.applet.Applet;
import java.applet.AudioClip;
import java.net.URL;
// System.out.println("1");
// URL url = new
URL("http://www.edu4java.com/es/game/sound/back.wav");
// System.out.println("2");
// AudioClip clip = Applet.newAudioClip(url);
// System.out.println("3");
// clip.play();
// System.out.println("4");
// Thread.sleep(1000);
clip.play();
Thread.sleep(1000);
clip2.loop();
Thread.sleep(20000);
clip2.stop();
System.out.println("end");
}
}
Una ventaja de esta metodologa es que slo tenemos que indicar la posicin del archivo
con respecto a la clase que lo usa. En nuestro caso como est en el mismo paquete basta
con el nombre "back.wav". Otra ventaja es que los archivos de sonido se pueden incluir en
un archivo *.jar. Veremos ms sobre archivos *.jar ms adelante. Una ves que tenemos el
objeto URL podemos crear objetos AudioClip usando Applet.newAudioClip(url).
El objeto AudioClip tiene un mtodo play() que inicia un thread independiente que
reproduce slo una vez el audio contenido en el archivo. Para reproducir el audio en forma
repetitiva podemos usar el mtodo loop() de AudioClip que reproducir el sonido una y otra
vez hasta que se llame al mtodo stop sobre el mismo objeto AudioClip.
Dos audioClips pueden reproducirse al mismo tiempo. En el ejemplo creo dos audioClips
con el mismo audio: clip y clip2. Reproduzco clip con play, espero un segundo
Thread.sleep(1000) y reproduzco clip2 con loop. El resultado es una mezcla de los dos
audios. Por ultimo despus de 20 segundos Thread.sleep(20000) llamo a clip2.stop() y
detengo la repeticin de clip2.
package com.edu4java.minitennis7;
import java.applet.Applet;
import java.applet.AudioClip;
Los objetos audioclips se crearn al cargarse la clase Sound la primera vez que alguien use
la clase Sound. A partir de este momento sern reutilizados una y otra vez. Ahora veamos
las modificaciones en la clase Game:
package com.edu4java.minitennis7;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
@SuppressWarnings("serial")
public class Game extends JPanel {
public Game() {
addKeyListener(new KeyListener() {
@Override
public void keyTyped(KeyEvent e) {
}
@Override
public void keyReleased(KeyEvent e) {
racquet.keyReleased(e);
}
@Override
public void keyPressed(KeyEvent e) {
racquet.keyPressed(e);
}
});
setFocusable(true);
Sound.BACK.loop();
}
@Override
public void paint(Graphics g) {
34
super.paint(g);
Graphics2D g2d = (Graphics2D) g;
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
ball.paint(g2d);
racquet.paint(g2d);
}
while (true) {
game.move();
game.repaint();
Thread.sleep(10);
}
}
}
package com.edu4java.minitennis7;
import java.awt.Graphics2D;
import java.awt.Rectangle;
int x = 0;
int y = 0;
int xa = 1;
int ya = 1;
35
void move() {
boolean changeDirection = true;
if (x + xa < 0)
xa = 1;
else if (x + xa > game.getWidth() - DIAMETER)
xa = -1;
else if (y + ya < 0)
ya = 1;
else if (y + ya > game.getHeight() - DIAMETER)
game.gameOver();
else if (collision()){
ya = -1;
y = game.racquet.getTopY() - DIAMETER;
} else
changeDirection = false;
if (changeDirection)
Sound.BALL.play();
x = x + xa;
y = y + ya;
}
Lo que hice en move() es agregar una variable changeDirection que inicializo a true.
Aadiendo un else a cada if y colocando un changeDirection = false que slo se ejecutar si
ninguna condicin en los if es cumplida, conseguimos enterarnos si la bola ha rebotado. Si
la pelota ha rebotado changeDirection ser verdadero y Sound.BALL.play() ser ejecutado.
Los objetos mviles del juego son la pelota y la raqueta. Modificando la velocidad de
movimiento de estos dos objetos modificaremos la velocidad del juego. Vamos a incluir una
propiedad llamada speed en la clase Game para mantener la velocidad del juego. La
propiedad speed ser inicialmente 1 e ir incrementndose cada vez que le demos a la
pelota con la raqueta.
Para la puntuacin necesitaramos otra propiedad a incrementar cada vez que golpeemos la
pelota. En vez de crear una nueva propiedad se me ocurri reutilizar speed. El nico
inconveniente es que las puntuaciones suelen iniciarse en 0 y no en 1 como speed. La
solucin que se me ocurri fue agregar un mtodo getScore() que retorne el valor de speed
menos uno.
package com.edu4java.minitennis8;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
@SuppressWarnings("serial")
public class Game extends JPanel {
public Game() {
addKeyListener(new KeyListener() {
@Override
public void keyTyped(KeyEvent e) {
}
@Override
public void keyReleased(KeyEvent e) {
racquet.keyReleased(e);
}
37
@Override
public void keyPressed(KeyEvent e) {
racquet.keyPressed(e);
}
});
setFocusable(true);
Sound.BACK.loop();
}
@Override
public void paint(Graphics g) {
super.paint(g);
Graphics2D g2d = (Graphics2D) g;
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
ball.paint(g2d);
racquet.paint(g2d);
g2d.setColor(Color.GRAY);
g2d.setFont(new Font("Verdana", Font.BOLD, 30));
g2d.drawString(String.valueOf(getScore()), 10, 30);
}
while (true) {
game.move();
game.repaint();
Thread.sleep(10);
}
}
}
Para pintar la puntuacin en el rincn superior izquierdo al final del mtodo paint he
agregado:
38
g2d.setColor(Color.GRAY);
g2d.setFont(new Font("Verdana", Font.BOLD, 30));
g2d.drawString(String.valueOf(getScore()), 10, 30);
En la primera lnea elegimos el color gris, en la segunda lnea el tipo de letra Verdana,
negrita de 30 pixeles y finalmente en la posicin (x,y) igual a (10,30) donde dibujamos la
puntuacin.
En la clase Ball el mtodo move() ha sido modificado para considerar la nueva propiedad
de velocidad "game.speed". Cuando la pelota cambiaba de direccin las propiedades de
velocidad xa y ya eran modificadas a 1 o -1. Ahora considerando la velocidad estas
propiedades son cambiadas a game.speed o -game.speed. Tambin se ha agregado en el
condicional if(collision()) que la velocidad se incremente "game.speed++".
package com.edu4java.minitennis8;
import java.awt.Graphics2D;
import java.awt.Rectangle;
int x = 0;
int y = 0;
int xa = 1;
int ya = 1;
private Game game;
void move() {
boolean changeDirection = true;
if (x + xa < 0)
xa = game.speed;
else if (x + xa > game.getWidth() - DIAMETER)
xa = -game.speed;
else if (y + ya < 0)
ya = game.speed;
else if (y + ya > game.getHeight() - DIAMETER)
game.gameOver();
else if (collision()){
ya = -game.speed;
y = game.racquet.getTopY() - DIAMETER;
game.speed++;
39
} else
changeDirection = false;
if (changeDirection)
Sound.BALL.play();
x = x + xa;
y = y + ya;
}
package com.edu4java.minitennis8;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.event.KeyEvent;
if (e.getKeyCode() == KeyEvent.VK_LEFT)
xa = -game.speed;
if (e.getKeyCode() == KeyEvent.VK_RIGHT)
xa = game.speed;
}
Si tenemos instalado la JDK tendremos un directorio donde estarn todos los archivos que
componen la plataforma java. Este directorio es conocido como java Home o
JAVA_HOME. En mi caso este es "C:\Program Files (x86)\Java\jdk1.6.0_21".
Dentro de JAVA_HOME existe una carpeta bin que contiene los ejecutable entre los que
podemos encontrar: El compilador: javac.exe y la mquina virtual: java.exe.
Para ejemplificar como funcionan estos programas vamos a crear un archivo llamado
HelloWorld.java en un directorio C:\testjava con el siguiente contenido:
42
import javax.swing.JOptionPane;
javac HelloWorld.java
o
"C:\Program Files (x86)\Java\jdk1.7.0_05\bin\javac" HelloWorld.java
Como resultado podemos ver que se ha creado un nuevo archivo HellowWorld.class con el
bytecode. Podemos ejecutar este bytecode con la siguiente instruccin:
java HelloWorld
o
"C:\Program Files (x86)\Java\jdk1.7.0_05\bin\java" HelloWorld
Un programa java normalmente esta compuesto por varios archivos java y por consiguiente
muchos archivos *.class. Adems estn los archivos de recursos como los sonidos en
nuestra aplicacin. Java permite empaquetar una aplicacin con todos los archivos antes
mencionados en un archivo *.jar.
Archivo JAR
Para que el archivo jar sea ejecutable hay que incluir en el archivo MANIFEST.MF una
lnea indicando la clase que contiene el mtodo esttico main() que se usar para iniciar la
aplicacin. En nuestro ejemplo anterior sera:
Main-Class: HelloWorld
43
Es importante destacar que al final de la lnea hay que agregar un retorno de carro para que
funcione. Los invito a crear un archivo testjava.zip que contenga el archivo
HelloWorld.class, el directorio META-INF y dentro el archivo MANIFEST.MF con la linea
Main-Class: HelloWorld. Para esto pueden usar los programas Winzip o WinRAR que
pueden descargar gratuitamente (buscar en Google).
Para crear un JAR ejecutable basta con ir a File-Export, seleccionar Runnable JAR file
44
Si java est bien instalado sobre Windows, con un doble click sobre minitennis.jar sera
suficiente para ejecutar nuestra aplicacin.
Examinando minitennis.jar
45
Eclipse realiza un excelente trabajo compilando, ejecutando y creando archivos JAR pero
es bueno entender que por debajo eclipse usa la instalacin de java de forma similar a
nuestro ejemplo HelloWorld.