En el artículo anterior ("Para hacer un juego con Java") vimos que la estructura básica de un videojuego es básicamente un ciclo infinito que ejecuta repetidamente los tres pasos siguientes:
- Lee los controles
- Ejecuta la lógica del juego
- Redibuja la pantalla
Para que el ejemplo fuera muy sencillo omitimos en ese artículo la parte de la interacción (el paso Lee los controles) y solamente hicimos un programa que mostraba una bola moviendose dentro de una ventana. Ahora vamos a ver cómo hacer que el jugador pueda controlar la bola con las flechas del teclado.
Un "juego" interactivo
Aquí está el fuente completo de un programa en Java que te permite emplear las flechas del teclado para mover una bola dentro de una ventana:
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class Demo2 extends JComponent {
private final static int ANCHO = 512;
private final static int ALTO = 384;
private final static int DIAMETRO = 20;
private float x, y;
private float vx, vy;
private boolean arriba, abajo, izquierda, derecha;
public Demo2() {
setPreferredSize(new Dimension(ANCHO, ALTO));
x = 10;
y = 20;
addKeyListener(new KeyAdapter() {
public void keyPressed(KeyEvent e) {
actualiza(e.getKeyCode(), true);
}
public void keyReleased(KeyEvent e) {
actualiza(e.getKeyCode(), false);
}
private void actualiza(int keyCode, boolean pressed) {
switch (keyCode) {
case KeyEvent.VK_UP:
arriba = pressed;
break;
case KeyEvent.VK_DOWN:
abajo = pressed;
break;
case KeyEvent.VK_LEFT:
izquierda = pressed;
break;
case KeyEvent.VK_RIGHT:
derecha = pressed;
break;
}
}
});
setFocusable(true);
}
private float clamp(float valor, float min, float max) {
if (valor > max)
return max;
if (valor < min)
return min;
return valor;
}
private void fisica(float dt) {
vx = 0;
vy = 0;
if (arriba)
vy = -300;
if (abajo)
vy = 300;
if (izquierda)
vx = -300;
if (derecha)
vx = 300;
x = clamp(x + vx * dt, 0, ANCHO - DIAMETRO);
y = clamp(y + vy * dt, 0, ALTO - DIAMETRO);
}
public void paint(Graphics g) {
g.setColor(Color.WHITE);
g.fillRect(0, 0, ANCHO, ALTO);
g.setColor(Color.RED);
g.fillOval(Math.round(x), Math.round(y), DIAMETRO, DIAMETRO);
}
private void dibuja() throws Exception {
SwingUtilities.invokeAndWait(new Runnable() {
public void run() {
paintImmediately(0, 0, ANCHO, ALTO);
}
});
}
public void cicloPrincipalJuego() throws Exception {
long tiempoViejo = System.nanoTime();
while (true) {
long tiempoNuevo = System.nanoTime();
float dt = (tiempoNuevo - tiempoViejo) / 1000000000f;
tiempoViejo = tiempoNuevo;
fisica(dt);
dibuja();
}
}
public static void main(String[] args) throws Exception {
JFrame jf = new JFrame("Demo2");
jf.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
jf.setResizable(false);
Demo2 demo2 = new Demo2();
jf.getContentPane().add(demo2);
jf.pack();
jf.setVisible(true);
demo2.cicloPrincipalJuego();
}
}
Para compilar y ejecutar este programa sigue los mismos pasos que para el programa del artículo anterior. La única diferencia es que ahora tienes que guardar el programa dentro de un archivo que se llame Demo2.java.
Cómo funciona el programa
Veamos ahora el programa parte por parte para entender cómo funciona. Este programa es muy similar al programa Demo1.java del artículo anterior, por lo tanto sólo vamos a explicar los cambios que le hicimos. En el artículo anterior ("Para hacer un juego con Java") puedes averiguar cómo funciona el resto del programa.
Cómo saber si está presionada una flecha del teclado
El primer cambio en el programa es que ahora tenemos cuatro nuevas variables de instancia:
private boolean arriba, abajo, izquierda, derecha;
Cada una de esas variables booleanas es verdadera cuando la tecla correspondiente del teclado está presionada. Claro que esto no ocurre automáticamente, tenemos que programar algún mecanismo para detectar cuando el usario presiona o suelta alguna de esas teclas y actualizar en ese momento la variable correspondiente.
Ese mecanismo está programado en el constructor de nuestra clase Demo2. Aquí esta el fuente del constructor, con los cambios respecto al ejemplo del artículo anterior en negritas para resaltar las diferencias:
public Demo2() {
setPreferredSize(new Dimension(ANCHO, ALTO));
x = 10;
y = 20;
addKeyListener(new KeyAdapter() {
public void keyPressed(KeyEvent e) {
actualiza(e.getKeyCode(), true);
}
public void keyReleased(KeyEvent e) {
actualiza(e.getKeyCode(), false);
}
private void actualiza(int keyCode, boolean pressed) {
switch (keyCode) {
case KeyEvent.VK_UP:
arriba = pressed;
break;
case KeyEvent.VK_DOWN:
abajo = pressed;
break;
case KeyEvent.VK_LEFT:
izquierda = pressed;
break;
case KeyEvent.VK_RIGHT:
derecha = pressed;
break;
}
}
});
setFocusable(true);
}
Nota: otro cambio es que ya no estamos dandole un valor inicial al vector velocidad (variables vx y vy) dentro del constructor.
Los componentes de Swing permiten que se registren con ellos unos objetos listeners (escuchadores) a los que les avisan cuando ocurren ciertos eventos dentro del componente. Nuestra clase Demo2 extiende (hereda de) la clase JComponent y eso hace que sea un componente de Swing. Unos de los eventos que pueden ocurrir es que el usuario presione o suelte una tecla estando ese componente seleccionado (más adelante explicamos lo que quiere decir que un componente esté seleccionado). Para poder registarse con un componente de Swing y enterarse de los eventos relacionados con el teclado se necesita un listener que implemente la interfaz KeyListener. Esa interfaz define tres métodos, de los cuales hay dos que nos interesan: keyPressed() y keyReleased. Para no tener que implementar el tercer método, que no nos interesa para este programa, creamos una clase anómima que hereda de la clase KeyAdapter, la cual implementa la interfaz KeyListener con implementaciones "vacias" de sus métodos (que no hacen nada).
Cada vez que el jugador presiona una tecla, el componente llama el método keyPressed() de todos sus listeners de tipo KeyListener pasándoles como argumento un objeto KeyEvent. Cuando el jugador suelta la tecla, el componente llama el método keyReleased() de esos mismos listeners, una vez más pasándoles como argumento un objeto KeyEvent.
Los objetos KeyEvent tienen un método getKeyCode() que devuelve un entero indicando cual fue la tecla que se presionó o se soltó. La clase KeyEvent también define unas constantes que permiten identificar con un nombre simbólico el entero que corresponde a cada tecla. Las constantes para las flechas son: VK_UP, VK_DOWN, VK_LEFT y VK_RIGHT.
Nuestras implementaciones de keyPressed() y keyReleased() obtienen el key code de la tecla correspondiente y se lo pasan al método actualiza(): un método privado de nuestra clase anónima. También le pasan un valor booleano que es verdadero cuando se presiona una tecla y falso al soltarla. El método actualiza() simplemente usa un switch para ver si el key code de la tecla es el de alguna de las flechas y, en caso de que sea así, almacena el valor booleano en la variable correspondiente.
Una venta de Swing puede contener varios componentes. En un momento dado sólo uno de esos componentes es el componente activo. Es decir que únicamente uno de esos componentes es el que está interactuando con el usuario (imaginate una ventana con varios campos de texto, lo que escribes aparece solamente en uno de esos campos). Típicamente, el usuario selecciona el componente activo con un click del mouse.
En Swing, cuando un componente es el componente activo se dice que "está enfocado" (es el foco de antención). Para que un componente pueda estar enfocado necesitamos indicarle a Swing que es un componente enfocable (focusable). Eso es lo que hacemos con la última linea del constructor:
setFocusable(true);
Física interactiva
El segundo cambio del programa es que ahora la física tiene que tomar en cuenta si alguna de las teclas de las flechas está presionada. Este es el nuevo método para calcular la física:
private void fisica(float dt) {
vx = 0;
vy = 0;
if (arriba)
vy = -300;
if (abajo)
vy = 300;
if (izquierda)
vx = -300;
if (derecha)
vx = 300;
x = clamp(x + vx * dt, 0, ANCHO - DIAMETRO);
y = clamp(y + vy * dt, 0, ALTO - DIAMETRO);
}
Empezamos por asumir que la velocidad, tanto en su componente horizontal como en su componente vertical, es cero. Después checamos las variables booleanas que nos indican si se está presionada la tecla de alguna de las flechas y, cuando eso ocurre, modificamos el componente correspondiente del vector velocidad.
Ya que actualizamos los componentes horizontal y vertical del vector velocidad, de acuerdo a las teclas que están presionadas, los empleamos para calcular la nueva posición de la bola. Al igual que en el ejemplo anterior, multiplicamos la velocidad por el tiempo transcurrido y le sumamos el resultado a la posición actual empleando las expresiones x + vx * dt y y + vy * dt. Aquí también debemos tener cuidado de que que la bola no se salga de la ventana. Esta vez decidimos emplear un método llamado clamp() para asegurar que los valores de las variables x y y se mantengan dentro de los límites válidos. El método clamp() espera tres argumentos: el nuevo valor, el límite inferior y el limite superior. Cuando el nuevo valor está dentro de los límites, el método clamp() simplemente devuelve ese mismo valor. Pero, cuando se sale de esos límites, devuelve el límite inferior si el nuevo valor era menor que él o el límite superior si el nuevo valor era mayor que él. En inglés la palabra clamp significa abrazadera y también existe el verbo to clamp que significa asegurar: fijar sólidamente. Estamos fijando los valores dentro de un rango válido, en el caso horizontal el valor mínimo de x es cero (cuando la bola está en el extremo izquierdo de la ventana) y el valor máximo es ANCHO - DIAMETRO (cuando la bola está en el extremo derecho de la ventana).
Y lo único que falta es la implementación del método clamp(). Este método es tan sencillo que no requiere ninguna explicación:
private float clamp(float valor, float min, float max) {
if (valor > max)
return max;
if (valor < min)
return min;
return valor;
}
En diagonal, la bola se mueve más rápido
Ejecuta el programa Demo2 y prueba lo que pasa cuando presionas simulatáneamente la fecha derecha y la flecha abajo. Al combinar el movimiento horizontal con el movimiento vertical, la bola se mueve en diagonal. Y no sólo eso, prueba mover la bola sólo horizontalmente o verticalmente y después vuelvela a mover en diagonal. ¿Viste lo que pasa? ¡La bola va más rápido cuando se mueve en diagonal! Esto se debe a que la velocidad en diagonal es la suma de sus componentes horizontal y vertical. Si la bola se mueve de 300 pixeles hacia la derecha y, al mismo tiempo, se mueve de 300 pixeles hacia abajo, entonces la distancia que recorrio fue de más de 300 pixeles. Pero no fue de 600 pixeles, porque los desplazamientos de 300 pixeles fueron en direcciones diferentes.
Para calcular cual es la velocidad de la bola cuando se mueve en diagonal, podemos emplear el Teorema de Pitágoras: en un triángulo rectángulo la suma de los cuadrados de los catetos es igual al cuadrado de la hipotenusa. En este caso, las velocidades horizontal y vertical forman los catetos del triángulo, mientras que la suma de esas dos velocidades (la diagonal) es la hipotenusa:

