viernes, 6 de marzo de 2015

Programación en red en MIDP

OBJETIVOS
En la clase de hoy vamos a ver el soporte que da MIDP a la programación en red. Como hemos visto en la clase teórica, en el CLDC de J2ME se rediseñaron las clases para entrada/salida y para el soporte a  conexiones de red de J2SE, definiendo lo que se denomina CLDC Generic Connection Framework.

CLDC Generic Connection Framework define una serie de interfaces para dar soporte a la variedad de tipos de conexiones que nos podemos encontrar en dispositivos móviles, pero no implementa ninguna de ellas, es en los perfiles donde se debe realizar esta implementación. En el perfil con el que estamos trabajando, MIDP, se debe dar soporte obligatoriamente a conexiones HTTP, a través de la implementación del interfaz HttpConnection.

Para la práctica final no necesitais hacer uso de conexiones de red, por lo que es muy recomendable realizar
esta práctica para que completeis vuestros conocimientos sobre MIDP.



 CLDC Generic Connection Framework
En el CLDC Generic Connection Framework, todas las conexiones se crean utilizando el método estático open de la clase Connector.
Si no se produce ningún error, este método devuelve un objeto que implementa una de las interfaces definidas en el CLDC Generic Connection Framework.

Estas interfaces están contenidas en el paquete javax.microedition.io y son:
  • El interfaz Connection es el tipo básico de conexión. Esta conexión sólo puede abrirse y cerrarse.

  • El interfaz InputConnection
    representa un dispositivo desde el que se pueden leer datos. Proporciona
    el método openInputStream
    que devuelve un stream de entrada para la conexión.


  • El interfaz OuputConnection representa un dispositivo en el que se pueden escribir datos.
    Proporciona el método openOutputStream que devuelve un stream de salida para la conexión.

  • El interfaz StreamConnection  que combina la conexiones de entrada y de salida anteriores.

  • El interfaz ContectConnection  que es una subinterfaz de StreamConnection. Proporciona acceso
    a información de algunos meta datos proporcionados en un conexión HTTP.

  • El interfaz StreamConnectionNotified que espera a que se establezca una conexión. Devuelve un
    StreamConnection a través del cual puede establecerse un enlace de comunicación bidireccional.

  • El interfaz DatagramConnection que representa el destino de un datagrama.
El método open de la clase Connector tiene la siguiente sintaxis, Connector.open(String);

donde el parámtero String tiene el formato "protocol:address;parameters". Por ejemplo:
  • Conexión HTTP:
    Connector.open("http://www.it.uc3m.es/pervasive");
    
  • Conexión Datagrama:
    Connector.open("datagram://address:port#");
    
  • Conmunicación con puerto serie:
    Connector.open("comm:0;baudrate=9600");
    
  • Acceso a ficheros:
    Connector.open("file:/miFichero.txt");
    

El objetivo de tener esta sintaxis, es abstraer al programador de las diferencias que existen entre los diferentes protocolos, de manera que la mayoría del código de una aplicación no se modifica cuando se cambia el protocolo que se está usando.

IMPORTANTE: Los ejemplos de conexiones anteriores son sólo ilustrativos. CLDC en sí mismo no proporciona ninguna implementación de ningún protocolo, éstas deben de proporcionarse a nivel de perfil. Además un determinado perfil no tiene porque implementar todos los protocolos, así MIDP proporciona una implementación del protocolo HTTP, pero en general no implementa ni sockets, ni datagramas (aunque algunos fabricantes pueden incorporarlas).

En el siguiente apartado veremos el interfaz HttpConnection propocionada en MIDP.



 HttpConnection
HTTP puede implementarse utilizando protocolos IP (como TCP/IP) o protocolos no-IP (como WAP o i-mode), por este motivo se seleccionó como protocolo para comunicaciones de red en MIDP. Todas las implementaciones de MIDP deben soportarlo, garantizando de esta manera la portabilidad de las aplicaciones, que utilizan este protocolo, entre diferentes dispositivos.

En MIDP se define un nuevo interfaz dentro de la jerarquía del CLDC Generic Connection Framework, el interfaz HttpConnection para el soporte a conexiones HTTP. Este interfaz extiende del interfaz ContentConnection.

El protocolo HTTP es un protocolo a nivel de aplicación del tipo petición/respuesta, en el que los parámetros de una petición deben establecerse antes de que se envíe la petición. La conexión puede estar en uno de los siguientes tres posibles estados:
  • "Setup": No se ha establecido todavía la conexión.
  • "Connected": Se ha establecido la conexión, la petición se ha enviado y se está esperando por una respuesta.
  • "Closed": La conexión se ha cerrado.
En el interfaz HttpConnection se proporcionan una serie de métodos que pueden invocarse en cada uno de los estados anteriores:

En el estado "setup" se pueden invocar los siguientes métodos:
  • setRequestMethod(String method) que establece el método de la petición, que puede ser POST,
    GET o HEAD.
    El método por defecto es GET y si el método no es HTTP o la implementación de HTTP está recortada y no lo soporta, se produce una IOException.
  • setRequestProperty(String key, String value) que permite indicar el valor de algunas propiedades HTTP antes de enviar la petición, la propiedad que se quiere establecer se indica en el parámetro key y su valor se establece en el parámetro value.
