martes, 13 de enero de 2015

Programación de juegos para móviles con J2ME Parte VIII

Enemigos, disparos y explosiones

Existen multiples técnicas relacionadas con la inteligencia artificial (IA) y que son ampliamente utilizadas en programación de juegos. La IA es un tópico lo suficientemente extenso como para rellenar varios libros del tamaño del que tienes ahora entre manos. Aún así, exploraremos algunas sencillas técnicas que nos permitiran dotar a los aviones enemigos de nuestro juego de una chispa vital. También haremos diaparar a los aviones enemigos y al nuestro, explosiones incluidas.

Tipos de Inteligencia

Hay, al menos, tres tendencias dentro del campo de la inteligencia artificial.
  • Redes neuronales
  • Algoritmos de búsqueda
  • Sistemas basados en conocimiento
Son tres enfoque diferentes que tratan de buscar un fin común. No hay un enfoque mejor que los demás, la elección de uno u otro depende de la aplicación.
Una red neuronal trata de simular el funcionamiento del cerebro humano. El elemento básico de una red neuronal es la neurona. En una red neuronal, un conjunto de neuronas trabajan al unísono para resolver un problema. Al igual que un niño tiene que aprender al nacer, una red de neuronas artificial tiene que ser entrenada para poder realizar su cometido. Este aprendizaje puede ser supervisado o no supervisado, dependiendo si hace falta intervención humana para entrenar a la red de neuronas. Este entrenamiento se realiza normalmente mediante ejemplos. La aplicación de las redes neuronales es efectiva en campos en los que no existen algoritmos concretos que resuelvan un problema o sean demasiado complejos de computar. Donde más se aplican es en problemas de reconocimiento de patrones y pronósticos.
El segundo enfoque es el de los algoritmos de búsqueda. Es necesario un conocimiento razonable sobre estructuras de datos como árboles y grafos. Una de las aplicaciones interesantes, sobre todo para videojuegos, es la búsqueda de caminos (pathfinding). Seguramente has jugado a juegos de estrategia como Starcraft, Age of Empires y otros del estilo. Puedes observar que cuando das la orden de movimiento a uno de los pequeños personajes del juego, éste se dirige al punto indicado esquivando los obstáculos que encuantra en su camino. Este algoritmo de búsqueda en grafos es llamado A*. La figura 7.1. es un ejemplo de árbol. Supongamos que es el mapa de un juego en el que nuestra misión es escapar de una casa.
Cada círculo representa un nodo del árbol. El número que encierra es el número de habitación. Si quisieramos encontrar la salida, usaríamos un algoritmo de búsqueda (por ejemplo A*) para recorrer todos los posibles caminos y quedarnos con el que nos interesa. El objetivo es buscar el nodo 8, que es el que tiene la puerta de salida. El camino desde la habitación 1 es: 1 4 5 8. El algoritmo A* además de encontrar el nodo objetivo, nos asegura que es el camino más corto. No vamos a entrar en más detalle, ya que cae fuera de las pretensiones de este libro profundizar en la implementación de algoritmos de búsqueda. Por último, los sistemas basados en reglas se sirven, valga la redundancia, de conjuntos de reglas y hechos. Los hechos son informaciones relativas al problema y a su universo. Las reglas son precisamente eso, reglas aplicables a los elementos del universo y que permiten llegar a deducciones simples. Veamos un ejemplo:
    Hechos: Las moscas tienen alas. Las hormigas no tienen alas. Reglas: Si (x) tiene alas, entonces vuela.
Un sistema basado en conocimiento, con estas reglas y estos hechos es capaz de deducir dos cosas. Que las moscas vuelan y que las hormigas no.
    Si (la mosca) tiene alas, entonces vuela.
Uno de los problemas de los sistemas basados en conocimiento es que pueden ocurrir situaciones como estas.
    Si (la gallina) tiene alas, entonces vuela.
Desgraciadamente para las gallinas, éstas no vuelan. Puedes observar que la construcción para comprobar reglas es muy similar a la construcción IF/THEN de los lenguajes de programación.

Comportamientos y máquinas de estado