Empleando el Teorema de Pitágoras, calculamos que la velocidad en diagonal de la bola es de:

424.264 pixeles/segundo. Un 41% más rápido.
En algún otro artículo explicaremos cómo hacer para que la bola vaya siempre a la misma velocidad, independientemente de la dirección en la que se esté moviendo.
Double buffering
Algo que todavía no hemos mencionado es que cuando redibujamos el contenido de la ventana hay que evitar que este sea visible mientras que estamos en medio del proceso de redibujado. No queremos que el jugador vea una imagen que todavía no está completa. Imaginate que tenemos algún juego en el cual aparecen cien bolas en la pantalla, no quieres que se vea la imagen cuando apenas haz dibujado la mitad de las bolas en su nueva posición. Para lograr esto se emplea una técnica que se llama double buffering. Esta técnica consiste en hacer todo el redibujado sobre una imagen que tenemos en memoria y, al terminar de redibujar, mandar esa imagen (donde ya está todo dibujado) a la pantalla. De esta manera el jugador nunca ve una imagen que esta dibujada "a medias".
Un detalle interesante de Swing es que dentro de su operación normal de redibujado ya está incluido el double buffering y no es necesario programarlo explícitamente. Nuestro programa, tal como está, emplea double buffering para redibujar el contenido de la ventana.

hace 1 año 12 semanas
hace 1 año 19 semanas
hace 1 año 20 semanas
hace 1 año 20 semanas
hace 1 año 20 semanas
hace 1 año 20 semanas
hace 1 año 20 semanas
hace 1 año 20 semanas
hace 1 año 20 semanas
hace 1 año 20 semanas