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
suficientemente 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ón
Lectura 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.
Constantes | Teclas |
---|---|
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.Thread. En todo momento podremos tener acceso al thread que está en ejecución usando el método Thread.currentThread(). Para que una clase pueda ser ejecutada como un thread ha de implementar la interfaz java.lang.Runnable, 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 serviceRepaints(). 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, respectivamente.
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; } }
Cuando pulsamos una
tecla, asignamos el valor correspondiente al desplazamiento deseado,
por ejemplo, si queremos mover el avión a la derecha, el valor asignado
a deltaX será 5. Esto significa que en cada vuelta del game
loop, sumaremos 5 a la posición de avión, es decir, se desplazará 5
píxeles a la derecha. Cuando se suelta la tecla, inicializamos a 0 la
variable, es decir, detenemos el movimiento.
La función encargada de calcular la nueva posición del avión es, pues, bastante sencilla.
void computePlayer() { // actualizar posición del avión if (hero.getX()+deltaX>0 && hero.getX()+deltaX hero.getY()+deltaY>0 && hero.getY()+deltaY hero.setX(hero.getX()+deltaX); hero.setY(hero.getY()+deltaY); } }
Simplemente sumamos deltaX a la posición X del avión y deltaY a la posición Y. Antes comprobamos que el avión no sale de los límites de la pantalla.
Construyendo el mapa del juego
En
nuestro juego vamos a utilizar una técnica basada en tiles para
construir el mapa. La traducción de la palabra tile es baldosa o
azulejo. Esto nos da una idea de en qué consiste la técnica: construir
la imagen a mostrar en la pantalla mediante tiles de forma cuadrada,
como si enlosáramos una pared. Mediante tiles distintos podemos formar
cualquier imagen. La siguiente figura pertenece al juego Uridium, un
juego de naves o shooter de los años 80 parecido al que vamos a
desarrollar para ordenadores de 8 bits.
Las líneas rojas dividen los tiles empleados para construir la imagen.
En
nuestro juego manejaremos mapas sencillos que van a estar compuestos
por mosaicos de tiles simples. Algunos juegos tienen varios niveles de
tiles (llamados capas). Por ahora, vamos a almacenar la información
sobre nuestro mapa en un array de enteros tal como éste:
int map[]= {1,1,1,1,1,1,1, 1,1,1,1,1,1,1, 1,2,1,1,1,1,1, 1,1,1,4,1,1,1, 1,1,1,1,1,1,1, 1,1,3,1,2,1,1, 1,4,1,1,1,1,1};
Este array representa un mapa de 7x7 tiles. Vamos a utilizar los siguientes tiles, cada uno con un tamaño de 32x32 píxeles.
Para cargar y manejar los tiles nos apoyamos en la librería de manejo de sprites que desarrollamos en el capítulo anterior.
private Sprite[] tile=new Sprite[5]; // Inicializamos los tiles for (i=1 ; i<=4 ; i++) { tile[i]=new Sprite(1); tile[i].on(); } tile[1].addFrame(1,"/tile1.png"); tile[2].addFrame(1,"/tile2.png"); tile[3].addFrame(1,"/tile3.png"); tile[4].addFrame(1,"/tile4.png");
Hemos creado un array de sprites, uno por cada tile que vamos a cargar.
El
proceso de representación del escenario consiste en ir leyendo el mapa y
dibujar el sprite leído en la posición correspondiente. El siguiente
código realiza este proceso:
// Dibujar fondo for (i=0 ; i<7 ; i++) { for (j=0 ; j<7 ; j++) { t=map[i*xTiles+j]; // calculo de la posición del tile x=j*32; y=(i-1)*32; // dibujamos el tile tile[t].setX(x); tile[t].setY(y); tile[t].draw(g); } }
El mapa es de 7x7, así que los dos primero bucles se encargan de recorrer los tiles. La variable t,
almacena el valor del tile en cada momento. El cálculo de la
coordenada de la pantalla en la que debemos dibujar el tile tampoco es
complicada. Al tener cada tile 32x32 píxeles, sólo hay que multiplicar
por 32 el valor de los contadores i o j, correspondiente a los bucles, para obtener la coordenada de pantalla.
Scrolling
Con
lo que hemos visto hasta ahora, somos capaces de controlar nuestro
avión por la pantalla, y mostrar el fondo del juego mediante la técnica
de los tiles que acabamos de ver. Pero un fondo estático no es
demasiado atractivo, además, un mapa de 7X7 no da demasiado juego,
necesitamos un mapa más grande. Para el caso de nuestro juego será de
7X20.
// Mapa del juego int map[]= {1,1,1,1,1,1,1, 1,1,1,1,1,1,1, 1,2,1,1,1,1,1, 1,1,1,4,1,1,1, 1,1,1,1,1,1,1, 1,1,3,1,2,1,1, 1,1,1,1,1,1,1, 1,4,1,1,1,1,1, 1,1,1,1,3,1,1, 1,1,1,1,1,1,1, 1,4,1,1,1,1,1, 1,1,1,3,1,1,1, 1,1,1,1,1,1,1, 1,1,1,1,1,1,1, 1,2,1,1,1,1,1, 1,1,1,4,1,1,1, 1,1,1,1,1,1,1, 1,1,3,1,2,1,1, 1,1,1,1,1,1,1, 1,4,1,1,1,1,1};
Se
hace evidente que un mapa de 7x20 tiles no cabe en la pantalla. Lo
lógico es que según se mueva nuestro avión por el escenario, el mapa
avance en la misma dirección. Este desplazamiento del escenario se
llama scrolling. Si nuestro avión avanza un tile en la pantalla hemos
de dibujar el escenario pero desplazado (offset) un tile.
Desafortunamente, haciendo esto exclusivamente veríamos como el
escenario va dando saltitos, y lo que buscamos es un scroll suave.
Necesitamos dos variables que nos indiquen a partir de que tile debemos
dibujar el mapa en pantalla (la llamaremos indice) y otra variable que
nos indique, dentro del tile actual, cuánto se ha desplazado (la
llamaremos indice_in). Las variables xTile y yTile contendrán el número
de tiles horizontales y verticales respectivamente que caben en
pantalla. El siguiente fragmento de código cumple este cometido:
void doScroll() { // movimiento del escenario (scroll) indice_in+=2; if (indice_in>=32) { indice_in=0; indice-=xTiles; } if (indice <= 0) { // si llegamos al final, empezamos de nuevo. indice=map.length-(xTiles*yTiles); indice_in=0; } }
El código para dibujar el fondo quedaría de la siguiente manera:
// Dibujar fondo for (i=0 ; i for (j=0 ; j t=map[indice+(i*xTiles+j)]; // calculo de la posición del tile x=j*32; y=(i-1)*32+indice_in; // dibujamos el tile tile[t].setX(x); tile[t].setY(y); tile[t].draw(g); } }
Como diferencia encontramos dos nuevas variables. La variable indice contiene el desplazamiento (en bytes) a partir del cual se comienza a dibujar el mapa. La variable indice_in, es la encargada de realizar el scroll fino. Al dibujar el tile, se le suma a la coordenada Y el valor de la variable indice_in,
que va aumentando en cada iteración (2 píxeles). Cuando esta variable
alcanza el valor 32, es decir, la altura del tile, ponemos la variable a
0 y restamos el número de tiles horizonlates del mapa a la variable
indice, o lo que es lo mismo, el offset a partir del que dibujamos el
mapa. Se realiza una resta porque la lectura del mapa la hacemos de
abajo a arriba (del último byte al primero). Recuerda que el mapa tiene
7 tiles de anchura, es por eso que restamos xTiles (que ha de
valer 7) a la variable indice. Una vez que llegamos al principio del
mapa, comenzamos de nuevo por el final, de forma que se va repitiendo
el mismo mapa de forma indefinida. Vamos a suponer que el dispositivo
es capaz de mostrar 8 líneas de tiles verticalmente (yTiles
vale 8). Si puede mostrar menos, no hay problema alguno. El problema
será que la pantalla pueda mostrar más, es decir, sea mayor de lo que
hemos supuesto. En ese caso aumentaremos la variable yTiles.
Un valor de 8 es lo suficientemente grande para la mayoría de los
dispositivos. Ten cuenta que las primeras 8 filas del mapa tienen que
ser iguales que las 8 últimas si no quieres notar un molesto salto
cuando recomienza el recorrido del mapa.
import javax.microedition.midlet.*; import javax.microedition.lcdui.*; public class Scrolling extends MIDlet implements CommandListener { private Command exitCommand; private Display display; private SSCanvas screen; public Scrolling() { display=Display.getDisplay(this); exitCommand = new Command("Salir",Command.SCREEN,2); screen=new SSCanvas(); screen.addCommand(exitCommand); screen.setCommandListener(this); new Thread(screen).start(); } public void startApp() throws MIDletStateChangeException { display.setCurrent(screen); } public void pauseApp() {} public void destroyApp(boolean unconditional) {} public void commandAction(Command c, Displayable s) { if (c == exitCommand) { destroyApp(false); notifyDestroyed(); } } }
class SSCanvas extends Canvas implements Runnable { private int indice_in, indice, xTiles, yTiles, sleepTime; private int deltaX,deltaY; private Sprite hero=new Sprite(1); private Sprite[] tile=new Sprite[5]; // Mapa del juego int map[] ={ 1,1,1,1,1,1,1, 1,1,1,1,1,1,1, 1,2,1,1,1,1,1, 1,1,1,4,1,1,1, 1,1,1,1,1,1,1, 1,1,3,1,2,1,1, 1,1,1,1,1,1,1, 1,4,1,1,1,1,1, 1,1,1,1,3,1,1, 1,1,1,1,1,1,1, 1,4,1,1,1,1,1, 1,1,1,3,1,1,1, 1,1,1,1,1,1,1, 1,1,1,1,1,1,1, 1,2,1,1,1,1,1, 1,1,1,4,1,1,1, 1,1,1,1,1,1,1, 1,1,3,1,2,1,1, 1,1,1,1,1,1,1, 1,4,1,1,1,1,1}; public SSCanvas() { // Cargamos los sprites hero.addFrame(1,"/hero.png"); // Iniciamos los Sprites hero.on(); } void iniciar() { int i; sleepTime = 50; hero.setX(getWidth()/2); hero.setY(getHeight()-20); deltaX=0; deltaY=0; xTiles=7; yTiles=8; indice=map.length-(xTiles*yTiles); indice_in=0; // Inicializamos los tiles for (i=1 ; i<=4 ; i++) { tile[i]=new Sprite(1); tile[i].on(); } tile[1].addFrame(1,"/tile1.png"); tile[2].addFrame(1,"/tile2.png"); tile[3].addFrame(1,"/tile3.png"); tile[4].addFrame(1,"/tile4.png"); } void doScroll() { // movimiento del scenario (scroll) indice_in+=2; if (indice_in>=32) { indice_in=0; indice-=xTiles; } if (indice <= 0) { // si llegamos al final, empezamos de nuevo. indice=map.length-(xTiles*yTiles); indice_in=0; } } void computePlayer() { // actualizar posición del avión if (hero.getX()+deltaX>0 && hero.getX()+deltaX hero.getY()+deltaY>0 && hero.getY()+deltaY hero.setX(hero.getX()+deltaX); hero.setY(hero.getY()+deltaY); } } // thread que contiene el game loop 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()); } } } 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; } } public void paint(Graphics g) { int x=0,y=0,t=0; int i,j; g.setColor(255,255,255); g.fillRect(0,0,getWidth(),getHeight()); g.setColor(200,200,0); // Dibujar fondo for (i=0 ; i for (j=0 ; j t=map[indice+(i*xTiles+j)]; // calculo de la posición del tile x=j*32; y=(i-1)*32+indice_in; // dibujamos el tile tile[t].setX(x); tile[t].setY(y); tile[t].draw(g); } } // Dibujar el jugador hero.setX(hero.getX()); hero.setY(hero.getY()); hero.draw(g); } }
No hay comentarios.:
Publicar un comentario