Una máquina de estados está compuesta por una serie de estados y una serie de reglas que indican en que casos se pasa de un estado a otro. Estas máquinas de estados nos perniten modelar comportamientos en los personajes y elementos del juego. Vamos a ilustrarlo con un ejemplo. Imagina que en el hipotético juego que hemos planteado unas líneas más arriba hay un zombie. El pobre no tiene una inteligencia demasiado desarrollada y sólo es capaz de andar hasta que se pega contra la pared. Cuando sucede esto, lo único que sabe hacer es girar 45 grados a la derecha y continuar andando. Vamos a modelar el comportamiento del zombie con una máquina de estados. Para ello primero tenemos que definir los posibles estados.
  • Andando (estado 1)
  • Girando (estado 2)
Las reglas que hacen que el zombie cambie de un estado a otro son las siguientes.
  • Si está en el estado 1 y choca con la pared pasa al estado 2.
  • Si esta en el estado 2 y ha girado 45 grados pasa al estado 1.
Con estos estados y estas reglas podemos construir el grafo que representa a nuestra máquina de estados.

La implementación de la máquina de estado es muy sencilla. La siguiente función simula el comportamiento del zombie.
int angulo;

void zombie() {
    int state, angulo_tmp;
    // estado 1
    if (state == 1) {
        andar();
        if (colision()) {
            state=2;
            angulo_tmp=45;
        }
    }

    // estado 2
    if (state == 2) {
        angulo_tmp=angulo_tmp-1;
        angulo=angulo+1;
        if (angulo_tmp <= 0) {
            state=1;
        }
    }
}
Éste es un ejemplo bastante sencillo, sin embargo utilizando este método podrás crear comportamientos inteligentes de cierta complejidad.

Enemigos

En nuestro juego vamos a tener dos tipos de aviones enemigos. El primero de ellos es un avión que cruzará la pantalla en diagonal, en dirección de nuestro avión. Al alcanzar una distancia suficiente a nosotros, disparará un proyectil. El segundo enemigo es algo menos violento, ya que no disparará. Sin embargo, al alcanzar cierta posición de la pantalla realizará un peligroso cambio de treyectoria que nos puede pillar desprevenidos. He aquí las máquinas de estado de ambos comprtamientos.

Para representar las naves enemigas, crearemos una clase llamada Enemy. Como al fin y al cabo, las naves enemigas no dejan de ser sprites, vamos a crear la clase Enemy heredando los métodos y atributos de la clase Sprite y añadiendo aquello que necesitemos.
class Enemy extends Sprite {

    private int type,state,deltaX,deltaY;

    public void setState(int state) {
        this.state=state;
    }

    public int getState(int state) {
        return state;
    }

    public void setType(int type) {
        this.type=type;
    }

    public int getType() {
        return type;
    }

    public void doMovement() {
        Random random = new java.util.Random();
        // Los enemigos de tipo 2 cambiaran su trayectoria
        // al alcanzar una posición determinada (pos. 50)
        if (type == 2 && getY() > 50 && state != 2) {
            // paso al estado 2 (movimiento diagonal)
            state = 2;

            if ((Math.abs(random.nextInt()) % 2) + 1 == 1) {
                deltaX=2; 
            } else {
                deltaX=-2; 
            }
        }

        // movemos la nave
        setX(getX()+deltaX);
        setY(getY()+deltaY);
    }

    public void init(int xhero) {
        deltaY=3;
        deltaX=0;

        if (type == 1) {
            if (xhero > getX()) {
                deltaX=2;
            } else {
                deltaX=-2;
            }
        }
    }

    // Sobrecarga del método draw de la clase Sprite
    public void draw (javax.microedition.lcdui.Graphics g) {
        selFrame(type);
        // llamamos al método 'draw' de la clase padre (Sprite)
        super.draw(g);
    }