Por ejemplo, en el código siguiente se crea una conexión HTTP a la URL http://www.it.uc3m.es/pervasive y se indica que el método de la petición es POST, y que la propiedad HTTP User-Agent tiene el valor Profile/MIDP-1.0 Configuration/CLDC-1.0:
HttpConnection c = (HttpConnection)Connector.open("http://www.it.uc3m.es/pervasive");
c.setRequestMethod(HttpConnection.POST);
c.setRequestProperty("User-Agent, "Profile/MIDP-1.0 Configuration/CLDC-1.0");

La transición entre el estado "setup" y el estado "connected" se produce cuando se invoca algún método que precisa enviar o recibir datos al servidor con el que se establece la conexión. Algunos de los métodos que proporciona la implementación del interfaz HttpConnection y que provocan esta transición, son:
Cuando la conexión está abierta (se ha pasado al estado de "connected"), se pueden invocar los siguientes métodos:
A continuación se muestra un ejemplo de código en el que se utiliza la implementación de HttpConnection para leer el contenido de la página http://www.it.uc3m.es/celeste/docencia/cr/hola.txt y mostrarselo al usuario (MIDletHTTPExample.java):

import java.io.*;
import javax.microedition.midlet.*;
import javax.microedition.io.*;
import javax.microedition.lcdui.*;
 
/**
 * Un ejemplo de MIDlet para visualizar el contenido de una URL
 * utilizando la implementación del interfaz HttpConnection.
 */
 
public class MIDletHTTPExample extends MIDlet {
 
  private Display display;
  private String url = "http://www.it.uc3m.es/celeste/docencia/cr/hola.txt";
 
  public MIDletHTTPExample() {
    display = Display.getDisplay(this);
  }
 
  /**
   * Método startApp()
   */
  public void startApp() {
    // Llama al método download para descargar el contenido de la URL
    try {
      download(url);
    } catch(IOException e) {
      System.out.println("IOException: " + e);
    }
  }
 
  private void download (String url) throws IOException {
    StringBuffer b = new StringBuffer();
    InputStream is = null;
    HttpConnection c = null;
    TextBox t = null;
 
    try {
      long len = 0 ;
      int ch = 0;
 
      // Abre una conexión del tipo HttpConnection
      c = (HttpConnection)Connector.open(url);
      
      // Obtiene un stream de entrada, para leer el contenido de la url
      // con la que establece la conexión
      is = c.openInputStream();
      
      // Lee hasta que se cierra la conexión
      while ((ch = is.read()) != -1) {
        b.append((char)ch);
      }
      
      // Se contruye un TextBox con el contenido de la URL
      t = new TextBox("Hola...", b.toString(), 1024, 0);
 
    } finally {
      // Se cierra tanto el stream de entrada
      if (is != null)
        is.close();
      // Se cierra la conexión
      if (c != null) 
        c.close();
    }  
    
    // Se visualiza el TextBox en el Display
    display.setCurrent(t);
  }
  
   /**
    * pauseApp
    */
   public void pauseApp() {
   }
 
   /**
    * destroyApp
    */
   public void destroyApp(boolean unconditional) {
   }
}
 
Ejercicio 1:Partiendo del código del ejemplo anterior realizad un MIDlet que solicite al usuario una URL y una etiqueta HTML, y le muestre como resultado el texto modificado por esa etiqueta.

Ejercicio 2:Repetid el ejercicio anterior, pero además de visualizar el texto modificado por la etiqueta HTML, almacenad su contenido en un Record Store, de forma que el usuario de forma "off-line" pueda visualizar el resultado de sus consultas (por lo tanto, se debe de almacenar: URL solicitada, etiqueta HTML indicada y texto modificado por la etiqueta HTML en la URL indicada).

RMS: Almacenamiento en MIDP



 OBJETIVOS
MIDP define una sencilla base de datos orientada a registros que permite almacenar a las aplicaciones datos de forma persistente. Esta base se denomina Record Management System (RMS).
La práctica de hoy tiene como objetivo aprender los conceptos básicos para el manejo de esta base datos, a través del API que nos ofrece MIDP. Todas las clases relacionadas con RMS están contenidas en el paquete javax.microedition.rms.
Esta práctica os debe servir para enfrentaros con dos partes de la práctica final: almacenar el estado de una partida, para que sea posible parar y reanudar un partida (opciones del menú Stop y Restart) y para mantener el registro de mejores puntuaciones (opción Records).


 RecordStore
El mecanismo básico de almacenamiento de RMS es denominado record store. Un record store es un  conjunto de registros, y un registro es un byte array de datos de tamaño variable. Un record store está  representado por un objeto de la clase RecordStore.
Existen reglas importantes sobre los record store:
  1. El nombre de un record store consiste en una combinación de hasta 32 caracteres (sensible a las mayúsculas).

  2. Los record stores creados por MIDlets de un mismo MIDlet suite están almacenados en el mismo espacio de nombres, y por lo tanto, pueden compartir y ver sus contenidos.

  3. Los record stores creados por MIDlets en un MIDlet suite, no son accesibles para los MIDltes de otros MIDlets suite.

  4. El nombre de un record store debe ser único en un MIDlet suite.

Operaciones con un Record Store

La clase RecordStore proporciona los siguientes métodos para crear, abrir, cerrar y borrar un record store:
Cada record store mantiene al menos un campo de cabecera. Si no existe espacio suficente para almacenar la cabecera, el record store no se creará y se producirá una RecordStoreFullException. Si ocurre otro tipo de problema, como que el nombre del record store es demasiado largo, o el record store está corrupto, se produce una RecordStoreException.

A continuación, se puede ver un sencillo ejemplo de creación de un RecordStore (RecordStoreTest1.java).

import javax.microedition.midlet.*;
import javax.microedition.rms.*;
 
public class RecordStoreTest1 extends MIDlet {
    public RecordStoreTest1() { 
    }
    public void startApp() throws MIDletStateChangeException {
        RecordStore rs=null;
        try {
     rs = RecordStore.openRecordStore("file1",true);
            System.out.println("record store file1 is opened.");
        }catch(Exception e){
            System.out.println("Error: "+e.getMessage());
        }
        finally{
            //close the record store
            try {
         rs.closeRecordStore();
                System.out.println("record store file1 is closed");
            }catch (Exception e){
                System.out.println("Error: "+e.getMessage());
            }
        }
        destroyApp(true);
        notifyDestroyed();
    }
 
    /**
     * Pause the MIDlet
     */
    public void pauseApp() {
    }
 
    /**
     * Called by the framework before the application is unloaded
     */
    public void destroyApp(boolean unconditional) {
    }
}

Records


Los elementos de un record store se denominan records. Los records se representan por arrays de bytes y se identifican univocamente mediante recordIDs (valores enteros que comienzan en 1). Los records pueden se añadidos, borrados, leidos y modificados a través de los siguientes métodos, proporcionados por el API:


A continuación podeis ver un ejemplo sencillo, que partiendo del primer ejemplo, añade dos records nuevos y borra uno de ellos (RecordStoreTest3.java).

import javax.microedition.midlet.*;
import javax.microedition.rms.*;
 
public class RecordStoreTest3 extends MIDlet {
    public RecordStoreTest3() { 
    }
    public void startApp() throws MIDletStateChangeException {
        RecordStore rs=null;
        try {
     rs = RecordStore.openRecordStore("file1",true);
            byte data[]= new byte[4];
            for(int j=0; j<2; j++) {
                int i=rs.getNextRecordID();
                data[0] = (byte)((i >> 24) & 0xff);
                data[1] = (byte)((i >> 16) & 0xff);
                data[2] = (byte)((i >> 8) & 0xff);
                data[3] = (byte)(i & 0xff);
                System.out.println("record "+ rs.addRecord(data,0,4)+" is added.");
            }
  try {
             rs.deleteRecord(2);
             System.out.println("record 2 is deleted.");
  }catch(InvalidRecordIDException e) {
   System.out.println("record 2 does not exist");
  }
        }catch(Exception e){}
        finally{
            //close the record store
            try {
         rs.closeRecordStore();
            }catch (Exception e){}
        }
        destroyApp(true);
        notifyDestroyed();
    }
 
    /**
     * Pause the MIDlet
     */
    public void pauseApp() {
    }
 
    /**
     * Called by the framework before the application is unloaded
     */
    public void destroyApp(boolean unconditional) {
    }
}

La cabecera de un Record Store

Los record stores al igual que los ficheros mantienen una cabecera con información propia:

El API nos proporciona además la siguiente información adicional sobre los records store, aunque no está almacenada en la cabecera:

El interfaz RecordListener

A través del interfaz RecordListener se pueden monitorizar cambios en record stores. Se pueden gestionar tres tipos de eventos, a través de los métodos:
La clase RecordStore proporciona los siguientes métodos para añadir o borrar un RecordListener:
NOTA:Un objeto RecordStore puede tener varios RecordListeners registrados.


 RecordEnumeration
Después de que los records se borren, los recordIds de los records que están almacenados en un record store no son consecutivos. Por lo tanto, realizar un recorrido secuencial para obtener todos los records almacenados no es la manera más eficiente de hacerlo.

Para ello se proporciona la clase RecordEnumeration, que aunque en el API de MIDP se presenta como un interfaz, toda implementación de MIDP debe proporcionar una implementación de ella.

Un RecordEnumeration es parecido a un lista doblemente enlazada, en la que cada nodo representa un recordId. Un RecordEnumeration mantiene una secuencia lógica de recordIds de los records almacenados en un record store.

Ejercicio:Analizad el interfaz y realizad un pequeño programa en el que obteneis secuencialmente el contenido de los records en un record store. Cread después un nuevo programa que utilice la implementacion de RecordEnumeration y comprobad la diferencia.


 RecordFilter y RecordComparator
El interfaz RecordFilter define un único método public boolean matches(byte candidate[]) que nos permite definir un criterio para seleccionar un record en un record store que cumpla una determinada condición, de tal forma que si la cumple, se devuelve true y si no false.

El interfaz RecordComparator nos proporciona una forma sencilla de clasificar records. Define una única
función int compare(byte[] rec1, byte[] rec2), que puede devolver únicamente los siguientes constantes:
Por ejemplo, una manera de clasificarlos sería por fecha de creación. Así al implentar esta función se devolvería PRECEDES si rec1 se ha creado antes de rec2, FOLLOWS si se ha creado después y EQUIVALENT si se han creado simultaneamente.



Programación de juegos para móviles con J2ME Parte XII Final

Código Fuente

M1945.java

package mygame;
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
import java.util.*;
import java.io.*;

public class M1945 extends MIDlet implements CommandListener {

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

    public M1945() {
        display=Display.getDisplay(this);
        exitCommand = new Command("Salir",Command.SCREEN,2);
        playCommand = new Command("Jugar",Command.CANCEL,2);
        endCommand = new Command("Salir",Command.SCREEN,2);

        screen=new SSCanvas();

        screen.addCommand(playCommand);
        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) {
            if (screen.isPlaying()) {
                screen.quitGame();
            } else {
                destroyApp(false);
                notifyDestroyed();
            }
        }

        if (c == playCommand && !screen.isPlaying()) {
            // Play!!!
            new Thread(screen).start();
        }
    }
}

