Programación de juegos para móviles con J2ME

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

El resultado de nuestro juego puede verse en la siguiente figura. El listado fuente completo del juego puede consultarse en el ap�ndice A.

COMPARTE ESTE ARTÍCULO

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