    public Enemy(int nFrames) {
        super(nFrames);
    }
}
A los atributos de la clase Sprite añadimos cuatro más. El atributo type, indicará cuál es el tipo de enemigo. En nuestro caso hay dos tipos. Para manejar este atributo dotamos a nuestra clase de los métodos getType() y setType() para consultar y establecer el tipo del enemigo.
El atributo state mantendrá el estado de la nave. La nave de tipo 2, es decir la que cambia su trayectoria, tiene dos estado. En el estado 1 simplemente avanza en horizontal. En el estado 2 su trayectoria es diagonal. Para manejar el estado de los enemigos añadimos las clases getState() y setState() para consultar y establecer el estado de los enemigos.
Los dos atributos que nos quedan son deltaX y deltaY, que contienen los desplazamientos en el eje horizontal y vertical respectivamente que se producirá en cada vuelta del game loop.
Al crear una instancia de nuestra clase, lo primero que hemos de hacer en el constructor es llamar a la clase padre, que es Sprite, para pasarle el parámetro que necesita para reservar el número de frames. También para inicializar el sprite.
super(nFrames);
Vamos tambien a sobrecargar el método draw() del método Sprite. En este método, primero seleccionaremos el tipo de avión que vamos a poner en la pantalla según su tipo, después, llamamos al método draw() de la clase padre.
Nuestro enemigo de tipo 1 debe tomar una trayectoria dependiendo de la posicón de nuestro avión. Para ello, necesitamos una forma de comunicarle a la nave enemiga dicha posición. Hemos creado un método llamado init() a la que le pasamos como parámetro la posición actual de nuestro avión. En este método ponemos los atributos deltaX y deltaY a sus valores iniciales.
Por último necesitaremos un método que se encargue de realizar el movimiento de la nave. Este método es doMovement(). Su función principal es actualizar la posición de la nave enemiga según los atributos deltaX y deltaY. También comprobamos la posición del enemigo de tipo 1 para cambiar su estado cuando sea necesario.
Ya disponemos de nuestra clase Enemy para manejar a los aviones enemigos. En nuestro juego permitiremos un máximo de 6 enemigos simultáneos en pantalla (realmente habrá menos), así que creamos un array de elementos de tipo Enemy. El siguiente paso es inicializar cada uno de estos seis elementos. Vamos a cargar dos frames, uno para la nave de tipo 1 y otro para la de tipo 2. Dependiendo del tipo de la nave, seleccionaremos un frame u otro antes de dibujar el avión.
private Enemy[] enemies=new Enemy[7];

// Inicializar enemigos
for (i=1 ; i<=6 ; i++) {
    enemies[i]=new Enemy(2);
    enemies[i].addFrame(1,"/enemy1.png");
    enemies[i].addFrame(2,"/enemy2.png");
    enemies[i].off();
}
Durante el trascurso de nuestro juego aparecerá un enemigo cada 20 ciclos del game loop. Cuando necesitemos crear un enemigo, hay que buscar una posición libre en el array de enemigos. Si hay alguno libre, ponemos su estado inicial (posición, tipo, etc...) de forma aleatoria y lo iniciamos (lo activamos).
// Creamos un enemigo cada 20 ciclos
if (cicle%20 == 0) {
    freeEnemy=0;

    // Buscar un enemigo libre
    for (i=1 ; i<=6 ; i++) {
        if (!enemies[i].isActive()) {
            freeEnemy=i;
        }
    }

    // Asignar enemigo si hay una posición libre
    // en el array de enemigos
    if (freeEnemy != 0) {
        enemies[freeEnemy].on();
        enemies[freeEnemy].setX((Math.abs(random.nextInt()) % getWidth()) + 1);
        enemies[freeEnemy].setY(0);
        enemies[freeEnemy].setState(1);
        enemies[freeEnemy].setType((Math.abs(random.nextInt()) % 2) + 1);
        enemies[freeEnemy].init(hero.getX());
    }
}
En cada ciclo del game loop, hemos de actualizar la posición de cada enemigo y comprobar si ha salido de la pantalla.
// Mover los enemigos
for (i=1 ; i<=6 ; i++) {
    if (enemies[i].isActive()) {
        enemies[i].doMovement();
    }

    // Mirar si la nave salió de la pantalla
    if ((enemies[i].getY() > getHeight()) || (enemies[i].getY() < 0)) {
        enemies[i].off();
    }
}

Disparos y explosiones

Ahora que conocemos una técnica para que nuestros aviones enemigos sean capaces de comportarse tal y como queremos que lo hagan, estamos muy cerca de poder completar nuestro juego. Lo siguiente que vamos a hacer es añadir la capacidad de disparar a nuestro avión. La gestión de los disparos y las explosiones va a ser muy similar a la de los aviones enemigos. Vamos a crear una clase a la que llamaremos Bullet, descendiente de la clase Sprite y que representará un disparo.
class Bullet extends Sprite {
    private int owner;