class SSCanvas extends Canvas implements Runnable {
    private int score,sleepTime,cicle,lives,shield;
    private int indice_in, indice, xTiles, yTiles;
    private boolean playing,fireOn=false;
    private boolean done;
    private int deltaX,deltaY;
    private Hero hero=new Hero(1);
    private Enemy[] enemies=new Enemy[7];
    private Bullet[] aBullet=new Bullet[7];
    private Sprite intro=new Sprite(1);
    private Explode[] explode=new Explode[7];
    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");
        intro.addFrame(1,"/intro.png");

        // Iniciamos los Sprites
        hero.on();
        intro.on();
    }

    void start() {
        int i;
        playing = true;
        sleepTime = 50;
        hero.setX(getWidth()/2);
        hero.setY(getHeight()-20);
        deltaX=0;
        deltaY=0;
        cicle=0;
        xTiles=7;
        yTiles=8;
        indice=map.length-(xTiles*yTiles);
        indice_in=0;
        score=0;
        lives=3;
        shield=0;

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

        // Inicializar balas
        for (i=1 ; i<=6 ; i++) {
            aBullet[i]=new Bullet(2);
            aBullet[i].addFrame(1,"/mybullet.png");
            aBullet[i].addFrame(2,"/enemybullet.png");
            aBullet[i].off();
        }

        // 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");

        // Inicializamos explosiones
        for (i=1 ; i<=6 ; i++) {
            explode[i]=new Explode(6);
            explode[i].addFrame(1,"/explode1.png");
            explode[i].addFrame(2,"/explode2.png");
            explode[i].addFrame(3,"/explode3.png");
            explode[i].addFrame(4,"/explode4.png");
            explode[i].addFrame(5,"/explode5.png");
            explode[i].addFrame(6,"/explode6.png");
        }
    }

    void computeEnemies() {
        int freeEnemy,i;
        Random random = new java.util.Random();

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

    void computeBullets() {
        int freeBullet,theEnemy,i,j;

        // Crear disparo del jugador
        freeBullet=0;
        if (fireOn) {
            // Buscar un disparo libre
            for (i=1 ; i<=6 ; i++) {
                if (!aBullet[i].isActive()) {
                    freeBullet=i;
                }
            }
            if (freeBullet !=0) {
                aBullet[freeBullet].on();
                aBullet[freeBullet].setX(hero.getX());
                aBullet[freeBullet].setY(hero.getY()-10);
                aBullet[freeBullet].setOwner(1);
            }
        }

        // Crear disparo de enemigos
        freeBullet=0;
        theEnemy=0;

        for (i=1 ; i<=6 ; i++) {
            if (enemies[i].getType() == 1 && enemies[i].isActive() && 
                enemies[i].getY() > getHeight()/2 && enemies[i].getY() < 
                (getHeight()/2)+5) {
                
                // Buscar un disparo libre
                for (j=1 ; j<=6 ; j++) {
                    if (!aBullet[j].isActive()) {
                        freeBullet=j;
                        theEnemy=i;
                    }
                }

                if (freeBullet !=0) {
                    aBullet[freeBullet].on();
                    aBullet[freeBullet].setX(enemies[theEnemy].getX());
                    aBullet[freeBullet].setY(enemies[theEnemy].getY()+10);
                    aBullet[freeBullet].setOwner(2);
                }
            }
        }


        // Mover los disparos
        for (i=1 ; i<=6 ; i++) {
            if (aBullet[i].isActive()) {
                aBullet[i].doMovement();
            }

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

    }

    void computeExplodes() {
        int i;

        for (i=1 ; i<=6 ; i++) {
            explode[i].doMovement();
        }
    }

    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 createExplode(int posx, int posy) {
        int freeExplode,i;
        freeExplode=0;

        // Buscar una explosión libre
        for (i=1 ; i<=6 ; i++) {
            if (!explode[i].isActive()) {
                freeExplode=i;
            }
        }

        if (freeExplode !=0) {
            explode[freeExplode].setState(1);
            explode[freeExplode].on();
            explode[freeExplode].setX(posx);
            explode[freeExplode].setY(posy);
        }
    }

    void checkCollide() {
        int i,j;
        boolean collision;
        collision=false;

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

        if (collision == true) {
            lives--;

            // poner heroe al estado inicial
            hero.setX(getWidth()/2);
            hero.setY(getHeight()-20);

            // Durante 20 ciclos nuestra nave será inmune
            shield=20;

            if (lives <= 0) {
                playing=false;
            }
        }

        if (shield > 0)
            shield--;
    }

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

    void quitGame() {
        playing = false;
    }

    boolean isPlaying() {
        return playing;
    }

    public void run() {
        start();

        while (playing) {
            // Actualizar fondo de pantalla
            doScroll();
            // Actualizar posición del jugador
            computePlayer();
            // Actualizar posición de los enemigos
            computeEnemies();
            // Actualizar balas
            computeBullets();
            // Actualizar explosiones
            computeExplodes();
            // Comprobar colisiones
            checkCollide();
            // Contador de ciclos
            cicle++;
            // Actualizar pantalla
            repaint();
            serviceRepaints();
            try {
                Thread.sleep(sleepTime);
            } catch (InterruptedException e) {
                System.out.println(e.toString());
            }
        }

        // Repintamos la pantalla
        // para mostrar pantalla de presentación
        repaint();
        serviceRepaints();
    }

    public void keyReleased(int keyCode) {
        int action=getGameAction(keyCode);

        switch (action) {
            case FIRE:
                fireOn=false;
                break;
            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 FIRE:
                fireOn=true;
                break;
            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);

        g.setFont(Font.getFont(Font.FACE_PROPORTIONAL,Font.STYLE_BOLD,Font.SIZE_MEDIUM));

        if (playing == false) {
            intro.setX(getWidth()/2);
            intro.setY(getHeight()/2);
            intro.draw(g);
        } else {
            // 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 enemigos
            for (i=1 ; i<=6 ; i++) {
                if (enemies[i].isActive()) {
                    enemies[i].setX(enemies[i].getX());
                    enemies[i].setY(enemies[i].getY());
                    enemies[i].draw(g);
                }
            }

            // Dibujar el jugador
            if (shield == 0 || shield%2 == 0) {
                hero.setX(hero.getX());
                hero.setY(hero.getY());
                hero.draw(g);
            }

            // Dibujar disparos
            for (i=1 ; i<=6 ; i++) {
                if (aBullet[i].isActive()) {
                    aBullet[i].setX(aBullet[i].getX());
                    aBullet[i].setY(aBullet[i].getY());
                    aBullet[i].draw(g);
                }
            }

            // Dibujar explosiones
            for (i=1 ; i<=6 ; i++) {
                if (explode[i].isActive())
                    explode[i].draw(g);
            }
                g.drawString(" "+score,getWidth()-20,20,Graphics.HCENTER|Graphics.BOTTOM);
                g.drawString(" "+lives,20,20,Graphics.HCENTER|Graphics.BOTTOM);
        }
    }
}

Sprite.java

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

Enemy.java

package mygame;

import java.util.*;

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

Bullet.java

package mygame;

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

Explode.java

package mygame;

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

Hero.java

package mygame;

class Hero extends Sprite {

    private int state;

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

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

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

lunes, 9 de febrero de 2015

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

Comunicaciones

Un teléfono móvil es capaz de realizar conexiones con un servidor. Esto abre un vasto campo de aplicaciones y también muchas posibilidades en el campo de los juegos. Una posible aplicación puede ser un servidor que almacene las puntuaciones máximas entre todos los jugadores de un juego, o incluso la posibilidad de juego online contra otros jugadores.
En principio un MIDlet puede establecer diversos tipos de conexiones: Sockets, http, https, datagramas, y otras, sin embargo, el standard sólo obliga a la implementación del protocolo http, así que dependiendo del dispositivo dispondremos de unos tipos de conexiones u otras. Para mantener la compatibilidad es aconsejable utilizar las basadas en el protocolo http y, por lo tanto, limitarnos a este tipo de conexión. La clase Connector, que se encuentra en el paquete javax.microedition.io, es la base de las comunicaciones en los MIDlets. Para abrir una conexión utilizamos el método open() de la clase Connector.
Connector.open(String conexion);
El parámetro conexión es la URL a la que queremos conectar. El formato de la URL es el standard. Los siguientes son formatos válidos.

  • http://www.dominio.com
  • http://www.dominio.com:8080
  • http://www.dominio.com/script.jsp?param1=1&param2=2
Una forma muy estandarizada de comunicación con un servidor es mediante llamadas a scripts escritos en JSP, PHP o cualquier otro lenguaje de script, y pasando los parámetros necesarios, ya sea por método post o método get. Este script procesará la información y hará uso de ella, por ejemplo, almacenándola en una base de datos. La escritura de scripts se escapa del ámbito de este libro, sin embargo, podrás encontrar numerosas publicaciones sobre el tema además de gran cantidad de información en Internet.
Además de conexiones http, podemos abrir otros tipos de conexiones siempre que nuestro teléfono móvil lo soporte.
Conexión URL
Socket socket://www.dominio.com:8000
Datagramas datagram://www.dominio.com:8000
Archivos file:/datos.txt
Puertos comm:0;baudrate=9600
Una vez abierto el canal de comunicación podemos utilizar los métodos openInputStream() y openOutputStream() de la clase Connector para abrir una corriente de lectura o escritura mediante la cual, leer y escribir información.
El método openInputStream() devuelve un objeto del tipo InputStream, que representa una corriente de entrada de bytes. Contiene algunos métodos que permiten controlar la entrada de datos. Los más interesantes son:
Método Cometido
int available() Número de bytes disponibles en la corriente de lectura
void close() Cierra la corriente
abstract int read() Lee el siguiente byte de la corriente
De forma similar, el método openOutputStream() devuelve un objeto de tipo OutputStream, que representa una corriente de salida de bytes. Los métodos más interesantes son:
Método Cometido
void close() Cierra la corriente
void flush() Fuerza la salida de los bytes almacenados en el buffer
void write(byte[] b)
abstract void write(int b)
Escribe un byte o un array de bytes en la corriente de salida
Nos resta conocer la clase HttpConnection, que gestiona una conexión basada en el protocolo http. Podemos crear una conexión de este tipo mediante el método open() de la clase Connector tal y como ya vimos.
HttpConnection c = (HttpConnection)Connector.open(url);
Algunos métodos interesantes son los siguientes:
Método Cometido
String getHost() Devuelve el nombre del host remoto
int getPort() Devuelve el puerto de conexión
String getQuery() Devuelve los parámetros pasados en la URL
String getRequestMethod() Devuelve el método de petición
int getResponseCode() Devuelve el código de respuesta
String getResponseMessage() Devuelve el mensaje de respuesta
String getURL() Devuelve la URL de la conexión
void setRequestMethod(String method) Establece el método de petición (GET, POST o HEAD)
Para ilustrar todo durante el capítulo, vamos a desarrollar un programa que es capaz de conectar con un servicio web y obtener información. Nos conectaremos a una web de noticias (weblog) y extraeremos las cabeceras de las noticias para mostrarlas en nuestro móvil. La web que vamos a utilizar es http://programacion.com/cursos/ags_j2me/www.barrapunto.com.

Esta web permite acceder a la información en formato XML en la URL http://backends.barrapunto.com/barrapunto.xml, que produce una salida como ésta (reproducimos aquí una sóla noticia):
  http://barrapunto.com/article.pl?sid=04/01/21/1048222 
   
  fernand0 
  ssh-scp-ssh-keygen 
  85 
  36 
  
articles
  topicseguridad.png  
Los títulos de las noticias están encerradas entre las etiquetas y , así que vamos a extraer el texto que se encuentre entre ambas.

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


public class Conexion extends MIDlet {

    private Display display;

    // Url que vamos a consultar
    String url = "http://backends.barrapunto.com/barrapunto.xml";

    public Conexion() {
        display = Display.getDisplay(this);
    }

    public void startApp() {
        try {
            verNoticias(url);
        } catch (IOException e) {}
    }

    public void pauseApp() {}

    public void destroyApp(boolean unconditional) {}

    void verNoticias(String url) throws IOException {
        HttpConnection c = null;
        InputStream is = null;
        StringBuffer b = new StringBuffer();
        StringBuffer salida = new StringBuffer();
        TextBox t = null;
        try {
            c = (HttpConnection)Connector.open(url);
            c.setRequestMethod(HttpConnection.GET);
            c.setRequestProperty("IF-Modified-Since", "10 Nov 2000 17:29:12 GMT");
            c.setRequestProperty("User-Agent","Profile/MIDP-1.0 Configuration/CLDC-1.0");
            c.setRequestProperty("Content-Language", "es-ES");
            is = c.openInputStream();
            int ch, i, j;

            // leer los datos desde la URL
            while ((ch = is.read()) != -1) {
                b.append((char) ch);
                if (ch == '\n') {
                    if (b.toString().indexOf("") > 0) {<br>                        i = b.toString().indexOf("<titl>")+7;<br>                        j = b.toString().indexOf("");
                        salida.append(b.toString().substring(i,j)); 
                        salida.append("\n-------------------\n");
                    }
                    b.delete(0,b.length()); 
                }
            }

            // mostrar noticias en la pantalla
            t = new TextBox("Noticias en barrapunto.com", salida.toString(), 1024, 0);
        } finally {
            if(is!= null) {
                is.close();
            }
            if(c != null) {
                c.close();
            }
        }
        display.setCurrent(t);
    } 
}

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

Almacenamiento. RMS

Hasta ahora hemos usado la memoria principal del móvil (la RAM) para almacenar datos temporales, pero al salir del midlet, estos datos son eliminados. Esto plantea algunos problemas. Por ejemplo, ¿cómo podemos almacenar las puntuaciones máximas de nuestro juego?
Un dispositivo móvil (al menos por ahora) no dispone de disco duro donde almacenar información permanentemente. J2ME resuelve el problema mediante el RMS (Record Management System). RMS es un pequeño sistema de bases de datos muy sencillo, pero que nos permite añadir información en una memoria no volátil del móvil. RMS no tiene nada que ver con JDBC debido a las limitaciones de los dispositivos J2ME, por lo tanto, el acceso y almacenamiento de la información se hace a mucho más bajo nivel. RMS no puede ser consultado con sentencias SQL ni nada parecido. En una base de datos RMS, el elemento básico es el registro (record). Un registro es la unidad de información más pequeña que puede ser almacenada. Los registros son almacenados en un recordStore que puede visualizarse como una colección de registros. Cuando almacenamos un registro en el recordStore, a éste se le asigna un identificador único que identifica unívocamente al registro.
Para poder utilizar RMS hemos de importar el paquete javax.microedition.rms. Este paquete nos provee de la clase RecordStore y de cuatro interfaces, de las cuales solamente profundizaremos en la interfaz RecordEnumeration.

Trabajando con RMS

Cubrir todas las capacidades de RMS se escapa del ámbito de este libro. Sin entrar en profundidades, vamos a ver cómo realizar las operaciones básicas de almacenamiento, lectura y borrado de registros.

Abrir y errar un recordStore

Antes de poder almacenar un registro hemos de abrir un recordStore con el método openRecordStore().
static RecordStore openRecordStore(String nombre, bolean crear)
El parámetro nombre es el nombre de la base de datos. El nombre puede tener un tamaño de 32 caracteres. El parámetro crear, si tiene su valor a true, creará la base de datos si no existe. Cuando creamos un recordStore, sólo puede ser accedido desde la suite de MIDlets que la creó.
Cuando terminamos de utilizar el recordStore, hemos de cerrarlo:
RecordStore.closeRecordStore();

Añadir registros

Una vez abierto nuestro recordStore podemos comenzar a añadir registros con el método addRecord().
public int addRecord(byte[] dato,int offset, int numBytes)
El primer parámetro es el dato que queremos almacenar. Es un array de bytes. El offset es la posición a partir de la cual (dentro del array) se va a almacenar el dato. Finalmente, numBytes es el número de bytes que se van a almacenar. El método retorna el identificador que el RMS ha asignado al registro.
El método addRecord puede lanzar la excepción RecordStoreException, por lo tanto hemos de capturarla.
try {
    int id = recordStore.addRecord (datos, 0, datos.length);
} catch (RecordStoreException e) {}

Leer registros

El método getRecord() permite acceder al registro que deseemos, siempre que conozcamos su identificador.
public byte[] getRecord(int Id)
No es necesario que almacenemos y mantengamos una lista con todos los identificadores de los registros. Un poco más adelante veremos el método recordEnumeration que nos permitirá conocer el identificador de cada registro. Al igual que con el método addRecord(), hemos de capturar la excepción RecordStoreException.
byte[] dato = null;

try {
    dato = recordStore.getRecord(id);
} catch (RecordStoreException e) {}

Borrar registros

El borrado de registros se realiza con el método deleteRecord().
public void deleteRecord(int recordId)
Al igual que con la escritura y lectura de registros hemos de tener en cuenta que puede provocar la excepción RecorStoreException.
try {
    recordStore.deleteRecord(id);
} catch (RecordStoreException e) {}

Recorriendo registros

Vamos a valernos del objeto RecordEnumeration para recorrer todos los registros almacenados en la base de datos. Para crear una enumeración utilizamos el método enumerateRecords().
public RecordEnumeration enumerateRecords(RecordFilter filtro,
    RecordComparator, comparador,boolean Actualizar)
Los dos primeros parámetros sirven para personalizar el recorrido de los registros. No entraremos en detalle, pero, gracias al primero podremos filtrar la búsqueda, y con el segundo podemos recorrer los registros de forma ordenada. El parámetro Actualizar indica si la enumeración debe actualizarse cuando realicemos alguna operación de inserción o borrado de registros. Si vas a hacer un recorrido rápido por los registros es mejor ponerlo a false para evitar la sobrecarga.
RecordEnumeration registro = null;

try {
    registro = recordStore.enumerateRecords(null, null, false);
    while (registro.hasNextElement()) 
        System.out.println (registro.nextRecordId());
} catch (Exception e) {}
Hay dos métodos interesantes del RecordEnumeration: hasNextElement() que devolverá el valor true si hay un siguiente elemento disponible para ser leído. Cada vez que leemos un elemento se adelanta el puntero al siguiente. El método nextRecordId() devuelve el identificador del siguiente registro.
El siguiente código muestra un ejemplo completo de uso de RMS.
import java.io.*;
import javax.microedition.midlet.*;
import javax.microedition.rms.*;

public class RMS extends MIDlet {

// nombre de la BD
static final String BD = "datos";

String dato;
int id, i;
char b;

public RMS() {
RecordStore rs = null;

// Borramos la BD si tenía algo
try {
RecordStore.deleteRecordStore(BD);
} catch( Exception e ){}

try {
// Abrimos el recordStore
rs = RecordStore.openRecordStore(BD, true);

guardaRegistro(rs,"Datos del registro 1");
guardaRegistro(rs,"Datos del registro 2");
guardaRegistro(rs,"Datos del registro 3");

// Leemos los registros
RecordEnumeration registros = rs.enumerateRecords(null, null, false);
// Recorremos todos los elementos
while (registros.hasNextElement()) {

// Obtenemos el ID del siguiente registro
verRegistro(rs, registros.nextRecordId());
}
rs.closeRecordStore();
} catch( RecordStoreException e ){
System.out.println( e );
}
notifyDestroyed();
}

public void verRegistro(RecordStore rs, int id) {
try {
ByteArrayInputStream bais = new ByteArrayInputStream(rs.getRecord(id));
DataInputStream is = new DataInputStream(bais);
// leemos el registro
try {
dato = is.readUTF();
System.out.println("-> "+dato);
} catch (EOFException eofe) {
} catch (IOException ioe) {}
} catch (RecordStoreException e) {}
}

public void guardaRegistro(RecordStore rs, String dato) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream os = new DataOutputStream(baos);
try {
// guadar el dato
os.writeUTF(dato);
} catch (IOException ioe) {}

// extraer el array de butes
byte[] b = baos.toByteArray();

// lo añadimos al recordStore
try {
rs.addRecord(b, 0, b.length);
} catch (RecordStoreException rse) {}
}

public void destroyApp( boolean unconditional ) {}

public void startApp() {}

public void pauseApp() {}
}

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

Sonidos

Nuestro juego M1945 no tiene sonido. La razón para ello ha sido mantener la compatibilidad con los dispositivos con soporte MIDP 1.0, que es el más extendido por ahora. Desgraciadamente MIDP 1.0. no ofrece ninguna capacidad para reproducir sonidos, por lo que los fabricantes han creado APIs propias e incompatibles entre sí. A partir de la versión 2.0 de MIDP sí se ha añadido soporte multimedia, aunque aún no se han extendido demasiado estos dispositivos en el mercado. La API encargada del sonido se llama MIDP 2.0 Media API. Hasta este capítulo, todo lo expuesto es compatible con MIDP 1.0, incluido el juego M1945. Se ha optado, pues, por separar en un capítulo aparte lo concerniente al sonido para no romper esta compatibilidad.
La API multimedia esta compuesta por tres partes:
  • Manager
  • Player
  • Control
La función de la clase Manager es crear objetos de tipo Player. Un Player es un objeto capaz de reproducir un tipo de información multimedia, ya sea audio o video. Por lo tanto, el Manager debe generar un tipo diferente de Player según la naturaleza de lo que queramos reproducir. Para utilizar estos objetos hemos de importar el paquete javax.microedition.media. Finalmente, la clase Control nos premite controlar y gestionar un objeto de tipo Player. Esta clase se encuentra en el paquete javax.microedition.media.control.

. Sonidos

Hay eventos en los juegos que generan sonidos, como una explosión o un disparo. Este tipo de sonido suelen ser samples digitales. El formato más habitual para almacenar estos sonidos es el formato WAV. La siguiente línea crea un Player para un archivo WAV.
Player sonido = Manager.createPlayer("http://www.dominio.com/music.wav");
Si queremos crear un Player para un objeto almacenado en nuestro archivo JAR, hemos de utilizar una corriente de entrada para leerlo. En este caso, hemos de indicarle al Manager que tipo de archivo es.
InputStream in = getClass().getResourceAsStream("/explosion.wav");
Player sonido = Manager.createPlayer(in, "audio/x-wav");
Debes capturar las excepciones IOException y MediaException para crear este Player. Para reproducir el sonido usamos el método start() del Player.
try {
    sonido.start();
} catch (MediaException me) { }

. Música

Además de samples, la API multimedia nos permite reproducir notas musicales (tonos). La forma más simple de reproducir un tono es utilizar el método playTone() de la clase Manager.
try {
    Manager.playTone(ToneControl.C4, 100, 80);
} catch (Exception e){}
Este método tiene tres parámetros. El primero es la frecuencia del tono. En este caso hemos utilizado la constante ToneControl.C4, que es la frecuencia de la nota Do central. Otra constante interesante es ToneControl.SILENCE. El segundo parámetro es la duración de la nota en milisegundos, y el tercero el volumen de reproducción.
Reproducir una melodía con la ayuda del método playTone() puede ser un trabajo algo arduo. Es por ello que la API multimedia nos ofrece otra forma de reproducir secuencias de notas. Una secuencia es un array de bytes con un formato muy concreto. El array se va rellenando con pares de bytes cuyo significado analizaremos con un ejemplo.
byte[] secuencia = {
    ToneControl.VERSION, 1,
    ToneControl.TEMPO, tempo,
    // comienzo del bloque 0
    ToneControl.BLOCK_START, 0, 
    // notas del bloque 0
    C4,d, F4,d, F4,d, C4,d, F4,d, F4,d, C4,d, F4,d,
    // fin del bloque 0
    ToneControl.BLOCK_END, 0, 
    // inicio del bloque 1
    ToneControl.BLOCK_START, 1, 
    // notas del bloque 1
    C4,d, E4,d, E4,d, C4,d, E4,d, E4,d, C4,d, E4,d,
    // fin del bloque 1
    ToneControl.BLOCK_END, 1, 
    // reproducir bloque 0
    ToneControl.PLAY_BLOCK, 0, 
    // reproducir bloque 1
    ToneControl.PLAY_BLOCK, 1, 
    // reproducir bloque 0
    ToneControl.PLAY_BLOCK, 0, 
};
Podemos observar que la secuencia está dividida en tres secciones bien diferenciadas. En la primera establecemos la versión (del formato de secuencia) y el tempo de la melodía. Observa como la información se codifica en pares de bytes. El primero indica el atributo para el que queremos establecer un valor, y el segundo es el valor mismo.
En la segunda sección de la secuencia definimos bloques de notas. Las notas comprendidas entre ToneControl.BLOCK_END y ToneControl.BLOCK_START forman un bloque. Podemos definir tantos bloques como necesitemos. Dentro de un bloque, las notas van definidas en pares, cuyo primer byte es la nota y el segundo es la duración. Finalmente, la tercera sección indica el orden de reproducción de cada bloque de notas.
El código encargado de reproducir la secuencia es el siguiente.
Player p = Manager.createPlayer(Manager.TONE_DEVICE_LOCATOR);
p.realize();
ToneControl c = (ToneControl)p.getControl("ToneControl");
c.setSequence(secuencia);
p.start();
Vamos a reunir en el siguiente ejemplo práctico todo lo expuesto en el presente capítulo.
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
import javax.microedition.media.*;
import javax.microedition.media.control.*;
import java.io.*;

public class Sonido extends MIDlet implements CommandListener {
    private Display display; 
    private Form formulario; 
    private Command exit; 
    private Command wav, nota, secuencia; 

    public Sonido() {
        display = Display.getDisplay(this);

        exit = new Command("Salir", Command.EXIT, 1);
        wav = new Command("WAV", Command.SCREEN, 2);
        nota = new Command("Nota", Command.SCREEN, 2);
        secuencia = new Command("Secuencia", Command.SCREEN, 2);
        formulario = new Form("Reproducir.");
        formulario.addCommand(exit);
        formulario.addCommand(wav);
        formulario.addCommand(nota);
        formulario.addCommand(secuencia);
        formulario.setCommandListener(this);
    }

    public void startApp() {
        display.setCurrent(formulario);
    }

    public void pauseApp() {}

    public void destroyApp(boolean unconditional) {}

    public void commandAction(Command c, Displayable s) {
        if (c == exit) { 
            destroyApp(false);
            notifyDestroyed();
        } else { 
            if (c == wav)
                playWav();
            if (c == nota)
                playNota();
            if (c == secuencia)
                playSecuencia();
        }
    }

    public void playWav() {
        try {
            // Abrir corriente de datos del archivo de sonido
            InputStream in = getClass().getResourceAsStream("/explosion.wav");
            Player p = Manager.createPlayer(in, "audio/x-wav");
            // comenzar reproducción
            p.start();
        } catch (Exception e) {
            Alert alr = new Alert("Error", "No se pudo reproducir el sonido.", 
                                   null, AlertType.ERROR);
            alr.setTimeout(Alert.FOREVER);
            display.setCurrent(alr, formulario);
        }
    }

    public void playNota() {
        try {
            // reproducir nota
            Manager.playTone(ToneControl.C4, 100, 80);
        } catch (Exception e){}
    }

    public void playSecuencia() {
        byte tempo = 30;
        byte d = 8;

        // Creamos las notas a partir del Do central
        byte C4 = ToneControl.C4;;
        byte D4 = (byte)(C4 + 2);
        byte E4 = (byte)(C4 + 4);
        byte F4 = (byte)(C4 + 5);
        byte G4 = (byte)(C4 + 7);
        byte silencio = ToneControl.SILENCE;

        byte[] secuencia = {
            ToneControl.VERSION, 1,
            ToneControl.TEMPO, tempo,
            // comienzo del bloque 0
            ToneControl.BLOCK_START, 0, 
            // notas del bloque 0
            C4,d, F4,d, F4,d, C4,d, F4,d, F4,d, C4,d, F4,d,
            // fin del bloque 0
            ToneControl.BLOCK_END, 0, 
            // inicio del bloque 1
            ToneControl.BLOCK_START, 1, 
            // notas del bloque 1
            C4,d, E4,d, E4,d, C4,d, E4,d, E4,d, C4,d, E4,d,
            // fin del bloque 1
            ToneControl.BLOCK_END, 1, 
            // reproducir bloque 0
            ToneControl.PLAY_BLOCK, 0, 
            // reproducir bloque 1
            ToneControl.PLAY_BLOCK, 1, 
            // reproducir bloque 0
            ToneControl.PLAY_BLOCK, 0, 
        };

        try{
            Player p = Manager.createPlayer(Manager.TONE_DEVICE_LOCATOR);
            p.realize();
            ToneControl c = (ToneControl)p.getControl("ToneControl");
            c.setSequence(secuencia);
            p.start();
        } catch (IOException ioe) {
        } catch (MediaException me) {}
    }
}

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