Un Universo en tu móvil.Ahora que hemos desarrollado una herramienta para el control de sprites, vamos a
aprender a sacarle partido. Con nuestra librería seremos capaces de mostrar en la
pantalla del dispositivo todo lo que va ocurriendo en el juego, pero también hemos
de ser capaces de leer la información desde el teclado del móvil para responder a
las instrucciones que da el jugador. También es importante que el movimiento del juego sea
suave y suficientement
e rápido. En este capítulo examinaremos las capacidades de
animación de los midlets, incluido el scrolling, así como la interfaz con el teclado.
Animando nuestro aviónLectura del teclado
Toda aplicación interactiva necesita un medio para comunicarse con el usuario. Vamos a
utilizar para ello tres métodos que nos ofrece la clase Canvas. Los métodos keyPressed(),
keyReleased() y keyRepeated(). Estos métodos son llamados cuando se produce un
evento relacionado con la pulsación de una tecla. keyPressed() es llamado cuando se
produce la pulsación de una tecla, y cuando soltamos la tecla es invocado el método
keyReleased(). El método keyRepeated() es invocado de forma repetitiva cuando
dejamos una tecla pulsada.
Los tres métodos recogen como parámetro un número entero, que es el código unicode de
la tecla pulsada. La clase Canvas también nos ofrece el método getGameAction(), que
convertirá el código a una constante independiente del fabricante del dispositivo. La siguiente
tabla, muestra una lista de constantes de códigos estándar.
KEY_NUM0, KEY_NUM1, KEY_NUM2,
KEY_NUM3, KEY_NUM4, KEY_NUM5,
KEY_NUM6, KEY_NUM7, KEY_NUM8,
KEY_NUM9
Teclas numéricas
KEY_POUND Tecla ‘almohadilla’
KEY_STAR Tecla asterisco
GAME_A, GAME_B, GAME_C, GAME_D Teclas especiales de juego
UP Arriba
DOWN Abajo
LEFT Izquierda
RIGHT Derecha
FIRE Disparo
Los fabricantes de dispositivos móviles suelen reservar unas teclas con funciones más o
menos precisas de forma que todos los juegos se controlen de forma similar. Otros, como el
caso del Nokia 7650 ofrecen un mini-joystick. Usando las constantes de la tabla anterior,
podemos abstraernos de las peculiaridades de cada fabricante. Por ejemplo, en el Nokia
7650, cuando movamos el joystick hacia arriba se generara el código UP.
Vemos un ejemplo de uso:
public void keyPressed(int keyCode) {
int action=getGameAction(keyCode);
switch (action) {
case FIRE:
// Disparar
break;
case LEFT:
// Mover a la izquierda
break;
case RIGHT:
// Mover a la derecha
break;
case UP:
// Mover hacia arriba
break;
case DOWN:
// Mover hacia abajo
break;
}
}
Puede parecer lógico utilizar keyRepeated() para controlar un sprite en la pantalla, ya
que nos interesa que mientras pulsemos una tecla, este se mantenga en movimiento. En
principio esta sería la manera correcta de hacerlo, pero en la practica, no todos los dispositivos
soportan la autorepetición de teclas (incluido el emulador de Sun). Vamos a solucionarlo con
el uso de los otros dos métodos. Lo que queremos conseguir es que en el intervalo de tiempo
que el jugador está pulsando una tecla, se mantenga la animación. Este intervalo de tiempo es
precisamente el transcurrido entre que se produce la llamada al método keyPressed() y la
llamada a keyReleased(). Un poco más abajo veremos como se implementa esta técnica.
Threads
Comenzamos este libro con una introducción a Java. De forma intencionada, y debido a lo
voluminoso que es el lenguaje Java, algunos temas no fueron cubiertos. Uno de estos temas
fueron los threads. Vamos a verlos someramente, ahora que ya estamos algo más
familiarizados con el lenguaje, y lo utilizaremos en nuestro juego.
Muy probablemente el sistema operativo que utilizas tiene capacidades de multiproceso o
multitarea. En un sistema de este tipo, puedes ejecutar varias aplicaciones al mismo tiempo. A
cada una de estas aplicaciones las denominamos procesos. Podemos decir que el sistema
operativo es capaz de ejecutar múltiples procesos simultáneamente. Sin embargo, en
ocasiones es interesante que dentro de proceso se lancen uno o más subprocesos de forma
simultánea. Vamos a utilizar un ejemplo para aclarar el concepto. Piensa en tu navegador web
favorito. Cuando lo lanzas, es un proceso más dentro de la lista de procesos que se estan
ejecutando en el sistema operativo. Ahora, supongamos que cargamos en el navegador una
web llena de imágenes, e incluso algunas de ellas animadas. Si observas el proceso de carga,
verás que no se cargan de forma secuencial una tras otra, sino que comienzan a cargarse
varias a la vez. Esto es debido a que el proceso del navegador lanza varios subprocesos, uno
por cada imagen, que se encargan de cargarlas, y en su caso, de animarlas de forma
independiente al resto de imágenes. Cada uno de estos subprocesos se denomina thread
(hilo o hebra en castellano).
En Java, un thread puede estar en cuatro estados posibles.
• Ejecutándose: Está ejecutándose.
• Preparado: Está preparado para pasar al estado de ejecución.
• Suspendido: En espera de algún evento.
• Terminado: Se ha finalizado la ejecución.
La clase que da soporte para los threads en Java es java.lang.Thre
ad. En todo
momento podremos tener acceso al thread que está en ejecución usando el método
Thread.current
Thread().
Para que una clase pueda ser ejecutada como un thread ha de implementar la interfaz
java.lang.Runn
able, en concreto, el método run(). Éste es el método que se ejecutará
cuando lancemos el thread:
public class Hilo implements Runnable {
public void run(){
// código del thread
}
}
Para arrancar un thread usamos su método start().
// Creamos el objeto (que implementa Runable)
Hilo miHilo = new Hilo();
// Creamos un objeto de la clase Thread
// Al que pasamos como parámetro al objeto miHilo
Thread miThread = new Thread( miHilo );
// Arrancamos el thread
miThread.start();
Si sólo vamos a utilizar el thread una vez y no lo vamos a reutilizar, siempre podemos
simplificarlo.
Hilo miHilo = new Hilo();
new Thread(miHilo).start();
La clase Thread nos ofrece algunos métodos más, pero los más interesantes son stop(),
que permite finalizar un thread, y sleep(int time), que lo detiene durante los
milisegundos que le indiquemos como parámetro.
El Game Loop
Cuando jugamos a un juego parece que todo pasa a la vez, en el mismo instante, sin
embargo, sabemos que un procesador sólo puede realizar una acción a la vez. La clave es
realizar cada una de las acciones tan rápidamente como sea posible y pasar a la siguiente, de
forma que todas se completen antes de visualizar el siguiente frame del juego.
El “game loop” o bucle de juego es el encargado de “dirigir” en cada momento que tarea se
está realizando. En la figura 6.1. podemos ver un ejemplo de game loop, y aunque más o
menos todos son similares, no tienen por que tener exactamente la misma estructura.
Analicemos el ejemplo.
Lo primero que hacemos es leer los dispositivos de entrada para ver si el jugador ha
realizado alguna acción. Si hubo alguna acción por parte del jugador, el siguiente paso es
procesarla, esto es, actualizar su posición, disparar, etc..., dependiendo de qué acción sea. En
el siguiente paso realizamos la lógica de juego, es decir, todo aquello que forma parte de la
acción y que no queda bajo control del jugador, por ejemplo, el movimiento de los enemigos,
cálculo de trayectoria de sus disparos, comprobación de colisiones entre la nave enemiga y la
del jugador, etc... Fuera de la lógica del juego quedan otras tareas que realizamos en la
siguiente fase, como son actualizar el scroll de fondo (si lo hubiera), activar sonidos (si fuera necesario), realizar trabajos de sincronización, etc.. Ya por último, nos resta volcar todo a la
pantalla y mostrar el siguiente frame. Esta fase es llamada “fase de render”.
Normalmente, el game loop tendrá un aspecto similar a lo siguiente:
int done = 0;
while (!done) {
// Leer entrada
// Procesar entrada
// Lógica de juego
// Otras tareas
// Mostrar frame
}
Antes de que entremos en el game loop, tendremos que realizar múltiples tareas, como
inicializar todas las estructuras de datos, etc...
El siguiente ejemplo es mucho más realista. Está implementado en un thread.
public void run() {
iniciar();
while (true) {
// Actualizar fondo de pantalla
doScroll();
// Actualizar posición del jugador
computePlayer();
// Actualizar pantalla
repaint();
serviceRepaints();
try {
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
System.out.println(e.toString());
}
}
}
Lo primero que hacemos es inicializar el estado del juego. Seguidamente entramos en el
bucle principal del juego o game loop propiamente dicho. En este caso, es un bucle infinito,
pero en un juego real, tendríamos que poder salir usando una variable booleana que se
activara al producirse la destrucción de nuestro avión o cualquier otro evento que suponga la
salida del juego.
Ya dentro del bucle, lo que hacemos es actualizar el fondo de pantalla -en la siguiente
sección entraremos en los detalles de este proceso-, a continuación, calculamos la posición
de nuestro avión para posteriormente forzar un repintado de la pantalla con una llamada a
repaint() y serviceRepaint
s(). Por último, utilizamos el método sleep()
perteneciente a la clase Thread para introducir un pequeño retardo. Este retardo habrá de
ajustarse a la velocidad del dispositivo en que ejecutemos el juego.
Movimiento del avión
Para mover nuestro avión utilizaremos, como comentamos en la sección dedicada a la
lectura del teclado, los métodos keyPressed() y keyReleased(). Concretamente, lo que
vamos a hacer es utilizar dos variables para almacenar el factor de incremento a aplicar en el
movimiento de nuestro avión en cada vuelta del bucle del juego. Estas variables son deltaX
y deltaY para el movimiento horizontal y vertical, respectivament
e.
public void keyReleased(int keyCode) {
int action=getGameAction(keyCode);
switch (action) {
case LEFT:
deltaX=0;
break;
case RIGHT:
deltaX=0;
break;
case UP:
deltaY=0;
break;
case DOWN:
deltaY=0;
break;
}
}
public void keyPressed(int keyCode) {
int action=getGameAction(keyCode);
switch (action) {
case LEFT:
deltaX=-5;
break;
case RIGHT:
deltaX=5;
break;
case UP:
deltaY=-5;
break;
case DOWN:
deltaY=5;
break;
}
}