    public Bullet(int nFrames) {
        super(nFrames);
    }

    public void setOwner(int owner) {
        this.owner=owner;
    }

    public int getOwner() {
        return owner;
    }

    public void doMovement() {
        // si owner = 1 el disparo es nuestro
        // si no, es del enemigo
        if (owner == 1) {
            setY(getY()-6);
        } else {
            setY(getY()+6);
        }
    }

    // Sobrecarga del método draw de la clase Sprite
    public void draw (javax.microedition.lcdui.Graphics g) {
        selFrame(owner);
        // llamamos al método 'draw' de la clase padre (Sprite)
        super.draw(g);
    }
}
Añadimos un atributo llamado owner que indica a quien pertenece el disparo (a un enemigo o a nuestro avión). Para gestionar este atributo disponemos de los métodos getOwner() y setOwner().
Este atributo lo vamos a utilizar para decidir si movemos el disparo hacia arriba o hacia abajo y para comprobar las colisiones.
En cuanto a las explosiones, vamos a crear una clase llamada Explode, también descendiente de Sprite.
class Explode extends Sprite {

    private int state;

    public Explode(int nFrames) {
        super(nFrames);
        state=1;
    }

    public void setState(int state) {
        this.state=state;
    }

    public int getState() {
        return state;
    }

    public void doMovement() {
        state++;
        if (state > super.frames())
            super.off();
    }

    // Sobrecarga del método draw de la clase Sprite
    public void draw (javax.microedition.lcdui.Graphics g) {
        selFrame(state);
        // llamamos al método 'draw' de la clase padre (Sprite)
        super.draw(g);
    }
}
El único atributo que añadimos es state, que nos indicará el estado de la explosión. En la práctica, el estado de la explosión va a ser el frame actual de su animación interna, de hecho, su clase doMovement() lo único que hace es aumentar el frame para realizar la animación.
La inicialización y gestión de explosiones y disparos es idéntica a la de los aviones enemigos, por lo que no vamos a entrar en mucho más detalle.
Nos resta comprobar las colisiones entre los sprites del juego. Vamos a realizar tres comprobaciones.
  • Colisión héroe – enemigo
  • Colisión héroe – disparo
  • Colisión enemigo – disparo
Ten en cuenta que hay 6 posibles enemigos y 6 posibles disparos a la vez en pantalla, por lo que hay que realizar todas las combinaciones posibles.
// Colisión heroe-enemigo
for (i=1 ; i<=6 ; i++) {
if (hero.collide(enemies[i])&&enemies[i].isActive()&&shield == 0) {
createExplode(hero.getX(),hero.getY());
createExplode(enemies[i].getX(),enemies[i].getY());
enemies[i].off();
collision=true;
}
}

// Colisión heroe-disparo
for (i=1 ; i<=6 ; i++) {
if (aBullet[i].isActive() && hero.collide(aBullet[i]) &&
aBullet[i].getOwner() != 1 && shield == 0) {
createExplode(hero.getX(),hero.getY());
aBullet[i].off();
collision=true;
}
}

// colisión enemigo-disparo
for (i=1 ; i<=6 ; i++) {
if (aBullet[i].getOwner() == 1 && aBullet[i].isActive()) {
for (j=1 ; j<=6 ; j++) {
if (enemies[j].isActive()) {
if (aBullet[i].collide(enemies[j])) {
createExplode(enemies[j].getX(),enemies[j].getY());
enemies[j].off();
aBullet[i].off();
score+=10;
}
}
}
}
}

Programación de juegos para móviles con J2ME Parte VII

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);
    }
}

Programación de juegos para móviles con J2ME Parte VI

Sprites

Durante los capítulos siguientes se profundiza en los diferentes aspectos concernientes a la programación de videojuegos. Ya dispones de las herramientas necesarias para emprender la aventura, así que siéntate cómodamente, flexiona tus dedos y prepárate para la diversión. Para ilustrar las técnicas que se describirán en los próximos capítulos desarrollaremos un pequeño videojuego. Va a ser un juego sin grandes pretensiones, pero que nos va a ayudar a entender los diferentes aspectos que encierra este fascinante mundo. Nuestro juego va a consistir en lo que se ha dado en llamar shooter en el argot de los videojuegos. Quizás te resulte más familiar “matamarcianos”. En este tipo de juegos manejamos una nave que tiene que ir destruyendo a todos los enemigos que se pongan en su camino. En nuestro caso, va a estar ambientado en la segunda guerra mundial, y pilotaremos un avión que tendrá que destruir una orda de aviones enemigos. El juego es un homenaje al mítico 1942.

