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