Programación de juegos para móviles con J2ME

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_POUNDTecla �almohadilla�
KEY_STARTecla asterisco
GAME_A, GAME_B, GAME_C, GAME_DTeclas especiales de juego
UPArriba
DOWNAbajo
LEFTIzquierda
RIGHTDerecha
FIREDisparo

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<getWidth() && 
        hero.getY()+deltaY>0 && hero.getY()+deltaY<getHeight()) {

        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<yTiles ; i++) {
    for (j=0 ; j<xTiles ; 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<getWidth() && 
            hero.getY()+deltaY>0 && hero.getY()+deltaY<getHeight()) {
                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<yTiles ; i++) {
            for (j=0 ; j<xTiles ; 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);
    }
}

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP
SIGUIENTE ARTÍCULO