Este capítulo lo vamos a dedicar a los sprites. Seguro que alguna vez has jugado a Space Invaders. En este juego, una pequeña nave situada en la parte inferior de la pantalla dispara a una gran cantidad de naves enemigas que van bajando por la pantalla hacia el jugador. Pues bien, nuestra nave es un sprite, al igual que los enemigos, las balas y los escudos. Podemos decir que un sprite es un elemento gráfico determinado (una nave, un coche, etc...) que tiene entidad propia y sobre la que podemos definir y modificar ciertos atributos, como la posición en la pantalla, si es o no visible, etc... Un sprite, pues, tiene capacidad de movimiento. Distinguimos dos tipos de movimiento en los sprites: el movimiento externo, es decir, el movimiento del sprite por la pantalla, y el movimiento interno o animación.
Para posicionar un sprite en la pantalla hay que especificar sus coordenadas. Es como el juego de los barquitos, en el que para identificar un cuadrante hay que indicar una letra para el eje vertical (lo llamaremos eje Y) y un número para el eje horizontal (al que llamaremos eje X). En un ordenador, un punto en la pantalla se representa de forma parecida. La esquina superior izquierda representa el centro de coordenadas. La figura siguiente muestra el eje de coordenadas en una pantalla con una resolución de 320 por 200 píxeles.

Un punto se identifica dando la distancia en el eje X al lateral izquierdo de la pantalla y la distancia en el eje Y a la parte superior de la pantalla. Las distancias se miden en píxeles. Si queremos indicar que un sprite está a 100 píxeles de distancia del eje vertical y 150 del eje horizontal, decimos que está en la coordenada (100,150).
Imagina ahora que jugamos a un videjuego en el que manejamos a un hombrecillo. Podremos observar cómo mueve las piernas y los brazos según avanza por la pantalla. Éste es el movimiento interno o animación. La siguiente figura muestra la animación del sprite de un gato.

Otra característica muy interesante de los sprites es que nos permiten detectar colisiones entre ellos. Esta capacidad es realmente interesante si queremos conocer cuando nuestro avión ha chocado con un enemigo o con uno de sus misiles.

Control de sprites

Vamos a realizar una pequeña librería (y cuando digo pequeña, quiero decir realmente pequeña) para el manejo de los sprites. Luego utilizaremos esta librería en nuestro juego, por supuesto, también puedes utilizarla en tus propios juegos, así como ampliarla, ya que cubrirá sólo los aspectos básicos en lo referente a sprites.
Dotaremos a nuestra librería con capacidad para movimiento de sprites, animación (un soporte básico) y detección de colisiones.
Para almacenar el estado de los Sprites utilizaremos las siguientes variables.
private int posx,posy;
 private boolean active;
 private int frame,nframes;
 private Image[] sprites;
Necesitamos la coordenada en pantalla del sprite (que almacenamos en posx y posy. La variable active nos servirá para saber si el sprite está activo. La variable frame almacena el frame actual del sprite, y nframes el número total de frames de los que está compuesto. Por último, tenemos un array de objetos Image que contendrá cada uno de los frames del juego.
Como puedes observar no indicamos el tamaño del array, ya que aún no sabemos cuantos frames tendrá el sprite. Indicaremos este valor en el constructor del sprite.
// constructor. 'nframes' es el número de frames del Sprite.
 public Sprite(int nframes) {
  // El Sprite no está activo por defecto.
  active=false;
  frame=1;
  this.nframes=nframes;
  sprites=new Image[nframes+1];
 } 
El constructor se encarga de crear tantos elementos de tipo Image como frames tenga el sprite. También asignamos el estado inicial del sprite.
La operación más importante de un sprite es el movimiento por la pantalla. Veamos los métodos que nos permitirán moverlo.
public void setX(int x) {
  posx=x;
 }

 public void setY(int y) {
  posy=y;
 }

 int getX() {
  return posx;
 }

 int getY() {
  return posy;
 }
Como puedes observar, el código para posicionar el sprite en la pantalla no puede ser más simple. Los métodos setX() y setY() actualizan las variables de estado del sprite (posx,posy). Los métodos getX() y getY() realizan la operación contraria, es decir, nos devuelve la posición del sprite. Además de la posición del sprite, nos va a interesar en determinadas condiciones conocer el tamaño del mismo.
int getW() {
  return sprites[nframes].getWidth();
 }

 int getH() {
  return sprites[nframes].getHeight();
 }
Los métodos getW() y getH() nos devuelven el ancho y el alto del sprite en píxeles. Para ello recurrimos a los métodos getWidth() y getHeigth() de la clase Image.
Otro dato importante del sprite es si está activo en un momento determinado.
public void on() {
  active=true;
 }

 public void off() {
  active=false;
 }

 public boolean isActive() {
  return active;
 }
Necesitaremos un método que active el sprite, al que llamaremos on(), y otro para desactivarlo, que como podrás imaginar, llamaremos off(). Nos resta un método para conocer el estado del sprite. Hemos llamado al método isActive().
En lo referente al estado necesitamos algún método para el control de frames, o lo que es lo mismo, de la animación interna del sprite.
public void selFrame(int frameno) {
  frame=frameno;
 }

 public int frames() {
  return nframes;
 }

 public void addFrame(int frameno, String path) {
  try {
   sprites[frameno]=Image.createImage(path);
  } catch (IOException e) {
   System.err.println("Can`t load the image " + path + ": " + e.toString());
  }
 }
El método selFrame() fija el frame actual del sprite, mientras que el método frame() nos devolverá el número de frames del sprite.
El método addFrame() nos permite añadir frames al sprite. Necesita dos parámetros. El parámetro frameno, indica el número de frame, mientras que el parámetro path indica el camino y el nombre del gráfico que conformará dicho frame.
Para dibujar el sprite, vamos a crear el método draw(). Lo único que hace este método es dibujar el frame actual del sprite en la pantalla.
public void draw(Graphics g) {
 g.drawImage (sprites[frame], posx, posy, Graphics.HCENTER|Graphics.VCENTER);
}
Nos resta dotar a nuestra librería con la capacidad de detectar colisiones entre sprites. La detección de colisiones entre sprites puede enfocarse desde varios puntos de vista. Imaginemos dos sprites, nuestro avión y un disparo enemigo. En cada vuelta del game loop tendremos que comprobar si el disparo ha colisionado con nuestro avión. Podríamos considerar que dos sprites colisionan cuando alguno de sus píxeles visibles (es decir, no transparentes) toca con un píxel cualquiera del otro sprite. Esto es cierto al 100%, sin embargo, la única forma de hacerlo es comprobando uno por uno los píxeles de ambos sprites. Evidentemente esto requiere un gran tiempo de computación, y es inviable en la práctica. En nuestra librería hemos asumido que la parte visible de nuestro sprite coincide más o menos con las dimensiones de la superficie que lo contiene. Si aceptamos esto, y teniendo en cuenta que una superficie tiene forma cuadrangular, la detección de una colisión entre dos sprites se simplifica bastante. Sólo hemos de detectar el caso en el que dos cuadrados se solapen.
En la primera figura no existe colisión, ya que no se solapan las superficies (las superficies están representadas por el cuadrado que rodea al gráfico). La segunda figura muestra el principal problema de este método, ya que nuestra librería considerará que ha habido colisión cuando realmente no ha sido así. A pesar de este pequeño inconveniente, este método de detección de colisiones es el más rápido. Es importante que la superficie tenga el tamaño justo para albergar el gráfico. Este es el aspecto que tiene nuestro método de detección de colisiones.
boolean collide(Sprite sp) {
  int w1,h1,w2,h2,x1,y1,x2,y2;

  w1=getW();  // ancho del sprite1
  h1=getH();  // altura del sprite1
  w2=sp.getW(); // ancho del sprite2
  h2=sp.getH(); // alto del sprite2
  x1=getX();  // pos. X del sprite1
  y1=getY();  // pos. Y del sprite1
  x2=sp.getX(); // pos. X del sprite2
  y2=sp.getY(); // pos. Y del sprite2

  if (((x1+w1)>x2)&&((y1+h1)>y2)&&((x2+w2)>x1)&&((y2+h2)>y1)) {
   return true;
  } else {
   return false;
  }
 }
Se trata de comprobar si el cuadrado (superficie) que contiene el primer sprite, se solapa con el cuadrado que contiene al segundo.
Hay otros métodos más precisos que nos permiten detectar colisiones. Consiste en dividir el sprite en pequeñas superficies rectangulares tal y como muestra la próxima figura.

Se puede observar la mayor precisión de este método. El proceso de detección consiste en comprobar si hay colisión de alguno de los cuadros del primer sprite con alguno de los cuadrados del segundo utilizando la misma comprobación que hemos utilizado en el primer método para detectar si se solapan dos rectangulos. Se deja como ejercicio al lector la implementación de este método de detección de colisiones. A continuación se muestra el listado completo de nuestra librería.
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
import java.util.*;
import java.io.*;

class Sprite {

 private int posx,posy;
 private boolean active;
 private int frame,nframes;
 private Image[] sprites;

 // constructor. 'nframes' es el número de frames del Sprite.
 public Sprite(int nframes) {
  // El Sprite no está activo por defecto.
  active=false;
  frame=1;
  this.nframes=nframes;
  sprites=new Image[nframes+1];
 }


 public void setX(int x) {
  posx=x;
 }

 public void setY(int y) {
  posy=y;
 }

 int getX() {
  return posx;
 }

 int getY() {
  return posy;
 }

 int getW() {
  return sprites[nframes].getWidth();
 }

 int getH() {
  return sprites[nframes].getHeight();
 }

 public void on() {
  active=true;
 }

 public void off() {
  active=false;
 }

 public boolean isActive() {
  return active;
 }

 public void selFrame(int frameno) {
  frame=frameno;
 }

 public int frames() {
  return nframes;
 }

 // Carga un archivo tipo .PNG y lo añade al sprite en
 // el frame indicado por 'frameno'
 public void addFrame(int frameno, String path) {
  try {
   sprites[frameno]=Image.createImage(path);
  } catch (IOException e) {
   System.err.println("Can`t load the image " + path + ": " + e.toString());
  }
 }


 boolean collide(Sprite sp) {
  int w1,h1,w2,h2,x1,y1,x2,y2;

  w1=getW();  // ancho del sprite1
  h1=getH();  // altura del sprite1
  w2=sp.getW(); // ancho del sprite2
  h2=sp.getH(); // alto del sprite2
  x1=getX();  // pos. X del sprite1
  y1=getY();  // pos. Y del sprite1
  x2=sp.getX(); // pos. X del sprite2
  y2=sp.getY(); // pos. Y del sprite2

  if (((x1+w1)>x2)&&((y1+h1)>y2)&&((x2+w2)>x1)&&((y2+h2)>y1)) {
   return true;
  } else {
   return false;
  }
 }


 // Dibujamos el Sprite
 public void draw(Graphics g) {
  g.drawImage(sprites[frame],posx,posy,Graphics.HCENTER|Graphics.VCENTER);
 }
Veamos un ejemplo práctico de uso de nuestra librería. Crea un nuevo proyecto en KToolBar, y añade el programa siguiente en el directorio ‘src’, junto con la librería Sprite.java. Por supuesto necesitarás incluir el gráfico hero.png en el directorio ‘res’.
En los siguientes capítulos vamos a basarnos en esta librería para el control de los Sprites del juego que vamos a crear.

import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;

public class SpriteTest extends MIDlet implements CommandListener {

private Command exitCommand, playCommand, endCommand;
private Display display;
private SSCanvas screen;

public SpriteTest() {
display=Display.getDisplay(this);
exitCommand = new Command("Salir",Command.SCREEN,2);

screen=new SSCanvas();

screen.addCommand(exitCommand);
screen.setCommandListener(this);
}

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 {

private Sprite miSprite=new Sprite(1);

public SSCanvas() {
// Cargamos los sprites
miSprite.addFrame(1,"/hero.png");

// Iniciamos los Sprites
miSprite.on();
}


public void paint(Graphics g) {

// Borrar pantalla
g.setColor(255,255,255);
g.fillRect(0,0,getWidth(),getHeight());

// situar y dibujar sprite
miSprite.setX(50);
miSprite.setY(50);
miSprite.draw(g);
}
}