Anda di halaman 1dari 181

ÍNDICE

1. ALMACENAMIENTO PERSISTENTE (RMS) ....................................................................................1

2. COMUNICACIONES DE RED..........................................................................................................27

3. EL API MULTIMEDIA.......................................................................................................................67

4. DESARROLLO DE JUEGOS CON J2ME......................................................................................113

GLOSARIO.........................................................................................................................................169

BIBLIOGRAFÍA..................................................................................................................................175

REFERENCIAS WEB.........................................................................................................................177
Almacenamiento
persistente
Tema 1
(RMS)

1.1. INTRODUCCIÓN................................................................................................................................... 3
1.1.1. La plataforma ...................................................................................................................... 3
1.1.2. Aplicaciones J2ME. MIDlets............................................................................................... 4
1.1.3. Interfaces de bajo y alto nivel ........................................................................................... 5
1.1.3.1. Bajo nivel............................................................................................................... 6
1.1.3.2. Alto nivel ............................................................................................................... 6
1.2. ALMACENAMIENTO PERSISTENTE (RMS) ....................................................................................... 7
1.2.1. Cómo almacena datos MIDP.............................................................................................. 7
1.2.2. Elementos del API implicados........................................................................................... 9
1.2.2.1. Interfaces............................................................................................................... 9
1.2.2.2. Clases.................................................................................................................. 12
1.2.2.3. Excepciones........................................................................................................ 15
1.2.3. Manipulación de almacenes ............................................................................................ 15
1.2.4. Búsqueda y ordenación de registros.............................................................................. 21
1.2.4.1. Búsqueda ............................................................................................................ 21
1.2.4.2. Ordenación.......................................................................................................... 24

-1-
1.1. INTRODUCCIÓN

El objetivo del presente curso es dotar a alumnos que ya poseen nociones básicas en la programación
bajo la plataforma J2ME, Java 2 Micro Edition, de más conocimientos y habilidades para alcanzar un nivel
más avanzado. No obstante, antes de entrar en la materia, realizaremos en este apartado un breve
resumen de algunos conceptos básicos.

1.1.1. La plataforma

En el gráfico se muestra qué lugar ocupa la plataforma J2ME en el mundo Java. Dicha plataforma se
divide en configuraciones y en perfiles por encima de ellas.

En función de las características del dispositivo para el cual desarrollemos nuestras aplicaciones, nos
acogeremos a las normas impuestas por alguna de las especificaciones que aparecen en el gráfico. En
nuestro caso, al orientarnos al desarrollo para dispositivos (teléfonos móviles) de capacidades muy
restringidas en gráfica, procesamiento y memoria, trabajaremos bajo la combinación siguiente:

− Configuración Connected Limited Device Configuration, CLDC (versión 1.1), a la cual nos obliga
la reducida Kilo Virtual Machine (KVM) que utilizan estos dispositivos para interpretar los
bytecodes JAVA que generemos.

-3-
− Perfil Mobile Information Device Profile, MIDP, en su última y mejorada versión 2.0.

1.1.2. Aplicaciones J2ME. MIDlets

Una aplicación JAVA que cumpla las especificaciones CLDC y MIDP será denominada MIDlet, y a varias
de ellas empaquetadas en un mismo elemento (JAR) se le denominará SUITE. El dispositivo nos
permitirá seleccionar una u otra aplicación de la suite como él considere apropiado (teléfonos móviles por
medio de una lista, usualmente PDAs por medio de iconos).

Los MIDlets siempre tendrán tres métodos básicos que marcan los estados de su ciclo de vida:

− Pausado. Estado "en espera" en el que el MIDlet mantiene los mínimos recursos posibles,
entrando en él al crearse, antes de ejecutarse su método startApp() o tras llamarse a su método
pauseApp(). La plataforma puede pasar el MIDlet a este estado si así lo estima oportuno (por
ejemplo, ante una llamada telefónica).

− Activado. Estado de ejecución del MIDlet al que pasa tras ejecutar su método startApp(), tanto
inicialmente, como ante la recuperación de una pausa o ante casos especiales que ya
estudiaremos en el siguiente capítulo. Desde este último podrá pasarse al anterior y viceversa.

− Destruido. Los dos estados anteriores pueden pasar a éste y de él ya no se podrá salir. Es el
estado donde el MIDlet concluye su actividad, pasando a él por medio de la invocación de su
método destroyApp() o, por ejemplo, ante excepción en el constructor del MIDlet.

Por otra parte, para producir un MIDlet pasaremos por las siguientes fases:

− Desarrollo. Crear el código fuente, para lo cual tendremos la parte de J2SE disponible en J2ME
(parte de los paquetes java.util, java.lang y java.io) la cual se hereda en las especificaciones
CLDC y MIDP. Por otro lado, estas dos ofrecerán sus propios paquetes (java.microedition.io,
java.microedition.lcdui, java.microedition.midlet, java.microedition.rms, etc.).

− Compilación. En esta fase pasamos nuestro código fuente a bytecode interpretable por la
máquina virtual. Para esta fase y las siguientes usaremos en este curso la herramienta de SUN
J2ME Wireless Toolkit (no ofrece editor), aunque existirán múltiples opciones con editores
propios (NetBeans Mobility Pack, EclipseME, etc.) más cómodas.

-4-
− Preverificación. De ella se encargan las máquinas virtuales preparadas para J2SE, no obstante,
dada la escasa capacidad de la KVM, será necesario realizarla antes de que nuestros bytecodes
pasen a ser interpretados por esta reducida máquina virtual. En esta fase se realizan
comprobaciones sobre los bytecode en tiempo de compilación, verificando que son correctos a
este tiempo (sobrecarga de la pila, uso de variables sin inicializar, etc.).

− Empaquetado. Con él se genera el JAR que acoge a la suite que creemos (tenga uno o más de
un MIDlet) y el JAD descriptor de la suite. En el JAR se aglutinan las clases de los MIDlets,
clases auxiliares que hayamos necesitado, recursos a utilizar por ellas (imágenes, sonidos, etc.)
y el fichero de manifiesto. De esta forma, se hace muy sencilla la distribución y descarga de la
suite.

− Ejecución y pruebas: Las pruebas de ejecución de nuestras aplicaciones las realizaremos


inicialmente sobre un simulador, ya sea genérico (J2ME Wireless Toolkit) o específico de algún
fabricante (Motorola, Nokia, etc.).

Una vez probado en el emulador, si deseamos llevarlo a un dispositivo real, seguiremos varios
pasos: la localización del MIDlet, su descarga y almacenamiento en el dispositivo y, una vez allí,
gestionarlo hasta permitirnos interactuar con él. Para llevar a cabo esto, todo dispositivo debe
contar con un gestor de aplicaciones (Application Management Software o AMS) residente en
memoria, el cual controlará este proceso, la actualización de MIDlets ya existentes, su
eliminación definitiva del dispositivo y, en general, toda la funcionalidad relacionada con la
gestión de las aplicaciones a ofrecer.

1.1.3. Interfaces de bajo y alto nivel

Con nuestros MIDlets tendremos dos formas de comunicarnos con el usuario: utilizando una interface
de bajo o de alto nivel, ambas generando eventos ante la interacción de éste que escucharemos
(listeners) para ejecutar una u otra acción.

De forma general, a bajo y alto nivel, toda interface tendrá un elemento principal, la pantalla, la cual
controlamos por medio de la clase Display. Ésta podrá manejar distintas instancias de la clase
Displayable, con la cual se representarán las distintas pantallas de las que se podrá componer la
aplicación.

-5-
Además, para ambos niveles podremos definir para la interacción del usuario, comandos a un nivel
genérico (tales como, salir, cancelar, regresar, etc.) por medio de la clase Command, los cuales serán
escuchados por nuestra aplicación al instanciar elementos que implementen la interface
CommandListener.

1.1.3.1. Bajo nivel

Una aplicación de bajo nivel es aquélla que utiliza elementos gráficos dibujados píxel a píxel sobre la
pantalla del dispositivo y recibirá del usuario eventos directos de teclado que provoquen un cambio sobre
el redibujo de estos gráficos, por ejemplo, para el desarrollo de juegos.

Con esta forma de trabajar tendremos un control preciso de lo que deseamos dibujar en pantalla, aunque
ello suponga tener que dibujarlo todo "a mano", con el trabajo extra que supone (por ejemplo, si
deseamos un botón, debemos dibujar un rectángulo que lo represente). Por ello, debemos cuidar mucho
la portabilidad de un dispositivo a otro, ya que, muchas de las características que manejaremos serán
distintas de una pantalla a otra (por ejemplo, las dimensiones de ésta).

La clase Canvas será la que representa la pantalla a su más bajo nivel y la que se utiliza para dibujar
sobre ella. Asimismo, dispone de varios métodos para captar los eventos directos de teclado que el
usuario provoque y así reaccionar convenientemente ante ellos. Normalmente, se creará un elemento que
extienda la clase Canvas y donde se sobreescriban el método paint(), encargado de dibujarla, y los
métodos encargados de capturar eventos de teclado (keyPressed(), keyReleased(), etc.).

Por otro lado, con la clase Graphics dispondremos de multitud de funciones de dibujo sobre la pantalla,
métodos para trazar líneas, dibujar rectángulos, incluir imágenes, etc.

1.1.3.2. Alto nivel

Las aplicaciones de alto nivel, sin embargo, se presentan en pantalla por medio de componentes o
controles predefinidos (cajas de texto, listas de elementos, etc.), de los cuales se supone su existencia en
todo dispositivo que cumpla con el perfil MIDP. Esto garantiza la portabilidad de las aplicaciones y su
desarrollo más directo, aunque la presentación gráfica de estos componentes quede en manos de cada
dispositivo concreto, con lo cual no tendremos un control preciso del resultado gráfico final.

La clase Screen será la superclase de la que hereden todas las clases del API de alto nivel. Como hijas
suyas aparecerán las clases con las que instanciamos los componentes antes citados, tales como los

-6-
formularios (Form), las cajas de texto (TextBox), etc. Con todos estos controles crearemos la GUI de
nuestras aplicaciones de alto nivel, usualmente destinadas a herramientas de negocio.

1.2. ALMACENAMIENTO PERSISTENTE (RMS)

Una vez recordados algunos conceptos previos que supondremos a partir de ahora, comenzaremos con
este apartado los contenidos propios de este curso avanzado que nos ocupa.

Para ello, en primer lugar, estudiaremos la forma en que el perfil MIDP almacenará datos en el dispositivo
móvil de forma persistente, de forma que no se pierdan de una ejecución a otra del MIDlet que los
almacenó, y también para que puedan ser compartidos por otros MIDlets que formen con el primero una
misma aplicación o suite (MIDlets en un mismo JAR).

1.2.1. Cómo almacena datos MIDP

Los elementos del perfil MIDP que nos permiten el almacenamiento persistente de datos en el dispositivo
forman el Record Management System (RMS). Este sistema de almacenamiento representa una base
de datos orientada a registros, única en el dispositivo, la cual será común a todas las aplicaciones que
éste albergue.

-7-
Cada suite podrá reservar una o varias zonas de almacenamiento o almacén de registros en el RMS
(implementada, como veremos, con la clase RecordStore), los cuales se identificarán con nombres, de
cómo máximo 32 caracteres, donde se distingue entre mayúsculas y minúsculas. Asimismo, distintas
suites pueden reservar almacenes dándoles el mismo nombre y el RMS seguirá considerándolas como
zonas claramente separadas al pertenecer a distintas aplicaciones.

Además de este nombre, el cual sí debe ser único entre los distintos almacenes que pueda reservar una
misma suite, un almacén de registros poseerá distintos atributos consultables mediante métodos de la
clase RecordStore:

− Número de registros, iniciándose en 0 y aumentando/disminuyendo conforme se vayan


insertando o eliminando registros del almacén (getNumRecords()).

− Número de versión, el cual irá actualizándose según varíe el estado del almacén dado
(getVersion()); comienza en 0 y va aumentando conforme efectuemos acciones sobre el
almacén.

− Fecha de última modificación, número de milisegundos desde el 01/01/1970 hasta el instante


en que se haya modificado por última vez el almacén (getLastModified()).

− Identificador del siguiente registro que se cree en el almacén, comenzando en 1 e


incrementándose cada vez que se inserte un nuevo registro en la zona de almacenamiento de la
suite (getNextRecordID()).

-8-
Tras estos atributos, pertenecientes a la cabecera del almacén (gris oscuro en el gráfico anterior),
aparecerán los registros en los cuales guardaremos la información a almacenar como pares índice -
contenido. Así, cada fila de una zona de almacenamiento irá identificada unívocamente por un índice
(1..n) el cual da acceso al valor que se haya guardado en ella. Este valor, sea del tipo que sea, se
almacena en forma de array de bytes.

Por último, indicaremos que la implementación RMS asegura la atomicidad, sincronización y serialización
de las operaciones sobre un almacén de registros de forma que no exista corrupción alguna provocada
por accesos simultáneos a los mismos recursos. No obstante, en aplicaciones multitarea con acceso a
datos es responsabilidad del programador sincronizar las operaciones sobre éstos.

1.2.2. Elementos del API implicados

El paquete javax.microedition.rms, ya presente desde la especificación MIDP 1.0 y con pocos cambios
desde ella, nos proporciona todos los recursos necesarios para llevar a cabo el almacenamiento
persistente de datos en nuestro dispositivo. Todos los elementos públicos que nos encontramos en él,
extraídos del javadoc de la especificación MIDP 2.0 actual, los detallamos en los siguientes apartados.

1.2.2.1. Interfaces

− RecordComparator

Con esta interface construimos métodos de ordenación sobre el almacén. Será un comparador
que nos proporciona tres constantes públicas y un solo método para comparar dos registros:

• static int EQUIVALENT. El contenido de los dos registros a comparar coincide.

• static int FOLLOWS. El primer registro queda tras el segundo al compararlos.

• static int PRECEDES. El primer registro queda delante del segundo al compararlos.

• int compare(byte[] rec1, byte[] rec2). Devuelve una de las constantes anteriores tras
comparar los dos contenidos de registro dados.

-9-
− RecordEnumeration

Con ella tendremos un enumerador para recorrer el almacén que lo origine en ambas
direcciones, iniciándose justo antes del primer elemento.

Los métodos disponibles son:

• void destroy(). Libera los recursos asignados a este enumerador.

• boolean hasNextElement(). Indica si existe un siguiente elemento en la dirección de


avance.

• boolean hasPreviousElement(). Indica si existe un siguiente elemento en la dirección


de retroceso.

• boolean isKeptUpdated(). Indica si el enumerador se mantiene actualizado con los


cambios en el RecordStore origen.

• void keepUpdated(boolean keepUpdated). Modifica la condición que observaba el


anterior método.

• byte[] nextRecord(). Devuelve una copia del contenido del registro siguiente en el
enumerador, avanzando el puntero de registro actual con el que hacemos el recorrido y,
tras ello, retornando el elemento que encuentre.

Al crear el enumerador (ver más adelante el método enumerateRecords() de la clase


RecordStore) podremos proporcionarle un comparador y/o un filtro, los cuales
impondrán la secuencia de registros que pasan al enumerador, obviamente marcando
con ello quién es el siguiente registro.

• int nextRecordId(). Devuelve el identificador de ese registro siguiente tras avanzar en


el recorrido.

• int numRecords(). Número de registros en el enumerador.

- 10 -
• byte[] previousRecord(). Análogo al método nextRecord, para el registro anterior al
actual en el enumerador.

• int previousRecordId(). Identificador de ese registro anterior.

• void rebuild(). Refresca el enumerador con los datos actuales del RecordStore que lo
originó.

• void reset(). Devuelve el enumerador a su estado original, tal y como se encontraba al


crearse.

− RecordFilter

Con esta interface implementaremos búsquedas sobre los elementos del almacén. Un solo
método para devolver si el contenido parámetro cumple con el criterio que demos al
implementarlo:

• boolean matches(byte[] candidate)

− RecordListener

Con él implementamos un escuchador de eventos (listener) para capturar qué ha sucedido sobre
un registro dado:

• void recordAdded(RecordStore recordStore, int recordId). Registro añadido al


almacén.

• void recordChanged(RecordStore recordStore, int recordId). Registro modificado


en el almacén.

• void recordDeleted(RecordStore recordStore, int recordId). Registro eliminado del


almacén.

- 11 -
1.2.2.2. Clases

− RecordStore

Única clase del paquete, la cual instancia un almacén de datos y ofrece 23 métodos y dos
constantes. Las excepciones que eleva cada método las comentaremos más adelante.

• static int AUTHMODE_ANY. Constante. Indica que cualquier suite tiene permitido el
acceso a este almacén.

• static int AUTHMODE_PRIVATE. Constante. Sólo se permite el acceso a la suite


actual.

• int addRecord(byte[] data, int offset, int numBytes). Añade un nuevo registro al
RecordStore, devolviendo el identificador que le toque. Le pasamos el array de bytes
que lleva el contenido a almacenar, el índice de este array desde donde se considera el
contenido y el número de elementos del array a almacenar a partir de él.

• void addRecordListener(RecordListener listener). Añade el escuchador parámetro al


RecordStore, si éste aún no ha sido asignado.

• void closeRecordStore(). Cierra el almacén, necesitándose para ello que este método
sea llamado tantas veces como ocasiones se llamó a su apertura. Cuando se cierra el
RecordStore, todos sus escuchadores son eliminados y los RecordEnumerations
asociados invalidados.

• void deleteRecord(int recordId). Elimina el registro identificado del almacén. Su índice


no es reutilizado.

• static void deleteRecordStore(String recordStoreName). Eliminación del


RecordStore identificado de la base de datos RMS, exclusivamente a manos de su suite
propietaria. Eleva excepción si el almacén está abierto (por su suite o cualquier otra, si
existe ese permiso) o si éste no existe.

Este método no provoca llamada alguna a los recordDeleted() de los escuchadores


asociados.

- 12 -
• RecordEnumeration enumerateRecords(RecordFilter filter, RecordComparator
comparator, boolean keepUpdated). Devuelve un enumerador con el cual podremos
recorrer los registros que almacena el RecordStore. Si se especifica el parámetro filter
(si no se desea, dar null) sólo se recorrerán los registros que cumplan el filtro.

El RecordComparator definirá el orden en el que se recorren los registros (también


opcional), y el tercer parámetro indica si el enumerador se mantiene actualizado o no
ante cambios en los registros del RecordStore que lo origina.

• long getLastModified(). Devuelve el atributo “fecha de la última modificación producida


en el almacén” de la cabecera del almacén comentada anteriormente.

• String getName(). Proporciona el nombre identificativo del RecordStore. También en


la cabecera del almacén.

• int getNextRecordID(). Devuelve un atributo de la cabecera del almacén. En este caso,


el identificador que le tocará al siguiente registro que se inserte.

• int getNumRecords(). Devuelve el atributo “número de registros del almacén“ de la


cabecera del almacén.

• byte[] getRecord(int recordId). Obtiene una copia de los datos almacenados en el


registro identificado del RecordStore. Si están vacíos devuelve null, y si el id no es
válido elevará excepción.

• int getRecord(int recordId, byte[] buffer, int offset). Análogamente al anterior,


obtiene una copia del registro identificado por recordId, aunque esta vez se guarda en
el array de E/S dado como parámetro. La copia se hace en el buffer a partir del índice
dado por el offset y devuelve el número de bytes que se han copiado.

• int getRecordSize(int recordId). Tamaño en bytes del contenido del registro


identificado.

• int getSize(). Aunque no es atributo de la cabecera del almacén, también da


información general del RecordStore. En este caso, el tamaño en bytes que el almacén
ocupa.

- 13 -
• int getSizeAvailable(). No atributo de la cabecera, da la cantidad de bytes disponible
para que el almacén actual crezca.

• int getVersion(). Devuelve el atributo “número de versión histórica del almacén“ de la


cabecera del almacén.

• static String[] listRecordStores(). Array de nombres de los almacenes de la suite


actual. Si no tiene ningún almacén reservado devolverá null.

• static RecordStore openRecordStore(String recordStoreName, boolean


createIfNecessary). Abre un RecordStore asociado a la suite actual, el cual se
devuelve como salida. Si no existe, se crea previamente, siempre que el segundo
parámetro lo permita: Si createIfNecessary vale false se elevará excepción al intentar
abrir un almacén que no exista.

Si ya ha sido abierto por otro MIDlet de la suite, se devuelve una referencia a esa
instancia abierta. Este método abre el almacén de forma que sólo es accesible por los
MIDlets de la suite actual (AUTHMODE_PRIVATE intrínseco).

• static RecordStore openRecordStore(String recordStoreName, boolean


createIfNecessary, int authmode, boolean writable). Análogo al anterior, aunque
ahora se nos permite dar con el tercer parámetro, permisos de lectura para otras suites,
usando una de las dos constantes vistas anteriormente (AUTHMODE_PRIVATE o
AUTHMODE_ANY). Con el cuarto parámetro damos, además, permisos de escritura a
las demás suites que no sean la actual, la cual siempre se considera la propietaria del
almacén y tiene permiso total sobre él.

• static RecordStore openRecordStore(String recordStoreName, String


vendorName, String suiteName). Abre un almacén existente identificando una suite
mediante el nombre del proveedor de la suite y el nombre de ésta.

La apertura del almacén se permitirá si la suite actual es la identificada (si el almacén se


creó sólo con permiso AUTHMODE_PRIVATE) o en cualquier caso si el almacén se
creó con AUTHMODE_ANY. Éste y el anterior son propios de MIDP 2.0.

- 14 -
• void removeRecordListener(RecordListener listener). Elimina el escuchador
especificado. Si no existe asociado al almacén, no hará nada.

• void setMode(int authmode, boolean writable). Sólo permitido su uso por parte de la
suite propietaria del RecordStore, varía los permisos de acceso por otras suites al
almacén. El primer parámetro proporciona o restringe el permiso de lectura (constantes
ya estudiadas) y el segundo el permiso de lectura. Propio de MIDP 2.0.

• void setRecord(int recordId, byte[] newData, int offset, int numBytes). Por último,
este método modifica el contenido del registro identificado con el array newData. Este
array se tendrá en cuenta sólo a partir del elemento de la posición offset, y sólo un
número numBytes de elementos.

1.2.2.3. Excepciones

− InvalidRecordIDException. Lanzada cuando no es válido un identificador de registro.

− RecordStoreException. Excepción general, de la cual heredan las otras cuatro.

− RecordStoreFullException. El almacén de registros actual no es capaz de almacenar más


información (completo).

− RecordStoreNotFoundException. En el almacén no se encuentra el registro cuyo identificador


se ha solicitado.

− RecordStoreNotOpenException. Se ha intentado una operación sobre un almacén cerrado no


válida en esta situación.

1.2.3. Manipulación de almacenes

A continuación, emplearemos los elementos descritos para llevar a cabo un ejemplo simple de uso del
RMS por parte de una suite compuesta de un solo MIDlet.

Debemos observar que para la clase RecordStore no dispondremos de ningún constructor público,
debiendo usarse uno de los métodos estáticos openRecordStore vistos, que internamente crearán una

- 15 -
instancia de almacén. El código del ejemplo básico, el cual hemos comentado exhaustivamente, es el
siguiente:

RMSEjemploMIDlet.java

import javax.microedition.rms.*;
import javax.microedition.midlet.*;
import javax.microedition.io.*;

public class RMSEjemploMIDlet extends MIDlet {


//Declaramos como atributo el almacén que asociaremos al MIDlet
private RecordStore rs = null;

//Constructor del MIDlet ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++


public RMSEjemploMIDlet(){

//1) Creamos el almacén, el cual queda asociado al MIDlet actual--------------------------------------------------------------------


System.out.println("1. Creamos el almacén");
try{
//Llamamos al método estático, creándolo si aún no existiera. Por defecto, permiso de acceso sólo para este MIDlet
rs = RecordStore.openRecordStore("Peliculas", true);
System.out.println("Versión del estado del almacén: " + rs.getVersion());
}catch(RecordStoreFullException e){
//Si el almacenamiento está lleno
System.out.println(e.toString());
}catch(RecordStoreException e){
//Cualquier otra excepción relacionada. Al dar el parámetro createIfNecessary=true, no se elevará
//RecordStoreNotFoundException
System.out.println(e.toString());
}

//2) Almacenamos tres nuevos registros en el RecordStore----------------------------------------------------------------------------


System.out.println("2. Almacenamos tres nuevos registros");
try{
byte[] registro;
String[] peliculas = {"Matrix", "La Dolce Vita", "Torrente II"};
for(int i=0; i<peliculas.length; i++){
//Pasamos a array de bytes el contenido a almacenar (siempre se almacena este tipo)
registro = peliculas[i].getBytes();

- 16 -
//Añadimos el array de bytes completo (desde su índice 0, todos sus bytes)
rs.addRecord(registro, 0, registro.length);
System.out.println("Película almacenada: " + peliculas[i]);
}
//Preguntamos por el número de registros que han entrado en el almacén
int numRegistros = rs.getNumRecords();
System.out.println("Número de Películas almacenadas: " + numRegistros);

//Llamo a función auxiliar que usa un enumerador para sacar todos los registros del rs
System.out.println("En el RecordStore aparecen como:");
this.recorreRegistros();

}catch(RecordStoreNotOpenException e){
//Si el almacenamiento estaba cerrado (al intentar insertar o al preguntar por su número de elementos)
System.out.println(e.toString());
}
catch(RecordStoreFullException e){
//Si el almacenamiento estaba completo al intentar insertar
System.out.println(e.toString());
}
catch(RecordStoreException e){
//Cualquier otra excepción relacionada
System.out.println(e.toString());
}

//3) Variamos el contenido del penultimo registro en el almacén. A este punto deben existir al menos 2 elementos----
System.out.println("3. Variamos el contenido del penúltimo registro por 'Una Historia del Bronx'");
try{
int penultimo = rs.getNextRecordID() - 2;
//Extraigo el tamaño del penultimo registro
int tamRegistro = rs.getRecordSize(penultimo);
//Con él instancio el buffer donde lo guardo para presentarlo
byte[] registro = new byte[tamRegistro];
//Lo copio completo en el buffer
tamRegistro = rs.getRecord(penultimo, registro, 0);
System.out.println("El penúltimo registro contiene inicialmente: " +
new String(registro) + ". Número de Bytes: " + tamRegistro);
//Varío el contenido del registro en el RecordStore
String nuevaCadena = "Una Historia del Bronx";
rs.setRecord(penultimo, nuevaCadena.getBytes(), 0, nuevaCadena.getBytes().length);

- 17 -
//Lo vuelvo a consultar
tamRegistro = rs.getRecordSize(penultimo);
byte[] registroNuevo = new byte[tamRegistro];
tamRegistro = rs.getRecord(penultimo, registroNuevo, 0);
System.out.println("El penúltimo registro contiene posteriormente: " +
new String(registroNuevo) + ". Número de Bytes: " + tamRegistro);

catch(RecordStoreNotOpenException e){
//Si el almacenamiento estaba cerrado (al consultar o modificar)
System.out.println(e.toString());

}catch(RecordStoreFullException e){
//Si el almacenamiento está lleno al modificar el registro
System.out.println(e.toString());
}
catch(InvalidRecordIDException e){
//Si el índice pasado es inválido
System.out.println(e.toString());
}
catch(RecordStoreException e){
//Cualquier otra excepción relacionada
System.out.println(e.toString());
}

//4) Eliminamos el último registro. Probamos enumeradores---------------------------------------------------------------------------


System.out.println("4. Eliminamos el último registro y probamos un enumerador");
try{
//Llamo a función auxiliar que usa un enumerador para sacar todos los registros del rs
this.recorreRegistros();
System.out.println("El siguiente id que tocaría, antes de eliminar: " + rs.getNextRecordID());
//Elimino el último registro
rs.deleteRecord(rs.getNextRecordID() - 1);
System.out.println("ÚLTIMO REGISTRO ELIMINADO");
//Vuelvo a presentar los registros
this.recorreRegistros();
//Comprobamos con lo siguiente que los índices no se actualizan
System.out.println("El siguiente id que tocaría, tras eliminar: " + rs.getNextRecordID());

- 18 -
}
catch(InvalidRecordIDException e){
//Si el índice pasado es inválido
System.out.println(e.toString());
}
catch(RecordStoreException e){
//Cualquier otra excepción relacionada
System.out.println(e.toString());
}

//5) Cerramos el Almacén------------------------------------------------------------------------------------------------------------------------


System.out.println("5. Cerramos el almacén");
try{
rs.closeRecordStore();
}
catch(RecordStoreNotOpenException e){
//Si el RecordStore no está abierto
System.out.println(e.toString());
}
catch(RecordStoreException e){
//Cualquier otra excepción relacionada
System.out.println(e.toString());
}

//6) Eliminamos el almacén, liberando así al RMS (INICIALMENTE COMENTADO)--------------------------------------------


System.out.println("6. Eliminamos el almacén. Inicialmente comentamos este punto para comprobar en sucesivas
ejecuciones del MIDlet que los datos persisten");
/* try{
RecordStore.deleteRecordStore("Peliculas");
}

catch(RecordStoreNotFoundException e){
//Si el nombre dado no es válido
System.out.println(e.toString());
}
catch(RecordStoreException e){
//Cualquier otra excepción relacionada
System.out.println(e.toString());
}*/

- 19 -
}//fín del constructor

//Función auxiliar de recorrido+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++


//Creamos un enumerador para pasar por los registros del almacén. Sin filtro ni comparador (se recorrerán en el
//mismo orden que están en el RecordStore original) ni actualización ante cambios en el rs
public void recorreRegistros(){
try {
int numRegistros = rs.getNumRecords();
System.out.println("Recorremos " + numRegistros + " registros");
if(numRegistros > 0){
//Creamos el enumerador
RecordEnumeration renum = rs.enumerateRecords(null, null, false);
while(renum.hasNextElement())
{
//Recogemos el id del registro y avanzamos en el recorrido. Si usáramos tanto nextRecordId()
//como nextRecord() avanzaríamos de 2 en 2; cada uno de estos métodos mueve el puntero.
int id = renum.nextRecordId();
byte[] contenido = rs.getRecord(id);
System.out.println("Índice: " + id + ", Contenido: " + new String(contenido));
}
//Liberamos el contenido del enumerador
renum.destroy();
}
}catch(RecordStoreException e){
System.out.println(e.toString());
}
}

//Vida del MIDlet +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++


public void startApp(){
//Simplemente tras pasar por el constructor (donde está el código que nos interesa) destruye el MIDlet
this.destroyApp(true);
this.notifyDestroyed();
}
public void pauseApp(){ }
public void destroyApp(boolean flag){ }

}//fín del MIDlet

- 20 -
Al ejecutar por primera vez este código, la salida por consola que se obtiene es:

1. Creamos el almacén
Versión del estado del almacén: 0
2. Almacenamos tres nuevos registros
Película almacenada: Matrix
Película almacenada: La Dolce Vita
Película almacenada: Torrente II
Número de Películas almacenadas: 3
En el RecordStore aparecen como:
Recorremos 3 registros
Índice: 3, Contenido: Torrente II
Índice: 2, Contenido: La Dolce Vita
Índice: 1, Contenido: Matrix
3. Variamos el contenido del penúltimo registro por 'Una Historia del Bronx'
El penúltimo registro contiene inicialmente: La Dolce Vita. Número de Bytes: 13
El penúltimo registro contiene posteriormente: Una Historia del Bronx. Número de Bytes: 22
4. Eliminamos el último registro y probamos un enumerador
Recorremos 3 registros
Índice: 2, Contenido: Una Historia del Bronx
Índice: 3, Contenido: Torrente II
Índice: 1, Contenido: Matrix
El siguiente id que tocaría, antes de eliminar: 4
ÚLTIMO REGISTRO ELIMINADO
Recorremos 2 registros
Índice: 2, Contenido: Una Historia del Bronx
Índice: 1, Contenido: Matrix
El siguiente id que tocaría, tras eliminar: 4
5. Cerramos el almacén
6. Eliminamos el almacén. Inicialmente comentamos este punto para comprobar en sucesivas ejecuciones
del MIDlet que los datos persisten

Será muy interesante ir ejecutando sucesivamente el MIDlet para comprobar cómo varía el contenido del
almacén que reserva y el orden de sus elementos en él; así como verificar fehacientemente como los
datos van quedando almacenados de forma persistente. Si se desea reiniciar el almacén, descomentar el
punto 6. del código. Por último, también comprobaremos, si variamos sucesivamente el nombre dado al
RecordStore al crearlo, cómo un mismo MIDlet puede reservar varios almacenes diferentes del RMS.

- 21 -
1.2.4. Búsqueda y ordenación de registros

En el ejemplo anterior hemos visto cómo podemos recorrer los elementos del almacén por medio de la
interface RecordEnumeration. Las otras tres interfaces del paquete rms nos capacitarán para buscar un
registro, ordenar los existentes en el almacén y definir un listener para el almacén. En esto último no nos
detendremos, RecordListener será un escuchador como otro cualquiera: la clase que lo implemente dará
cuerpo a sus tres métodos vistos, los cuales serán llamados al ocurrir el evento adecuado. Un
RecordStore podrá tener tantos escuchadores registrados como desee.

1.2.4.1. Búsqueda

Para la búsqueda de un registro o grupo de ellos, de entre todos los del RecordStore se utilizará la
interface RecordFilter, en combinación con la RecordEnumeration ya vista. Dando un filtro como
parámetro de un enumerador que instanciemos para el RecordStore, conseguiremos una "vista" de los
datos totales formada sólo por los elementos que deseemos, quedando ésta almacenada en el
enumerador para utilizarla como deseemos. Daremos cuerpo al único método de la interface, matches(),
en el cual definimos qué debe cumplir un registro para formar parte de la vista. Con el siguiente ejemplo,
en el que implementamos una clase que nos ayudará a buscar las películas del ejemplo anterior que
tengan más de una palabra en su nombre (buscaremos cadenas formadas por un espacio), se observará
perfectamente lo explicado:

FiltroRMS.java
import javax.microedition.rms.*;
import javax.microedition.midlet.*;
import java.io.*; //Las clases Stream no están en javax.microedition.io, sino en la de J2SE

public class FiltroRMS implements RecordFilter {


private String cadenaBuscar = null;

//Constructor del filtro+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++


//Damos al construirlo la cadena que deseamos buscar (llegará espacio " " en el ejemplo)
public FiltroRMS(String cad){
this.cadenaBuscar = cad.toUpperCase(); //Para comparar en Mayúsculas ambas cadenas
}

//Método de búsqueda matches+++++++++++++++++++++++++++++++++++++++++++++++++++++++


public boolean matches(byte[] registro){
boolean res = false;

- 22 -
String contenido;
//Utilizamos los dos tipos siguientes para generalizar a cualquier tipo de dato almacenado en el
//registro, no sólo String (eso sí, siempre en él se guardan como array de bytes)
ByteArrayInputStream bStream;
DataInputStream dStream;
try{
bStream = new ByteArrayInputStream(registro);
dStream = new DataInputStream(bStream);

//Leemos caracteres del stream. Variaremos este método si el contenido es de otro tipo,
//por ejemplo si tenemos almacenados floats usaríamos el método DataInputStream.readFloat()
byte[] aux = new byte[registro.length];
dStream.read(aux);
contenido = new String(aux);
//NOTA: Usamos lo anterior en vez de 'contenido = dStream.readUTF();' pues este método
//puede dar problemas

if( (contenido != null) && (contenido.toUpperCase().indexOf(cadenaBuscar) != -1) )


res = true; //Encontrado lo buscado: este registro entrará en la vista
else
res = false; //No encontrado lo buscado: este registro no entrará en la vista
}
catch(Exception e){
System.out.println(e.toString());
res = false;
}
return res;
}

}//fin FiltroRMS

Para probar esta clase, bastará cambiar en el código del MIDlet anterior la línea:

RecordEnumeration renum = rs.enumerateRecords(null, null, false);

Por:

FiltroRMS busqueda = new FiltroRMS(" "); //Damos aquí la cadena a buscar (en este ejemplo, el espacio)
RecordEnumeration renum = rs.enumerateRecords(busqueda, null, false);

- 23 -
Con lo cual aplicaríamos un filtro a los elementos que recibe el enumerador, almacenándose en éste sólo
los registros de películas cuyo nombre tenga varias palabras (contenga espacios).

1.2.4.2. Ordenación

Para ordenar los registros del almacén utilizamos la interface RecordComparator, la cual funcionará de
forma muy parecida a la interface RecordFilter estudiada. Asimismo, implementaremos un método
compare() que decidirá si un primer registro parámetro es mayor, menor o coincide con el segundo
registro parámetro pasado, devolviendo esa información en forma de constante; una de las tres que ya
vimos al definir la interface.

Siguiendo con nuestro ejemplo, implementemos un comparador que devuelva una vista de los registros
del almacén en orden alfabético:

ComparadorRMS.java
import javax.microedition.rms.*;
import javax.microedition.midlet.*;
import java.io.*;

public class ComparadorRMS implements RecordComparator {

//Constructor del Comparador, vacío++++++++++++++++++++++++++++++++++++++++++++++++++++


public ComparadorRMS(){
}

//Método de comparación compare+++++++++++++++++++++++++++++++++++++++++++++++++++++


public int compare(byte[] registro1, byte[] registro2){

int res = RecordComparator.EQUIVALENT; //Por defecto, iguales


String contenido1, contenido2;
//Utilizamos los dos tipos siguientes para generalizar a cualquier tipo de dato almacenado en el
//registro, no sólo String (eso sí, siempre en él se guardan como array de bytes)
ByteArrayInputStream bStream;
DataInputStream dStream;
try{
//Sacamos a String el contenido del primer registro
bStream = new ByteArrayInputStream(registro1);
dStream = new DataInputStream(bStream);

- 24 -
//Variar el siguiente método según sea el tipo del contenido del registro
//contenido1 = dStream.readUTF(); DESACONSEJADO
byte[] aux1 = new byte[registro1.length];
dStream.read(aux1);
contenido1 = new String(aux1);
//Sacamos a String el contenido del segundo registro
bStream = new ByteArrayInputStream(registro2);
dStream = new DataInputStream(bStream);
//Variar el siguiente método según sea el tipo del contenido del registro
//contenido2 = dStream.readUTF(); DESACONSEJADO
byte[] aux2 = new byte[registro2.length];
dStream.read(aux2);
contenido2 = new String(aux2);

//NOTA: El siguiente método es la clave, variará según cómo deseemos ordenar los registros
//y de qué tipo de dato sea su contenido
res = contenido1.compareTo(contenido2);
if(res == 0)
res = RecordComparator.EQUIVALENT;
else if(res < 0)
res = RecordComparator.PRECEDES;
else
res = RecordComparator.FOLLOWS;
}
catch(Exception e){
System.out.println(e.toString());
res = -1;
}
return res;
}

}//fín ComparadorRMS

Para probar esta clase, bastará cambiar en el código del MIDlet anterior la línea:

RecordEnumeration renum = rs.enumerateRecords(null, null, false);

- 25 -
Por:

ComparadorRMS comparador = new ComparadorRMS();


RecordEnumeration renum = rs.enumerateRecords(null, comparador, false);

De este modo, aplicaríamos el comparador a los elementos que recibe el enumerador, de forma que irán
almacenándose en éste por orden alfabético. El uso del enumerador ya nos permitirá recorrer el
RecordStore en el orden establecido.

- 26 -
RECUERDE

− J2ME es la parte de JAVA enfocada a dispositivos de escasos recursos.

− Programaremos bajo configuración CLDC (máquina virtual KVM) y perfil MIDP. Las aplicaciones
JAVA construidas siguiendo esta especificación y perfil son denominadas MIDlets, formando una
SUITE un conjunto de ellas ofrecidas en el mismo JAR.

− Una aplicación J2ME respecto a su interface de usuario puede ser de bajo nivel, es decir,
utilizará la pantalla como lienzo donde dibujará pixel a pixel lo que desee ofrecer al usuario; o de
alto nivel donde se utilizarán componentes estándar que todo dispositivo asegurará poder
presentar de una forma u otra.

− El Record Management System (RMS) es el sistema que utilizan los dispositivos MIDP para
almacenar datos persistentemente. Sólo existe uno para todas las suites que el dispositivo
utilice, pudiendo tener cada una de ellas, una o varias zonas de almacenamiento (RecordStore)
en él reservadas.

− Para instanciar un RecordStore usaremos cualquier sobrecarga del método estático


openRecordStore(), al no ofrecérsenos ningún constructor público. Manipularemos el almacén
como deseemos y, finalmente, lo cerraremos con closeRecordStore(), tantas veces como se
haya llamado a su apertura.

− Además de la clase RecordStore, conocemos cuatro interfaces que nos posibilitarán el recorrido
del RecordStore (RecordEnumeration); la búsqueda de registros que cumplan un cierto patrón
dado (RecordFilter); una con la cual ordenar los elementos del almacén (RecordComparator)
y, por último, una con la cual asociar escuchadores de eventos al RecordStore
(RecordListener).

- 27 -
Comunicaciones
Tema 2 de Red

2.1. INTRODUCCIÓN...........................................................................................................................31
2.2. ELEMENTOS DEL API IMPLICADOS..........................................................................................33
2.2.1. J2SE...................................................................................................................................34
2.2.2. CLDC 1.1 ...........................................................................................................................34
2.2.2.1. Interfaces ............................................................................................................ 35
2.2.2.2. Clases.................................................................................................................. 38
2.2.2.3. Excepciones ....................................................................................................... 40
2.2.3. MIDP 2.0 ............................................................................................................................40
2.2.3.1. Interfaces ............................................................................................................ 41
2.2.3.2. Clases.................................................................................................................. 48
2.3. COMUNICACIÓN HTTP................................................................................................................50
2.3.1. Fases en la comunicación HTTP.....................................................................................51
2.4. CONVERSACION MIDLET - SERVLET........................................................................................53
2.5. OTRAS CUESTIONES SOBRE HTTP..........................................................................................63
2.5.1. Redireccionamiento URL.................................................................................................63
2.5.2. Uso de Cookies ................................................................................................................64

- 29 -
2.1. INTRODUCCIÓN

Dada la gran cantidad de restricciones que supone programar bajo el perfil MIDP, nuestra capacidad en
casi todos los campos (interfaces gráficas de usuario, almacenamiento de datos, tipado, etc.), se ve
mermada ante lo que sería trabajar pensando en equipos de superiores características, con máquinas
virtuales que soportarán toda la potencia del lenguaje JAVA. No obstante, esto ocurre en todos menos en
uno, que es donde reside la característica más importante y obvia de un dispositivo móvil que actúe bajo
MIDP: su capacidad de comunicación con el exterior.

Así, en estos dispositivos siempre tendremos la oportunidad de estar conectados a una red y
comunicarnos por ella de diferentes formas con otros dispositivos móviles, servidores de aplicaciones,
bases de datos; en definitiva, cualquier ente que tenga capacidad de establecer una comunicación con
nosotros.

Este segundo capítulo que nos ocupa puede tomarse, por tanto, como el más importante de los que
veremos en el curso, ya que aquí explotaremos la característica principal de nuestros dispositivos: la
comunicación en red que éstos nos ofrecen y la capacidad de transmitir cualquier tipo de información
mientras ésta permanezca abierta.

Las comunicaciones en red J2ME más interesantes a programar hoy día son las basadas en el protocolo
HTTP (Hyper Text Transfer Protocol). Dicho protocolo nos proporciona la capacidad de crear aplicaciones
cliente-servidor para llevar la lógica de trabajo más pesada de nuestra aplicación cliente hacia un servidor
J2EE en el cual, por ejemplo, poseeremos toda la capacidad del lenguaje JAVA y acceso a SGBD
(Sistemas de Gestión de Bases de Datos) potentes con los que tratar, para con ello, una vez realizada la
tarea requerida, devolver una respuesta al dispositivo.

En este tipo de comunicación nos centraremos en este capítulo, ya que será el protocolo estrella por su
flexibilidad, estar disponible universalmente, estar implementado por todo dispositivo que se precie de
funcionar bajo MIDP 2.0 y, además, ser el protocolo de transporte usado por mecanismos de servicios
web como XML-RPC y SOAP. HTTP podrá implementarse utilizando tanto protocolos IP (como el TCP/IP)
como protocolos no-IP (como WAP o I-MODE).

La especificación MIDP 2.0 también recomienda (aunque no la exige, a diferencia de HTTP y HTTPS) la
implementación por parte de los dispositivos de conexiones distintas a la más habitual HTTP, conexiones
por medio de SOCKETS y conexiones usando DATAGRAMAS, sobre las cuales hablaremos al pasar por
los elementos del API implicados. Además de los accesos a red, MIDP 2.0 también define

- 31 -
comunicaciones por PUERTO SERIE lógico con la interface CommConnection. El puerto utilizado por la
conexión estará determinado por el dispositivo y puede no corresponder a un puerto serie RS-232 físico.

Un ejemplo de funcionamiento de una aplicación de negocios J2ME que haga uso de su posibilidad de
conectividad HTTP sería el caso de un viajante, el cual accede al servidor de su empresa donde se
encuentra una aplicación web que le indicará cuál es su siguiente tarea, tras recibir de él en qué punto
geográfico se encuentra actualmente y bajo qué condiciones.

La aplicación web que espera en el servidor estará compuesta idealmente por servlets y páginas JSP
(aunque podrá comunicarse nuestro MIDlet cliente con otras tecnologías de servidor, la relación entre
J2ME y J2EE será la más práctica), los cuales reaccionarán ante las peticiones HTTP de nuestro MIDlet
como lo hacen ante las peticiones HTTP de un navegador web como pueden ser Mozilla o Internet
Explorer. Este requerimiento lo gestionarán como marque su lógica de negocio y devolviendo una
respuesta HTTP adecuada al MIDlet.

En J2ME no dispondremos de estos navegadores Web, siendo nosotros los encargados de implementar
MIDlets capaces de formar convenientemente una petición HTTP que refleje lo que el usuario requiere y
sea entendible por el servidor y, tras su tratamiento por parte de éste, recoger la respuesta y presentarla
ante el usuario convenientemente.

Aunque a primera vista la creación de una aplicación cliente-servidor con J2ME-J2EE pueda parecer
compleja, más adelante intentaremos explicarlo de forma sencilla. Así, no debemos asustarnos ante la
implementación del cliente MIDP, ya que, la estructura de un cliente J2ME tendrá una lógica de acceso a
la red muy similar de una a otra aplicación, lo cual hará factible su modularización en paquetes e incluso
la generación de este código por medio de programas asistentes.

En este sentido, SUN ofrece una herramienta, J2ME Wireless Connection Wizard, la cual nos facilitará
la creación de este tipo de aplicaciones proporcionando el esqueleto de las clases necesarias tanto para
el cliente como para el servidor y automatizando gran parte de la tarea de codificar la comunicación entre
ellos.

- 32 -
2.2. ELEMENTOS DEL API IMPLICADOS

Los elementos que vemos en la siguiente figura constituyen la jerarquía de interfaces que están
disponibles en el perfil MIDP 2.0 propias de J2ME, relativas a la conexión de red del dispositivo.
Pertenecientes al paquete javax.microedition.io, todas representan una conexión por medio de la cual
intercambiar datos entre nuestro dispositivo y otro ente cualquiera capacitado. Las interfaces superiores
ofrecerán un grado de abstracción más alto a la hora de definir la conexión, siendo la más general de
ellas la interface Connection. Toda conexión en MIDP es pues una Connection en términos abstractos.

De entre ellas, las coloreadas de amarillo las ofrece la configuración CLDC como conexiones básicas
(llamado el Generic Connection Framework de la CLDC) que sirven de punto de partida para que cada
perfil las concrete en su especificación con sus propias subinterfaces. En el caso del perfil MIDP, estas
conexiones más concretas son las interfaces que aparecen coloreadas en naranja, válidas sólo para
dicho perfil. De entre ellas, haremos especial hincapié en la interface HttpConnection, la cual nos
ofrecerá la perseguida conexión HTTP.

Además de esto, dispondremos de parte del paquete java.io heredado de J2SE. Estos elementos de
J2SE, los que especifica la configuración CLDC y, por supuesto, los que incorpora MIDP aparecerán
todos empaquetados en la especificación del perfil MIDP 2.0. Con esta API, pues, construiremos nuestras
aplicaciones con acceso al exterior.

- 33 -
Dada la gran cantidad de elementos de que disponemos, los enumeraremos todos, aunque no los
explicaremos en su totalidad. Además, aprovecharemos la ocasión para describir cada tipo de conexión y,
en el caso de la comunicación http, desarrollaremos la mayoría de sus componentes detenidamente. Este
apartado, pues, nos servirá para explicar el comportamiento de conexiones distintas a la HTTP, ya que
ésta será la única que veremos en profundidad tras él.

2.2.1. J2SE

Los elementos de J2SE relacionados con la comunicación que se nos ofrecen en J2ME pertenecen al
paquete java.io. De él nos llegan las interfaces DataInput y DataOutput, y las clases
ByteArrayInputStream, ByteArrayOutputStream, DataInputStream, DataOutputStream,
InputStream, InputStreamReader, OutputStream, OutputStreamWriter, PrintStream, Reader y
Writer. Recordemos que todas ellas y algunas más se utilizan en J2SE para leer y escribir flujos de datos.

En el tema anterior (RMS) empleábamos algunas de ellas para extraer de un array de bytes, una
instancia de un tipo determinado. No obstante, en esta ocasión nos servirán principalmente para lo
siguiente: en J2ME el paquete javax.microedition.io nos proporciona el soporte para crear el acceso a la
red que en J2SE facilitaba el paquete java.net y que aquí no tendremos ni necesitamos (uno existe en
lugar del otro). Por tanto, gracias a este paquete crearemos y manejaremos las conexiones de red que
deseemos, sean del tipo que sean (HTTP, sockets, etc.). Una vez creadas, será necesario disponer de
recursos con los que leer y escribir los datos que viajen por esa conexión, siendo aquí donde entra en
juego la java.io.

Si la javax.microedition.io nos sirve para instanciar un canal de comunicación, la java.io nos sirve
para leer y escribir datos en él.

2.2.2. CLDC 1.1

Los elementos de los siguientes apartados aparecen en el paquete javax.microedition.io de la


especificación CLDC1.1. En ellos, trataremos los elementos públicos que nos ofrece esta API,
deteniéndonos, sobre todo, en los ancestros de la interface HttpConnection y, por supuesto, en la
decisiva clase Connector.

- 34 -
2.2.2.1. Interfaces

− Connection

Esta interface representa la conexión padre de todas las conexiones J2ME, la más básica. Para
abrir una instancia de ella, como con todas las demás, necesitaremos invocar al método open()
de la clase Connector que ya estudiaremos más adelante. Dispone de un único método, el cual
será heredado por todas las conexiones definidas en CLDC y MIDP: void close()

void close() cierra la conexión. Una vez cerrada, cualquier operación sobre ella eleva una
excepción IOException, salvo un nuevo close(), el cual no tendrá efecto alguno si ya está
cerrada. Si existe algún flujo asociado a la conexión que aún esté abierto al llamar a close(),
hará que la conexión aguante abierta mientras no se cierre ese flujo. El acceso al flujo ya abierto,
pues, estará permitido tras el close(), pero no se podrá acceder de nuevo a la conexión.

− Datagram extends java.io.DataInput, java.io.DataOutput

Esta interface no pertenece a la jerarquía de la Generic Connection Framework, sino que


hereda de las interfaces DataInput y DataOutput de java.io para definir las características de un
paquete Datagrama.

Las instancias de esta interface, por tanto, nos servirán para almacenar datos a leer o escribir
por medio de una conexión DatagramConnection.

− DatagramConnection extends Connection

Con esta interface se define una conexión basada en datagramas, a partir de la cual se podrán
definir distintos protocolos basados en ellos ya a manos de cada perfil. (Más adelante, veremos
cómo lo hace el perfil MIDP usando el protocolo UDP con la UDPDatagramConnection).

La comunicación por medio de datagramas se basa en el envío de paquetes de datos por la red,
a utilizar en defecto de una comunicación por sockets, más fiable, en los casos en que la
conexión entre los dos puntos no se considere estable.

Los datagramas no proporcionan soporte para la reorganización de paquetes, ni se garantiza


que éstos lleguen en el orden en que fueron enviados, aunque habrá métodos para corregir esto

- 35 -
por programación. La principal razón del uso de datagramas es la velocidad, ya que, se evita el
control del orden de los paquetes y la comprobación de su número o integridad.

Por ejemplo, las aplicaciones que necesiten comunicación de audio o vídeo se servirán
comúnmente de este tipo de conexión, ya que, ella se considera siempre exitosa aunque se haya
producido la pérdida de algunos paquetes: esto aparecerá temporalmente en forma de ruido, a la
espera de que una nueva recepción proporcione los paquetes que falten.

− InputConnection extends Connection

Con esta interface definimos una conexión por medio de la cual podremos leer datos, por tanto,
está basada en flujos de entrada. Sus dos únicos métodos son:

• DataInputStream openDataInputStream(). Con este método obtenemos un flujo de


entrada del que leer, cuyos elementos son de un tipo de dato conocido. Para poder
obtener este flujo, previamente debemos haber obtenido una instancia de
InputConnection sobre la que llamar a este método, usando la clase Connector como
ya veremos.

• InputStream openInputStream(). Éste obtiene un InputStream asociado a la conexión.


Un InputStream es padre de DataInputStream en J2SE, representando este último un
flujo más concreto donde sabemos qué tipo de dato deseamos leer. Cuando deseemos
leer bytes en general, podremos obtener simplemente un InputStream.

− OutputConnection extends Connection

En contraposición a la interface anterior, ésta representará conexiones de escritura. Los dos


métodos que posee serán análogos a los de la interface anterior, siendo en este caso de salida:

• DataOutputStream openDataOutputStream(): con este método obtenemos un flujo de


salida en el que escribir, cuyos elementos son de un tipo de dato conocido.

• OutputStream openOutputStream(): Éste obtiene un OutputStream asociado a la


conexión para escribir en él bytes en general.

- 36 -
− StreamConnectionNotifier extends Connection

Utilizándose para sockets que actúen como servidor, esta interface representa la espera de
establecimiento de la conexión que lanzará un posible socket cliente. Con un único método
public StreamConnection acceptAndOpen(), nos notificará si la conexión a emplear con el
cliente se ha realizado con éxito, devolviendo ésta de forma que ya podremos trasvasar los datos
deseados. (Comentaremos este tipo de conexión más detalladamente al llegar a MIDP).

− StreamConnection extends InputConnection, OutputConnection

Esta conexión se basa tanto en flujos de entrada como de salida, es decir, por ella se nos
permitirá tanto leer como escribir datos. No añade ningún método nuevo a los heredados de sus
interfaces padre, simplemente representa un ente con toda la funcionalidad de E/S de sus dos
ancestros inmediatos.

− ContentConnection extends StreamConnection

Padre de la HttpConnection, esta interface define las características de una conexión que
puede describir el contenido que viaja por ella.

Las conexiones vistas hasta ahora transmiten datos sin importar qué representan, pero en ésta
su estructura debe ser conocida de antemano, ya que, aglutinará a los protocolos en los que
aparecen campos describiendo los datos que se transportan de un punto a otro. Los tres
métodos que heredarán todas ellas son:

• String getEncoding(). Informa sobre la codificación del contenido que se está


transmitiendo con una cadena identificativa. Si la conexión es de tipo HttpConnection
heredará este método, informando en ese caso del valor del campo de cabecera HTTP
content-encoding. Devuelve null si no es conocido.

• long getLength(). Longitud en bytes del contenido a transmitir. Si la conexión es una


HttpConnection, dará con este método el valor del campo de cabecera content-length.
Devuelve -1 si no es conocido.

- 37 -
• String getType(). Devuelve el tipo de contenido que se está transmitiendo por la
conexión. En el caso de una HttpConnection, se da con este método el valor del campo
de cabecera content-type. Devuelve null si no es conocido.

2.2.2.2. Clases

− Connector extends Object

La única clase de la especificación actúa como factoría con la que instanciar todos los tipos de
conexiones que estudiaremos. Con ella, podemos realizar cualquier conexión sin importarnos
cómo se implementa internamente la adecuada al protocolo que le estamos requiriendo. Se
usarán para ello llamadas del estilo de:

Connector.open("http://www.site.com");
Connector.open("datagram://127.0.0.1:8090");
Connector.open("file://fichero.txt");
Connector.open("comm://9600:18N);

Con dichas llamadas dejamos en manos de Connector la decisión de utilizar una u otra de las
clases privadas disponibles para establecer la conexión de forma transparente a nosotros. Si es
capaz de ello, el método devuelve una instancia que implementa Connection, la cual
recogeremos en una u otra conexión concreta mediante casting. Por ejemplo, para HTTP de la
forma:

HttpConnection conex = (HttpConnection)Connector.open("http://www.site.com");

Ésta será la forma de trabajar para la cual nos servimos de esta importante clase, tanto aquí
como heredada en MIDP. Sus métodos y constantes son:

• static int READ: constante que nos indicará la apertura de una conexión de lectura.

• static int READ_WRITE: constante que nos indicará la apertura de una conexión de
lectura y escritura.

• static int WRITE: constante que nos indicará la apertura de una conexión de escritura.

- 38 -
• static Connection open(String name): primero de los métodos estáticos con los que
obtener una instancia de conexión, en este caso, pasándole sólo una cadena. Esta
cadena representa el objetivo al que deseamos conectar y tendrá el siguiente esquema:

<protocolo>://<destino>;<parámetros>

Donde damos el protocolo o forma en que la conexión se desea establecer (http, socket,
datagrama, file, comm, etc.), el destino con el cual se quiere conectar (sitio Internet,
fichero, puerto serie, etc.) y, si son necesarios, se dan también los parámetros que
determinan la conexión, dados como pares de valores (param1=val1; param2=val2).

• static Connection open(String name, int mode): también ofrece una instancia de
conexión, dando en el primer parámetro una cadena de igual formato que el visto para el
método anterior. Además, ahora tenemos un segundo parámetro de entrada (mode) para
indicar con él, el modo de acceso a la conexión, dando para ello una de las tres
constantes ya estudiadas: READ, READ_WRITE o WRITE.

• static Connection open(String name, int mode, boolean timeouts): aunque es


análogo al open() anterior, ahora ofrece la posibilidad de determinar una espera tras la
cual se eleve una excepción InterruptedIOException. Ésta será lanzada si el tiempo que
tarda la conexión en quedar totalmente establecida supera el dado por el parámetro
timeouts.

• static DataInputStream openDataInputStream(String name): con éste y los tres


siguientes métodos crearemos flujos de entrada y salida al mismo tiempo que es creada
internamente la instancia de Connection deseada. En estos casos, la conexión queda
oculta, permitiéndonos trabajar directamente con los flujos. Es más, en cuanto el flujo se
devuelve, se invoca al método close() de la conexión utilizada de forma que queda en
manos del cierre del flujo la finalización con la vida de la conexión, como ya hablamos al
pasar por la interface Connection. Esto puede verse en el código fuente del método
openDataInputStream(): (en los demás se actúa análogamente).

- 39 -
public static DataInputStream openDataInputStream(String name) throws IOException {
InputConnection con = (InputConnection)Connector.open(name, Connector.READ);
try { return con.openDataInputStream(); }
finally { con.close(); }
}

• static DataOutputStream openDataOutputStream(String name): en este caso, el flujo


devuelto es un DataOutputStream en el que escribir.

• static InputStream openInputStream(String name): en este caso, el flujo devuelto es


un InputStream del que leer.

• static OutputStream openOutputStream(String name): en este caso, el flujo devuelto


es un OutputStream en el que escribir.

2.2.2.3. Excepciones

− ConnectionNotFoundException extends java.io. IOException

Dicha excepción será elevada para señalar que el objetivo a conectar no es encontrado o que el
protocolo requerido no está soportado.

2.2.3. MIDP 2.0

Seguidamente consideraremos los elementos que incorpora el perfil MIDP a los ya existentes dados por
CLDC para la comunicación del dispositivo.

Consultando la API de MIDP 2.0 podemos observar que están incorporados en ella todos los elementos
de J2SE y de la CLDC que acabamos de estudiar sin variación alguna, además de los que ella misma
aporta. Éstos aparecen en los paquetes MIDP java.io, javax.microedition.io y javax.microedition.pki:

Nota: en este apartado sólo veremos los elementos del segundo paquete, no obstante, debemos
mencionar del tercero que nos ofrecerá una sola interface Certificate, la cual será utilizada para
protocolos seguros.

- 40 -
2.2.3.1. Interfaces

Además de todas las ya estudiadas, aparecen las siguientes interfaces, extendiendo el Generic
Connection Framework de la CLDC visto.

Nota: sólo describiremos detalladamente la HttpConnection, en la cual está centrado este capítulo.

− UPDDatagramConnection extends DatagramConnection

Representa una conexión basada en datagramas donde la dirección a conectar es conocida. La


conexión se abrirá como siempre, usando un Connector donde a su método open() le pasamos
una URL de formato:

datagram://<host>:<port>

Donde el <host> será la dirección de la máquina a conectar y <port> el puerto por el que
escucha. Por último, en el punto donde se reciben los datos, deberá ser abierto a su vez un
datagrama en el que se omite el host.

Además de los métodos heredados de su padre, tendremos dos métodos para conocer la
dirección y puerto local de la conexión: String getLocalAddress() e int getLocalPort().

− ServerSocketConnection extends StreamConnectionNotifier

Con esta interface definimos un socket servidor que quedará a la espera de conexión de posibles
sockets clientes. Para obtener una instancia suya usaremos el método open() con su parámetro
name de la forma:
socket://:<port>

Como podemos apreciar, el host es omitido. Por ejemplo, Connector.open("socket://:79");


crearía un socket servidor de entrada que escuchará peticiones de sockets cliente por el puerto
79. Una vez establecida la conexión con el cliente por medio del método acceptAndOpen()
heredado de StreamConnectionNotifier, él nos devolverá un SocketConnection con el cual
leer y escribir la información a comunicar entre ambos sockets conectados.

- 41 -
Un extracto básico de código donde podemos ver el comportamiento descrito es el siguiente:

...
//Creación del socket servidor que quedará en espera:
ServerSocketConnection servCon = (ServerSocketConnection)Connector.open(url);
//Esperaríamos conexiones de cliente en un bucle infinito (se cortará al cerrar la aplicación servidora):
while(true){
//Esperamos a un Cliente. Una vez aceptada comunicación de alguno, se devuelve una conexión con él:
SocketConnection con = (SocketConnection) servCon.acceptAndOpen();
...
}

De no existir esta interface, ambos sockets al conectarse deberían establecer comunicación de


forma simultánea, produciendo un error en caso contrario. Este elemento pues nos facilita
enormemente la labor de conectar dos sockets.

− HttpConnection

A continuación, consideraremos la interface que nos proporciona una conexión http., en la cual
está centrado el capítulo presente.

HTTP es un protocolo de petición - respuesta en el que los parámetros de la petición deben ser
fijados antes de que ésta sea enviada. Esta conexión estará en todo momento en alguna de las
tres fases siguientes:

1. Establecimiento. En esta fase los parámetros necesarios son establecidos.


2. Conectado. La conexión ya está preparada y se comienza la conversación.
3. Cierre, estado final. Se da por concluida la comunicación.

La forma general para la URL a dar en el método Connector.open() será la siguiente en


HttpConnection (más adelante, estudiaremos la forma de trabajar con HTTP detenidamente):

http://<host>:<port>/ruta/recurso?Query#Ref

- 42 -
Las constantes que ofrece la interface son:

• static String GET. Indicador de tipo de petición GET. En estas peticiones los datos se
enviarán como parte de la URL demandada.

• static String HEAD. Indicador de tipo de petición HEAD. Similar a GET, aunque el
servidor sólo responde la línea de estado y cabeceras HTTP, sin el cuerpo de la
respuesta.

• static String POST. Indicador de tipo de petición POST. Aquí, el cuerpo de la petición se
envía como un bloque separado.

• static int HTTP_ACCEPTED ... static int HTTP_VERSION. Se tendrán 38 constantes


identificando los posibles códigos de estado que pueden aparecer en toda respuesta
HTTP del servidor: desde el 202 de HTTP_ACCEPTED informando de que la petición ha
sido aceptada pero aún no procesada, hasta el 505 de HTTP_VERSION que indica que la
versión del protocolo HTTP usado en la petición no está soportado por el servidor. En
general, estos códigos se agrupan en 5 tipos:

 1xx: Código de información.


 2xx: Código de éxito.
 3xx: Código de redirección.
 4xx: Código de error de Cliente.
 5xx: Código de error de Servidor.

Nota: recomendamos la consulta a la especificación MIDP 2.0 (ver Bibliografía) para ver
todos estos códigos en detalle. Tras las constantes, los 21 métodos que incorpora esta
interface son:

 long getDate(). Valor del campo de cabecera date, fecha de la respuesta. Todas
las fechas se dan como el número de milisegundos transcurridos desde el
01/01/1970 hasta el momento a indicar.

 long getExpiration(). Valor del campo de cabecera expires (fecha de caducidad


del recurso requerido). Devuelve 0 si no es conocido.

- 43 -
 String getFile(). Devuelve la porción de URL que lleva la ruta y el recurso
solicitado. Si no existe, devuelve null.

 String getHeaderField(int n). Devuelve el valor del n-ésimo campo de los que
forman la cabecera de la respuesta HTTP. Da null si el índice está fuera de rango.

 String getHeaderField(String name). Da el valor del campo de cabecera cuyo


nombre indicamos, o null si éste no se encuentra.

 long getHeaderFieldDate(String name, long def). Da el valor del campo de


cabecera indicado por su nombre, parseado como una fecha. Si no se encuentra,
se da el valor por defecto indicado en def.

 int getHeaderFieldInt(String name, int def). Da el valor del campo de cabecera


indicado por su nombre, parseado como un entero. Si no se encuentra, se da el
valor por defecto indicado en def.

 String getHeaderFieldKey(int n). Devuelve el nombre del campo de cabecera


que ocupa el lugar n pasado, o null si el índice está fuera de rango.

 String getHost(). Devuelve el identificador del host a alcanzar con la conexión; un


nombre de dominio o una dirección IP.

 long getLastModified(). Valor del campo de cabecera last-modified (fecha de la


última modificación del recurso requerido).

 int getPort(). Devuelve el puerto solicitado en la URL. Si no se ha indicado se


devuelve 80, que es el puerto por defecto para la comunicación HTTP.

 String getProtocol(). Nombre del protocolo usado en la conexión, por ejemplo,


HTTP o HTTPS.

 String getQuery(). Devuelve la porción de URL a conectar que lleva la Query,


definiéndose ésta como la porción de cadena tras el primer interrogante (?). En
ella irá el cuerpo de la petición en el caso de GET, dándose en forma de pares
parámetro=valor separados por ampersand (&). Si no existe, devuelve null.

- 44 -
 String getRef(). Devuelve la porción de URL a conectar que lleva el Ref,
definiéndose éste como la porción de cadena tras la primera almohadilla (#). Si no
existe, devuelve null.

 String getRequestMethod(). Devuelve el tipo de la petición a realizar, sea GET,


HEAD o POST.

 String getRequestProperty(String key). Da el valor de un campo de cabecera de


la petición, cuyo nombre pasamos como parámetro (por ejemplo Accept, User-
Agent, Referer, etc.). Devuelve null si no es conocido.

 int getResponseCode(). Informa del código de la línea de estado de la respuesta,


o null si éste no se puede distinguir. Por ejemplo, dada la línea HTTP/1.0 200 OK,
se devolvería 200.

 String getResponseMessage(). Informa de la descripción en la línea de estado


de la respuesta, o null si ésta no se puede distinguir. Por ejemplo, dada la línea
HTTP/1.0 200 OK, se devolvería OK.

 String getURL(). Cadena con la URL completa que se ha intentado alcanzar con
esta conexión.

 void setRequestMethod(String method). Establece el tipo de la petición a


realizar, sea GET,HEAD o POST. Por defecto el tipo es GET.

 void setRequestProperty(String key, String value). Inserta (o modifica si ya


existe) el valor de un campo de la cabecera de petición, cuyo nombre pasamos
como parámetro (por ejemplo Accept, User-Agent, Referer, etc.), con el valor
también pasado como parámetro.

− CommConnection extends StreamConnection

Con esta interface obtendremos una conexión a puerto serie lógico, no teniendo éste que
corresponder necesariamente a un RS-232 físico. Por ejemplo, los puertos IrDA IRCOMM
pueden ser configurados como puertos serie lógicos dentro del sistema operativo y así actuar
como tales.

- 45 -
La cadena que pasamos al método open() tiene el formato:

comm:<port identifier>[<optional parameters>]

Donde el identificador del puerto será (convencionalmente) COM# para los puertos RS-232 o IR#
para los puertos IrDA IRCOMM, siendo # el número asignado al puerto. Los parámetros
opcionales serán pares parámetro-valor separados por punto y coma (;).

Por ejemplo, Connector.open("comm:com0;baudrate=19200"); abriría una conexión con el


puerto serie "com0" de velocidad de transferencia de bits 19200 baudios. Si esta velocidad no es
soportada por la plataforma se usará un valor alternativo, el cual podremos consultar
posteriormente con el método int getBaudRate(). Éste, junto con void setBaudRate(int) son los
dos únicos métodos nuevos que aporta la interface.

− SocketConnection extends StreamConnection

Esta interface nos ofrece una conexión basada en sockets. Con ella, podremos comenzar una
conexión desde un socket cliente o, una vez establecida una ServerSocketConnection en un
socket servidor tras petición de un socket cliente, recoger en una instancia de
SocketConnection la conexión que devuelve el método acceptAndOpen() de aquella interface.

Para intentar una conexión vía socket desde nuestro MIDlet usaremos el Connector.open() con
una cadena de formato:

socket://<host>:<port>

Donde host es la máquina en el que espera el socket servidor y port el puerto por el que
escucha. Heredando de StreamConnection, será una conexión tanto de entrada como de
salida, donde si se cierra un canal, el otro podrá seguir usándose.

La comunicación por medio de sockets podrá ser tratada como un flujo tanto de entrada como de
salida donde, una vez establecida la conexión, se podrá leer del flujo que representa usando
InputStream y escribir en él por medio de OutputStream. Serán necesarios en aplicaciones
donde la pérdida de paquetes que sufrían los datagramas no sea permisible. Frente a esta
seguridad, perderemos ancho de banda en la comunicación debido al tamaño de las cabeceras
TCP.

- 46 -
Los sockets sólo definen el transporte de datos a bajo nivel. Esto hace que exista la necesidad
de definir un protocolo para comunicar ambos sistemas, dejando en manos del desarrollador la
definición del formato (en ambos puntos de la conexión) que deberá cumplir la información que
se intercambia. En aquellos casos en los que la comunicación necesite cumplir ciertos
estándares o no haya control sobre alguno de los dos sistemas a conectar, el uso de HTTP es
recomendado. Por supuesto, este protocolo será mucho más lento que sockets o datagramas,
pero es algo universal.

− HttpsConnection extends HttpConnection

Versión segura de la conexión HTTP, donde existirá un proceso de autenticación entre cliente y
servidor previo al intercambio de información objeto de la comunicación. Estas conexiones se
deben implementar bajo una especificación como TLS, WTLS, etc., siendo la más usual la SSL.
La URL del método Connector.open() se utilizará en HttpsConnection, así:

https://<host>:<port>/ruta/recurso?Query#Ref

Incorporan dos nuevos métodos además de los heredados de HttpConnection: el método int
getPort(), que nos permitirá conocer el puerto por el que se nos ha atendido, y el SecurityInfo
getSecurityInfo(), que nos dará información asociada con la comunicación HTTPS una vez
establecida ésta correctamente.

− SecureConnection extends SocketConnection

Versión segura de la conexión por sockets que está implementada usualmente con el protocolo
SSL (Secure Socket Layer). Dicho protocolo encripta los datos transportados en la
comunicación, proporcionando autenticación tanto a un lado como a otro de ésta. Para abrirla se
usará una cadena de la forma:

ssl://<host>:<port>

Como en el caso anterior, un método SecurityInfo getSecurityInfo() nos da información sobre


la comunicación establecida.

- 47 -
− SecurityInfo

Interface que permite acceder a la información que marca la conexión segura, tanto para
HttpsConnection como para SecureConnection donde se ofrece, por ejemplo, el protocolo
utilizado, el certificado que autentica la comunicación, etc.

Los datos sobre el certificado se proporcionan en una instancia de la clase Certificate, única
interface del paquete javax.microedition.pki.

2.2.3.2. Clases

En este apartado tendemos la imprescindible clase Connector, ya vista en CLDC, junto con otra que
incorpora el perfil MIDP 2.0:

− PushRegistry extends Object

El Push Registry es un componente del AMS (Application Management Software) que permite
que los MIDlets puedan ser lanzados automáticamente sin necesidad de ser inicializados por el
usuario.

Este concepto no modifica el ciclo de vida del MIDlet, simplemente introduce dos nuevas vías
por las que puede ser activado: activación causada por conexiones de red entrantes y
activación causada por temporizadores.

- 48 -
Con esta clase, por tanto, tratamos las conexiones de entrada que puede recibir el dispositivo.
De ellas se informará al AMS dinámica (lo comentaremos más adelante) o estáticamente. Esto
último se consigue por medio del archivo descriptor de la aplicación (JAD) asociado a cada suite,
donde aparecerá por cada conexión de entrada a registrar en el AMS para esta suite, una línea
textual de formato:

MIDlet-Push-<n>: <ConnectionURL>, <MIDletClassName>, <AllowedSender>

Donde MIDlet-Push-<n> actúa como identificador de la entrada, <ConnectionURL> da la


definición de la conexión de entrada (cadena dada al usar Connector.open());
<MIDletClassName> el nombre del MIDlet que espera la conexión y al cual ésta queda asociada
(de entre todos los de la suite propietaria del JAD); y <AllowedSender> un filtro por el cual
discriminar quién puede hacer una petición a esta conexión. Por ejemplo, las siguientes líneas
registrarían que el MIDlet "SampleChat" podrá recibir peticiones a las conexiones definidas por
parte de cualquier cliente que lo desee:

MIDlet-Push-1: socket://:79, com.sun.example.SampleChat, *


MIDlet-Push-2: datagram://:50000, com.sun.example.SampleChat, *

De esta forma, el AMS podrá lanzar el MIDlet (invocando su método MIDlet.startApp()) asociado
a la conexión de entrada si éste no está corriendo cuando la petición llega. Si el MIDlet sí está en
marcha, será él el encargado de ocuparse de sus conexiones de entrada. Como seguidamente
veremos, además de con los ficheros descriptores, será posible informar dinámicamente al AMS
de las conexiones de entrada que deseemos esperar.

Aunque el Push Registry es parte del AMS y está gestionado por él, nosotros podremos llevar a
cabo actuaciones sobre este componente. Los métodos de los que disponemos para ello,
ofrecidos por la clase PushRegistry, son:

• static String getFilter(String connection). Obtiene el filtro definido en el descriptor para


esta conexión. Dicha conexión será proporcionada con una típica cadena protocolo-host-
puerto.

• static String getMIDlet(String connection). Obtiene el nombre del MIDlet asociado a


esta conexión.

- 49 -
• static String[] listConnections(boolean available). Devuelve un array con todas las
conexiones de entrada registradas para la suite actual (available a false), o sólo las que
tengan en este momento la entrada disponible (available a true).

• static long registerAlarm(String midlet, long time). Define un instante de tiempo en el


cual el MIDlet parámetro será lanzado. Devuelve 0 si es la primera vez que se define un
temporizador para este MIDlet o el existente si ya ha sido definido previamente. Ésta es la
segunda nueva forma de activación que comentábamos, además de la provocada por
conexiones de red entrantes.

• static void registerConnection(String connection, String midlet, String filter). De


forma análoga a como registramos estáticamente la conexión de entrada por medio del
archivo JAD descriptor, también podremos hacerlo dinámicamente por medio de este
método. Así, en ejecución informamos al AMS de las conexiones de entrada que admite
el MIDlet dado.

• static boolean unregisterConnection(String connection). Elimina una conexión de


entrada que se haya registrado dinámicamente.

2.3. COMUNICACIÓN HTTP

En este apartado, estudiaremos en profundidad la capacidad de comunicación HTTP de nuestro


dispositivo MIDP.

El protocolo HTTP 1.1 se basa en el paradigma cliente-servidor, donde un cliente establece una conexión
con un servidor y le envía una petición formada por: una línea inicial donde irá el tipo de petición (GET,
HEAD o POST), la URL del recurso a obtener y la versión del protocolo (HTTP/1.1), seguida de sucesivas
líneas con los campos de cabecera de petición que se consideren necesarios (Accept: text/plain; Accept:
tetx/html; User-Agent: <versiónNavegador>; etc.) y, tras ellos, el posible cuerpo del mensaje de petición
(en el caso de GET irá en la propia URL).

Por su parte, el servidor responde con un mensaje formado por una línea de estado (versión del
protocolo, código de respuesta y descripción de éste, por ejemplo HTTP/1.0 200 OK), seguida de los
campos de cabecera de respuesta necesarios (Date: Monday, 6-Feb-06 17:00:00; Content-type:

- 50 -
text/xml; Content-length: 245, etc.) y, tras ellos, el cuerpo del mensaje de respuesta. Después de esto, el
servidor queda a la espera de una nueva petición de este o cualquier otro cliente.

Todos estos elementos se enviarán en forma de texto plano de un lado a otro de la comunicación,
necesitando por debajo un protocolo de bajo nivel para su transporte, como es el TCP.

Veamos en el siguiente apartado cómo llevamos a cabo esta comunicación en J2ME, donde la mayoría
de los detalles de implementación de este protocolo quedarán ocultos a los ojos del programador,
haciendo de nuestro objetivo de conectividad HTTP una sencilla tarea.

2.3.1. Fases en la comunicación HTTP

HTTP es un protocolo de petición (cliente) - respuesta (servidor), donde los parámetros de la petición
deben ser fijados antes que ésta sea enviada. La comunicación que nos permite hacer una petición y
recibir una respuesta pasa por tres fases o estados, los cuales ya enumeramos anteriormente y ahora
detallamos:

− ESTABLECIMIENTO

En esta fase, los parámetros de la petición son establecidos y con ellos se intenta la conexión
con el servidor. De los vistos para HttpConnection, existen dos métodos que sólo pueden ser
invocados en este estado y con los cuales informamos al servidor de las características de
nuestra petición, como son los ya estudiados setRequestMethod() y setRequestProperty(). El
primero determina un valor de entre GET, HEAD o POST, y con el segundo se podrán añadir
tantos campos de cabecera de petición como deseemos, de entre los más de 40 existentes.
Algunos de ellos son:

• User-Agent. Tipo de cliente que realiza la petición.


• Content-Language. País e idioma del cliente.
• Accept. Formatos de respuesta que acepta el cliente.

- 51 -
• Expires. Tiempo máximo que el cliente espera la respuesta del servidor.
• Content-Length. Longitud en bytes de la petición.

Instanciamos pues la conexión y la preparamos para ser lanzada, lo cual se codificará de una
forma u otra. Un ejemplo básico de petición GET sería:

//Creamos la conexión dando al método Connector.open() una URL donde, en el caso de GET, los datos
//a enviar en el mensaje forman parte de ella (Query). En este caso, el volumen del cuerpo está limitado.
String URL = "http://www.site.com/servlet?accion=inicio&digo=hola";
HttpConnection con = (HttpConnection)Connector.open(URL);

//Establecemos parámetros para la conversación:


con.setRequestMethod(HttpConnection.GET);
con.setRequestProperty("User-Agent", "Profile/MIDP-2.0 Configuration/CLDC-1.1");

En el caso de una petición POST, los datos de la petición (cuerpo) se enviarán en un flujo aparte.
Veamos cómo en el siguiente ejemplo:

//Creamos la conexión dando al método Connector.open() una URL sin dato alguno:
String URL = "http://www.site.com/servlet";
HttpConnection con = (HttpConnection)Connector.open(URL);

//Establecemos parámetros para la conversación:


con.setRequestMethod(HttpConnection.POST);
con.setRequestProperty("User-Agent", "Profile/MIDP-2.0 Configuration/CLDC-1.1");

//En el tipo POST debemos enviar los datos en un flujo separado:


OutputStream cuerpo = con.openOutputStream();
cuerpo.write("accion=inicio".getBytes());
cuerpo.write("&digo=hola".getBytes());
cuerpo.flush();

- 52 -
− CONECTADO

Con la fase anterior la conexión ya ha sido preparada y lista para ser lanzada, tras lo cual se
comenzará la conversación con el servidor. Para ello, debemos provocar el paso del estado
anterior al presente lanzando la petición preparada, lo cual se consigue invocando alguno de
los siguientes métodos:

openInputStream(), openDataInputStream(), getLength(), getType(), getEncoding(),


getHeaderField(), getResponseCode(), getResponseMessage(), getHeaderFieldInt(),
getHeaderFieldDate(), getExpiration(), getDate getLastModified(), getHeaderField(),
getHeaderFieldKey().

Dado que todos ellos necesitan la respuesta del servidor para devolver su resultado, MIDP
espera hasta el momento en que se invoque alguno de ellos para lanzar la petición. En ese
momento, estaremos ya en el estado CONECTADO disponiéndonos a esperar la respuesta del
servidor para una vez recibida tratarla convenientemente. En el ejemplo del apartado siguiente
veremos cómo recogemos lo que el servidor nos responde, además de comprobar
fehacientemente el paso de un estado a otro de la conexión.

− CIERRE

Estado final donde se da por concluida la comunicación. Para ello, simplemente lanzamos desde
nuestro MIDlet cliente el método close() heredado de Connection, con él cual cortamos la
comunicación con el servidor, quedando éste a la espera de nuevas conexiones (aunque esto no
nos atañe a nosotros como cliente). Como ya vimos, si aún no hemos cerrado los flujos utilizados
durante la conexión, permanecerá abierta hasta el cierre de estos.

2.4. CONVERSACIÓN MIDLET - SERVLET

Todo lo explicado hasta ahora y algunas cosas más las pondremos en práctica ahora en el siguiente
ejemplo. En él codificamos una breve pero intensa conversación entre un MIDlet J2ME y un servlet J2EE,
los cuales se comunicarán vía HTTP. El código está profusamente comentado para que su compresión
sea completa.

- 53 -
HTTPEjemploMIDlet.java

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

//Clase del MIDlet: Implementamos CommandListener al definir un escuchador de comandos


//los cuales permitirán al usuario decidir cuándo preguntar al servidor y cuándo salir+++++++++++++++++++++++++
public class HTTPEjemploMIDlet extends MIDlet implements CommandListener{
private Display pantalla;
private Command cerrar;
private Command preguntar;
private Form formulario;
private String respuesta;
private StringItem campoPregunta;
private StringItem campoRespuesta;
private Comunicador com;
private String[] preguntas = {"HolaServlet!",
"pepe",
"QueHacesHoy?",
"AdiosServlet,Encantado"};
private int numPregunta;

//Constructor del MIDlet+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++


public HTTPEjemploMIDlet()
{
//Capturamos el Display de pantalla asociado al MIDlet
pantalla = Display.getDisplay(this);

//Instanciamos nuestro objeto Comunicador, el cual se encargará de conectarnos con el servlet


com = new Comunicador(this);

//Creamos un formulario y un componente de texto de alto nivel, donde mostrar las respuestas del servlet
formulario = new Form("Servlet");
campoPregunta = new StringItem("PREGUNTA: ","");
campoRespuesta = new StringItem("RESPUESTA: ","");

//Creamos los comandos de la aplicación y los asociamos al formulario de alto nivel

- 54 -
cerrar = new Command("Cerrar",Command.EXIT, 1);
preguntar = new Command("Preguntar",Command.OK, 1);
formulario.addCommand(cerrar);
formulario.addCommand(preguntar);
formulario.setCommandListener(this);

//Cadena donde mostramos el cuerpo de la respuesta del servidor, vacía inicialmente


respuesta = "";
numPregunta = 0;
}

//Métodos del ciclo de vida del MIDlet++++++++++++++++++++++++++++++++++++++++++++++++++


public void startApp(){
pantalla.setCurrent(formulario);
}
public void pauseApp(){
}
public void destroyApp(boolean unconditional){
notifyDestroyed();
}

//Manejador de eventos+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
public void commandAction(Command c, Displayable d){
//Si el comando seleccionado por el usuario fue cerrar, salimos de la aplicación
if (c == cerrar){
destroyApp(false);
}
//Si el comando seleccionado por el usuario fue "Preguntar", llamamos al método de Comunicador encargado.
//Cada vez que pulsamos pues conectamos con el servidor: UNA PREGUNTA => UNA CONEXIÓN
else{
com.preguntar(preguntas[numPregunta]);
//Preparamos la siguiente pregunta para la siguiente pulsación sobre el comando Preguntar.
//Si ya hemos hecho todas las preguntas, volvemos a hacer la primera:
if(numPregunta == preguntas.length - 1)
numPregunta = 0;
else
numPregunta++;
}
}

- 55 -
//Presenta en pantalla la pregunta emitida y la respuesta devuelta por el servidor++++++++++++++++++++++++++
public void setRespuesta(String respuesta){
//Coloco los textos en sus campos. El de la respuesta llega desde la invocación en
//Comunicador.procesarRespuesta(); el de la pregunta lo tengo en el propio MIDlet, pero cuidando que
//numPregunta ya lo hemos avanzado para esperar la siguiente pulsación sobre "Preguntar"
if(numPregunta == 0)
campoPregunta.setText(preguntas[preguntas.length - 1]);
else
campoPregunta.setText(preguntas[numPregunta-1]);
campoRespuesta.setText(respuesta);

//Limpiamos el formulario
formulario.deleteAll();

//Añadimos los elementos de texto deseados


formulario.append(campoPregunta);
formulario.append(campoRespuesta);
}

}//fin del MIDlet

Comunicador.java

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

//Clase que llevará toda la carga de la conexión con el servlet+++++++++++++++++++++++++++++++++++++++++


public class Comunicador implements Runnable{
private Thread t;
private String respuesta;
private String pregunta;
private HttpConnection con;
private HTTPEjemploMIDlet midlet;
private String urlBase;

//Constructor de la clase++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
public Comunicador(HTTPEjemploMIDlet m) {
respuesta = null;

- 56 -
midlet = m;
//En las sucesivas conexiones con el servidor (una por pregunta) usamos siempre la
//misma url; variaremos sólo su componente Query (parámetros tras '?': USAMOS GET)
urlBase = "http://127.0.0.1:8080/escuchadorDeJ2ME/HTTPEjemploServlet";
}

//Creamos la conexión+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
public void preguntar(String pregunta){
//Modificamos el atributo pregunta que usará run():
this.pregunta = pregunta;

//MIDP 2.0 exige llevar la comunicación en red en un hilo diferente al de la ejecución normal de la aplicación. Con
//él conectamos al servidor, ya que este proceso puede ser costoso. Si no se hace así, se puede provocar un
//bloqueo en la pantalla del dispositivo mientras se consigue la respuesta del servlet.
t = new Thread(this);

//Comenzamos la ejecución del hilo; start() llamará al run() definido abajo


t.start();
}

//Método de ejecución del hilo dedicado a la conexión++++++++++++++++++++++++++++++++++++++++++++++


public void run(){
try{
//Definimos la conexión a establecer
con = (HttpConnection)Connector.open(this.urlBase + "?frase=" + this.pregunta);

//Preparamos la petición para al servidor, estableciendo método y cabeceras


con.setRequestMethod(HttpConnection.GET);
con.setRequestProperty("Content-Language","es-ES");
con.setRequestProperty("User-Agent","Profile/MIDP-2.0 Configuration/CLDC-1.0");
con.setRequestProperty("Connection","close");

for(int i = 0; i < 100; i++) System.out.println("+++++ ESTADO ESTABLECIMIENTO +++++");

//El siguiente método provoca el paso del estado de ESTABLECIMIENTO al estado CONECTADO.
//Con el bucle anterior forzamos una breve pausa mientras la cual podemos comprobar en la consola de
//Tomcat como aún no ha llegado petición alguna al servlet. Una vez que se salga del bucle y se ejecute la
//siguiente sentencia, comprobaremos en la consola de Tomcat cómo le llega la petición y lo que responde
//ante ella.

- 57 -
InputStream flujoRespuesta = con.openInputStream();

System.out.println("+++++ ESTADO CONECTADO +++++");

//Una vez conectados, recibimos la respuesta a leer del flujo de entrada que hemos obtenido
leerRespuesta(flujoRespuesta);

//Cerramos el flujo de entrada abierto y la conexión, dando paso al estado de CIERRE


flujoRespuesta.close(); con.close();
System.out.println("+++++ ESTADO CIERRE +++++");
}
catch (Exception e){
System.out.println("EXCEPCIÓN EN EL MIDLET: " + e.toString());
}
}

//Método para leer por medio de un flujo de entrada el cuerpo de la respuesta++++++++++++++++++++++++++++


public void leerRespuesta(InputStream flujoRespuesta) throws IOException {
if (con.getResponseCode() == HttpConnection.HTTP_OK){
System.out.println("OK! Respuesta del servidor en procesarRespuesta(): " + con.getResponseMessage());

//Heredado de ContentConnection, getLength nos da el valor de la cabecera content-length,


//la cual hemos añadido con el servlet convenientemente.
int lon = (int) con.getLength();

//Si se ha leido la cabecera correctamente, podemos instanciar un array de bytes para guardar lo leído (óptimo)
if (lon != -1){
byte datos[] = new byte[lon];
flujoRespuesta.read(datos,0,datos.length);
respuesta = new String(datos);
}
//Si la longitud no es conocida, debemos usar un flujo de escritura donde guardar lo leido
else{
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int ch;
while ((ch = flujoRespuesta.read()) != -1) baos.write(ch);
respuesta = new String(baos.toByteArray());
baos.close();
}

- 58 -
//Llamamos al método del MIDlet que se encargará de presentar la respuesta en la interface
midlet.setRespuesta(respuesta);
}
else{
System.out.println("NO OK! Respuesta del servidor en procesarRespuesta(): " + con.getResponseMessage());
}
}

}//fin de la clase Comunicador

HTTPEjemploServlet.java

package servlets;
import java.io.*;
import javax.servlet.http.*;

//Servlet que esperará peticiones del MIDlet++++++++++++++++++++++++++++++++++++++++++++++++++++++


//Este servlet se ofrece desde una aplicación web de nombre escuchadorDeJ2ME desplegada en un servidor
//Tomcat y mapeada en el web.xml como /HTTPEjemploServlet
public class HTTPEjemploServlet extends HttpServlet{

//Tanto el doGet como el doPost son derivados a este método++++++++++++++++++++++++++++++++++++++++


public void processRequest(HttpServletRequest req, HttpServletResponse res){
String[] respuestas = {"Hola MIDlet, cómo te va?",
"Aquí, respondiendo al que quiera preguntar",
"Adios querido MIDlet.",
"No te entiendo, pregunta otra cosa"};

try{
res.setContentType("text/plain");
//Recogemos el cuerpo de la petición del MIDlet
String fraseMIDlet = req.getParameter("frase");

//La preparamos para comparar con los datos de que dispone el servlet. En ejemplos
//más complejos, podrían venir estos de una BD en vez del array anterior
if(fraseMIDlet==null) fraseMIDlet = "";
else fraseMIDlet = fraseMIDlet.trim().toUpperCase();
System.out.println("LLEGA AL SERVLET LA PETICION: " + fraseMIDlet);

- 59 -
//Preparamos un flujo de salida en el que escribir y por él enviar la respuesta
PrintWriter out = res.getWriter();
int indiceRespuestas;
if (fraseMIDlet.indexOf("HOLA") != -1)
indiceRespuestas = 0;
else if(fraseMIDlet.indexOf("HACES") != -1)
indiceRespuestas = 1;
else if(fraseMIDlet.indexOf("ADIOS") != -1)
indiceRespuestas = 2;
else
indiceRespuestas = 3;

System.out.println("ENVIA EL SERVLET LA RESPUESTA: " + respuestas[indiceRespuestas]);

//Asignamos la cabecera content-length para que el MIDlet haga uso de ella


res.setContentLength(respuestas[indiceRespuestas].length());

//Escribimos en el flujo de salida y lo enviamos al MIDlet


out.println(respuestas[indiceRespuestas]);
out.close();
}
catch (Exception e)
{
System.out.println("EXCEPCION EN EL SERVLET:" + e.toString());
}

//Peticiones por GET y POST al servlet, le llegan por estos métodos++++++++++++++++++++++++++++++++++++


public void doGet(HttpServletRequest req, HttpServletResponse res) {
processRequest(req,res);
}
public void doPost(HttpServletRequest req, HttpServletResponse res) {
processRequest(req,res);
}

}//fin servlet

- 60 -
Como cliente de la comunicación hemos probado el MIDlet con el J2ME Wireless Toolkit y hemos
ofrecido el servlet por medio de un servidor Tomcat. Las líneas relativas a este servlet que aparecerán
en el fichero de configuración web.xml de la aplicación J2EE escuchadorDeJ2ME serían:

<!--Asociamos un nombre al servlet y declaramos la clase JAVA donde se encuentra codificado-->


<servlet>
<servlet-name>HTTPEjemploJ2ME</servlet-name>
<servlet-class>servlets.HTTPEjemploServlet</servlet-class>
</servlet>

<!--Por medio del nombre asociado damos un mapeo lógico para llegar al servlet. Con esa url-pattern se pedirá-->
<servlet-mapping>
<servlet-name>HTTPEjemploJ2ME</servlet-name>
<url-pattern>/HTTPEjemploServlet</url-pattern>
</servlet-mapping>

Las capturas de pantalla siguientes de la consola del J2ME WT, del emulador de este mismo y de la
consola de Tomcat corresponden a la ejecución de la última pregunta del ciclo. Recordemos que este
ciclo de preguntas se podrá repetir las veces que se desee.

- 61 -
- 62 -
2.5. OTRAS CUESTIONES SOBRE HTTP

Para finalizar el tema, comentaremos algunas circunstancias de interés con las que nos encontraremos al
utilizar el protocolo HTTP, que es conveniente saber manejar.

2.5.1. Redireccionamiento URL

Es circunstancia habitual en la navegación por Internet que tras pedir un determinado recurso, como
puede ser una página HTML, seamos redirigidos a otra URL diferente de la que habíamos solicitado
inicialmente (portales, páginas trasladadas, etc.). Esto debemos preveerlo en nuestros MIDlets, si es que
deseamos hacer una conexión robusta que no se pierda fácilmente y, por tanto, sea capaz de "perseguir"
automáticamente su objetivo a lo largo de todas las redirecciones a las que deseen enfrentarla.

La solución que ofrecemos es bastante sencilla. Extrayendo las ideas clave sería:

1. Intentamos la conexión con la URL que conocemos, como siempre con (HttpConnection)
Connector.open(url).

2. Comprobamos el código HTTP de respuesta ofrecido con


HttpConnection.getResponseCode(). Si éste ha sido un código que indica redireccionamiento
(constantes 3xx de la clase HttpConnection: HTTP_MOVED_PERM, HTTP_MOVED_TEMP,
HTTP_SEE_OTHER, HTTP_TEMP_REDIRECT, etc.) sabremos que nuestra conexión no va a
tener éxito a la primera: debemos redireccionar, pero ¿a dónde?.

3. La respuesta se nos envía a la vez que el código anterior, en el campo de cabecera Location.
Llamando a HttpConnection.getHeaderField("Location") tendremos esa URL donde se ha
movido al recurso y allí reintentaremos conectar.

4. La URL de Location puede haberse ofrecido localmente, por ejemplo "/nuevoDirectorio/servlet".


En este caso, hacia el cual nos llevará preguntar por if(urlLocation.startsWith("/"), debemos
componer la url de nuevo con algo similar a: nuevaUrl = "http://" + conex.getHost() + ":" +
Integer.toString(conex.getPort()) + urlLocation.

Así seguiríamos en bucle hasta que se nos ofrezca una respuesta HTTP de código distinto a los de
redireccionamiento indicados. Con esto, logramos que nuestro MIDlet consiga realizar su petición del
recurso deseado al servidor, aunque existan varios redireccionamientos entre la URL que nuestro MIDlet

- 63 -
conoce (donde cree inicialmente que está el recurso que desea) y la URL donde finalmente se ha
trasladado a ese recurso.

2.5.2. Uso de cookies

Una de las características clave de HTTP, y que aún no hemos comentado, es que se define como un
protocolo SIN ESTADO, esto es, no recuerda los parámetros de una llamada al realizar la siguiente. Por
este motivo, el servidor no tiene conocimiento de si un cliente le ha realizado varias peticiones seguidas ni
recuerda las características de éste de una a otra conexión, a no ser que se usen estrategias externas al
propio protocolo. Si consideramos las distintas conexiones que pueden producirse en una misma sesión
de trabajo del usuario, la necesidad de estas estrategias se hace mucho más evidente.

Dichas estrategias forman parte del seguimiento de sesiones. Para conseguir este seguimiento, cada
cliente debe informar al servidor de su identidad en cada una de las peticiones que le realice. Así, el
servidor podrá seguir la relación que mantiene con sus clientes creando una sesión de trabajo para cada
uno de ellos, a las cuales dará cuerpo almacenando en su máquina información relativa a cada cliente
concreto.

Esta información la podrá recuperar de una a otra conexión simplemente reconociendo quién es el cliente
que le reclama en cada momento. Con esto, en el ejemplo anterior podríamos variar el servlet para que
fuera capaz de responderle al MIDlet "Esto ya me lo dijiste antes", almacenando en algún lado el texto
"frase" de todas las peticiones que le llegan del cliente x. Sin esto, la potencia de las aplicaciones web se
vería mermada: pensemos en un servidor atendiendo a 10 clientes a la vez y que de una a otra petición
no sea capaz de recordar qué comunicó anteriormente con cada uno de ellos.

La forma en que el servidor reconoce a cada cliente concreto suele ser por medio de un identificador
único que el cliente envía adjunto a cada petición que realice. Este identificador será inicialmente
asignado por el servidor a cada cliente la primera vez que éste haga una petición y enviado por medio de
una cookie de sesión (breve cadena de información codificada) que viajará como campo de la cabecera
de respuesta. De él extraerá el cliente el valor del identificador de sesión, debiéndolo almacenar para así
poder reenviarlo al servidor en la cabecera de las posteriores peticiones que le haga. Ésta no será la
única cookie que pueden intercambiar cliente y servidor, aunque sí la imprescindible para el seguimiento
de sesiones. En servidores J2EE suele denominarse JSESSIONID a la cookie de identificación de sesión.

En aplicaciones web J2EE, donde la interface se le ofrece al usuario por medio de un navegador web
como Mozilla o Internet Explorer, el almacenamiento de cookies en la máquina cliente es transparente al

- 64 -
programador, quedando éste en manos del navegador web. En cada petición que el cliente haga al
servidor será el navegador el encargado de enviar las cookies necesarias y de recibir y almacenar las que
el servidor devuelva.

En J2ME no tendremos un navegador web que haga este trabajo por nosotros, aun así podremos llevar a
cabo este seguimiento de sesiones codificándolo adecuadamente ayudándonos del almacenamiento
persistente (RMS) disponible en el dispositivo, el cual estudiábamos en el primer tema.

El proceso a seguir para ello, además de la cookie de sesión, lo podrá ejecutar cualquier otra cookie.
Dicho proceso lo podemos resumir de la siguiente manera:

1. Declaramos como atributo del MIDlet un RecordStore donde almacenar la cookie de sesión que
nos enviará el servidor la primera vez que conectemos a él y la cual mantendremos en el
almacenamiento mientras dure nuestra sesión de trabajo. También como atributo tendremos el
valor de nuestra cookie.

private String cookieSesionId = null;


private RecordStore cookieRs = null;

2. En el startApp() del MIDlet abrimos el RecordStore y leemos la cookie almacenada (si existe):

cookieRs = RecordStore.openRecordStore("AlmacenCookie", true);


byte[] cad = new byte[100];
cookieRs.getRecord(1, cad, 0);
cookieSesionId = new String(cad);

3. Tras esto, cuando el cliente decida hacer una petición al servlet, lo primero que hacemos es
preparar una cabecera de petición con la cookie almacenada, si ya la tenemos. Así
informamos al servidor de nuestra identidad:

HttpConnection con = (HttpConnection)Connector.open(url);


if (cookieSesionId != null)
con.setRequestProperty("cookie", cookieSesionId);

4. Una vez establecida la conexión, leemos la respuesta del servidor. Si nos ha enviado una
cabecera donde va una cookie de sesión significará que no nos conoce (no le habremos

- 65 -
enviado nosotros previamente ninguna, ya que, aún no la tendríamos almacenada); debemos
recoger nuestra nueva identificación y almacenarla:

String cookieConocida = con.getHeaderField("set-cookie");


if(cookieConocida != null){
byte[] cad = cookieConocida.getBytes();
cookieRs.addRecord(cad, 0, cad.length);
cookieSesionId = cookieConocida;
}

5. Por su parte, de una u otra forma el servlet debe encargarse de observar las cabeceras de la
petición donde van las cookies, usando la apropiada que reciba para reconocer al cliente y
tratarlo convenientemente. Si no le llega la cookie en la petición deberá asignarle a este nuevo
cliente un identificador y enviárselo en la cookie de sesión que espera el MIDlet.

- 66 -
RECUERDE

− La característica más importante en un dispositivo móvil MIDP es su permanente posibilidad


de conexión con el exterior, la cual nos permitirá establecer una conexión HTTP con un servidor
J2EE (entre otras muchas).

− Toda conexión en MIDP hereda de la interface Connection. Dicha interface representa la


conexión más abstracta posible. Para obtener una instancia de ella o de cualquiera de sus hijas
debemos usar alguno de los métodos open() de la clase factoría Connector.

− Los tipos de conexión actualmente soportados por MIDP 2.0 son el HTTP, su versión segura
HTTPS, las conexiones basadas en SOCKETS (de espera, en servidor, seguros, comunes), la
realizada por medio de DATAGRAMAS y la comunicación con puertos serie COMM.

− Una conexión HTTP siempre se encontrará en alguno de los tres estados siguientes:

• Establecimiento, donde se fijan los parámetros de la comunicación con el servidor.


• Conectado, donde se intercambia la información deseada entre el cliente y el servidor.
• Cierre, donde finaliza la conexión.

− Podremos mejorar las aplicaciones que requieran comunicación HTTP con múltiples
consideraciones. Dos de ellas son: cuidar un redireccionamiento adecuado de la URL a buscar
y otra, el uso de cookies utilizando el RMS del dispositivo para permitir que el servidor a
conectar mantenga un seguimiento adecuado de nuestra sesión.

- 67 -
El API
Tema 3
Multimedia

3.1. INTRODUCCIÓN...........................................................................................................................71
3.1.1. Necesidad de sonido........................................................................................................71
3.1.2. Potenciando MIDP con MMA...........................................................................................72
3.2. ELEMENTOS DEL API IMPLICADOS..........................................................................................73
3.2.1. MIDP 2.0 ............................................................................................................................74
3.2.1.1. javax.microedition.media .................................................................................. 75
3.2.1.2. javax.microedition.media.control ..................................................................... 84
3.2.2. MMA 1.1.............................................................................................................................87
3.2.2.1. javax.microedition.media .................................................................................. 88
3.2.2.2. javax.microedition.media.control ..................................................................... 91
3.3. EJEMPLO MULTIMEDIA ..............................................................................................................95

- 69 -
3.1. INTRODUCCIÓN

En el presente tema estudiaremos el tratamiento en J2ME de elementos multimedia como pueden ser
músicas, sonidos y vídeo. Con ellos, apoyaremos ciertas aplicaciones en las que la gráfica y su
interacción con el usuario es muy importante (por ejemplo en juegos), sin olvidar los casos en los que
contrariamente la gráfica esté al servicio de la multimedia que se desee emitir (reproductores, gestores
multimedia, etc.). Además, este tema nos será de gran utilidad en el siguiente de Juegos en J2ME, en el
cual ya nos veremos capacitados para dotar de sonido a los juegos que deseemos desarrollar.

3.1.1. Necesidad de sonido

En aplicaciones, como pueden ser los juegos interactivos, donde la apariencia visual y su respuesta se
consideran los elementos más importantes, el sonido también juega un papel destacado. Los usuarios
desean ver y también escuchar lo que está ocurriendo en la pantalla del dispositivo, para lo cual
debemos conocer las herramientas disponibles para crear ese contexto sonoro con el que facilitarle al
usuario su inmersión en la aplicación y hacerle creer que está en un universo distinto.

En el caso de los juegos usualmente se utilizará una música ambiental para mantener la tensión,
provocar sensaciones en el jugador o simplemente marcar la diferencia entre una y otra situación en su
recorrido por el juego. Por otro lado, también podremos valernos de la emisión de ciertos sonidos
puntuales y vibraciones del dispositivo con los que avisar al usuario de ciertos eventos o remarcar
alguna acción importante que se haya producido. La emisión de un vídeo de presentación para cada fase
del juego, por ejemplo, también nos será de gran ayuda para introducir al usuario en el objetivo del juego
y marcarle el rol que representa dentro de él.

Y no sólo los juegos lúdicos tendrán esta necesidad sonora; existirán múltiples aplicaciones donde el
sonido sea imprescindible, como pueden ser reproductores de formatos de audio y/o vídeo, gestores de
recursos multimedia, etc. Además de todo ello, numerosas aplicaciones de propósito más general
posiblemente se vean enriquecidas al incluir en ellas ciertos efectos sonoros indicativos de eventos,
animaciones de ayuda, etc.

En definitiva, hemos podido comprobar, de forma general, la importancia que tiene el estudio de los
elementos disponibles en J2ME para llevar a cabo el enriquecimiento multimedia de nuestras
aplicaciones.

- 71 -
Las grandes aportaciones que introdujo la especificación 2.0 del perfil MIDP versaban sobre temas de
seguridad, comunicación en red y, sobre todo, sobre una API para el tratamiento multimedia y elementos
para facilitar la producción de juegos interactivos. Con esto, se paliaba el defecto de MIDP 1.0 de ser
relativamente "mudo", salvo por su capacidad de emitir sonidos predefinidos ante ciertos eventos, gracias
al escaso método playSound() de la clase javax.microedition.lcdui.AlertType.

Frente a esto, MIDP 2.0 introdujo para el tratamiento multimedia dos nuevos paquetes en el perfil:

− javax.microedition.media. Define elementos para acceder y reproducir los diferentes formatos


de audio y vídeo soportados.

− javax.microedition.media.control. Con él se dispone de los elementos para controlar la


funcionalidad asociada a cada formato.

Con estas incorporaciones se dotó al programador MIDP 2.0 de la capacidad, antes inexistente, de incluir
de forma potente contenido multimedia en sus aplicaciones. Y aún faltaba algo por llegar: el paquete
opcional MMA (Mobile Media API), recogido en la especificación del JSR (Java Specification Request)
número 135. Veremos detenidamente cada elemento de esta API en el punto 3.2 del tema.

3.1.2. Potenciando MIDP con MMA

A pesar de que MIDP nos ofrece una API lo suficientemente potente para ofrecer al usuario elementos
multimedia de sonido asociados a la interface que debe utilizar, aún podía mejorarse. Con este objetivo
nace en su día el paquete opcional MMA de la mano de NOKIA, pensado tanto para la configuración CDC
como para la CLDC, en la que centramos el presente curso.

Un paquete opcional en J2ME está a nivel de los perfiles, sin encontrarse asociado a ninguno de ellos ni
a ninguna configuración concreta. Son conjuntos de APIs que proporcionan soporte para características
adicionales que se consideran aún no abarcadas apropiadamente por la configuración o perfil que
debería poseerlas.

Por ejemplo, el soporte para tratamiento gráfico 3D en J2ME está definido en un paquete opcional (JSR
184, Mobile 3D Graphics API for J2ME). Este paquete no puede ser incluido directamente en el perfil
MIDP ya que, esto obligaría a que todo dispositivo móvil que deseara preciarse de cumplir el perfil MIDP,
deba estar preparado para tratar este tipo de gráficos, y esto quizás no se tiene hoy día, Estaríamos
limitando el soporte del perfil MIDP a un conjunto restringido de dispositivos, ya que, ninguna de las

- 72 -
capacidades de un perfil puede ser opcional. Si un dispositivo cumple con un perfil, debe hacerlo con el
perfil completo.

Teóricamente ésta es la excusa, aunque la lentitud de aprobación de un paquete opcional es la realidad


de que aún no se vean incluidos en la plataforma J2ME paquetes que hoy día son soportados por la
inmensa mayoría de los dispositivos móviles que salen al mercado, como es el caso del API MMA.

En el mundo JAVA, cada carencia que se observa es introducida de forma oficial por medio de una
propuesta JSR. Cuando una persona o entidad cree que es necesaria la inclusión de nuevas capacidades
en alguna de las plataformas JAVA existentes, ya sea J2EE, J2SE o alguna configuración o perfil J2ME,
debe presentar una propuesta JSR (formada por una especificación, un test de compatibilidad y una
implementación de referencia) para su revisión por el ente apropiado. Una vez aprobada, cosa que
ocurrirá tras un proceso lento y complejo en el cual no entraremos, pasará a formar parte de una nueva
versión de la plataforma para la que haya sido presentada.

En este tema estudiaremos además de los elementos propios de MIDP 2.0, los que nos ofrece la API
MMA, ya que, nos permitirá trabajar con el contenido multimedia a ofrecer en nuestros dispositivos MIDP
de forma más potente y, aunque no forme parte aún de ningún perfil estándar J2ME, estará soportado
actualmente por la mayoría de los dispositivos móviles que puedan llegar a ejecutar nuestras
aplicaciones.

El API MMA engloba, entre sus elementos, todos aquéllos que MIDP 2.0 ofrece relativos al tratamiento
multimedia. Aunque nos centraremos en los ofrecidos por MIDP 2.0, pasaremos por todos en el siguiente
apartado, centrándonos en los más significativos y explicando la forma de trabajar con estas APIs para
gestionar el contenido multimedia deseado.

3.2. ELEMENTOS DEL API IMPLICADOS

En este apartado consideraremos los elementos ofertados por MIDP 2.0 y por la extensión MMA 1.1,
la cual podemos suponerla lo suficientemente extendida como para considerar totalmente válido el uso de
las herramientas que nos ofrece.

El J2ME Wireless ToolKit 2.2 (WTK 2.2), el cual usamos en este curso para producir nuestras
aplicaciones J2ME, también dará soporte para esta JSR 135. Por este motivo, no tendremos problema

- 73 -
para utilizar localmente los elementos que aquí veamos, ni probar los ejemplos que desarrollaremos más
adelante.

Antes de comenzar, comentaremos de forma general cuál será el proceso. Para presentar un contenido
multimedia de audio o vídeo (en adelante, simplemente "media"), del cual se dispondrá localmente (en el
RMS o en el directorio de recursos del propio JAR), o por conexión en red con otra máquina que lo
ofrezca, tendremos que obtener una instancia de un reproductor Player, la cual nos la ofrecerá la clase
factoría Manager. Este Player hereda de Controllable, un elemento que aglutina un conjunto de
controles, cada uno definido por una hija de la interface Control. Por ello, el Player podrá configurarse
gracias a su grupo de elementos Control asociados, cada uno definiendo ciertas características de la
reproducción.

3.2.1. MIDP 2.0

Una vez asimilados los conceptos principales del proceso anterior, pasaremos a ver en profundidad los
elementos implicados. Como ya comentamos, los dos paquetes relacionados con el tratamiento
multimedia en el perfil MIDP 2.0 son los siguientes:

− javax.microedition.media
− javax.microedition.media.control

- 74 -
3.2.1.1. javax.microedition.media

− INTERFACES

• Control

Heredando de ella en MIDP 2.0 sólo las interfaces VolumeControl y ToneControl, que más
adelante estudiaremos, en MMA 1.1 veremos que heredarán de ella algunas más, todas dando
capacidad de ejercer control sobre ciertas funcionalidades de un objeto Player. Obtendremos del
Player el Control deseado (Controllable.getControl()) y, al modificarlo, estaremos definiendo
nuevas características en la reproducción del Player del cual hemos obtenido ese Control.

Esta interface no tiene métodos; nos servirá simplemente para abstraer todos los controles (de
MIDP o MMA) que se definan para el elemento Player. Así, todos ellos serán un Control en
último término.

• Controllable

Asumiremos a partir de ahora que usamos los métodos de Controllable asociados siempre al
Player que hereda de ella, así podremos acceder a los controles definidos para un objeto Player.

Los métodos que ofrece Controllable (y por herencia, Player) son:

 Control getControl(String controlType): método que nos devuelve la instancia de


Control asociada al Player, sea la hija Control que sea, pasándole el nombre de la clase
que identifica al control.

El nombre de la clase se debe dar con su ruta de paquetes completa, si no es así se


asume la ruta javax.microedition.media.control.

Por otra parte, si existe más de una instancia de la clase indicada, sólo se devolverá
una de ellas. Para obtenerlas todas, deberá usarse el método getControls() y buscar
las deseadas de entre los controles que devuelve, por ejemplo así:

Controllable controllable; //En la práctica se usará un Player hijo


Control controles[];

- 75 -
Vector controlesVolumen = new Vector();
controles = controllable.getControls();
for (int i = 0; i < controles.length; i++)
if (controles [i] instanceof javax.microedition.media.control.VolumeControl)
controlesVolumen.addElement(controles [i]);

 Control[] getControls(). Como hemos visto, devuelve un array con todos los controles
asociados al Player actual, devolviendo un array vacío si no tiene ninguno. Entre los
controles devueltos no existirán duplicados y el contenido de este array no variará en
ejecución.

• Player extends Controllable

Esta interface hereda de la anterior y representa un reproductor con el que presentar un media
determinado. Al igual que ocurría en el capítulo anterior con las conexiones y la clase factoría
Connector, para instanciar un Player recurriremos a la clase factoría Manager y a sus métodos
createPlayer() que ya estudiaremos.

Las constantes disponibles en Player son:

 static int CLOSED. Representa el estado cerrado del Player. Valor 0.

 static int PREFETCHED. Estado en el que ha terminado de adquirir los recursos del
dispositivo que necesita para comenzar la reproducción del media. Valor 300.

 static int REALIZED. Estado en el cual ha adquirido el media a reproducir pero no los
recursos del dispositivo (salida de audio, driver de vídeo, etc.) que necesita para
hacerlo. Valor 200.

 static int STARTED. Estado en el que la reproducción ya ha comenzado. Valor 400.

 static long TIME_UNKNOWN. Para indicar que el tiempo solicitado es desconocido.


Valor -1.

 static int UNREALIZED. Estado en el que aún no ha adquirido ni la información inicial


ni los recursos. Valor 100.

- 76 -
Excepto TIME_UNKNOWN, el resto de constantes representan cada uno de los estados en los
que puede encontrarse un objeto Player durante su ciclo de vida, el cual está orientado a
capacitarnos para separar programáticamente operaciones sobre el Player que podrán ser muy
costosas.

El ciclo de vida de un reproductor Player es el siguiente:

 Comienza al construir una instancia suya vacía, quedando en ese momento en el


estado UNREALIZED, aún sin información alguna (en este estado no deben ser usados
los métodos heredados de Controllable).

 Tras ello, el reproductor necesita localizar el media a reproducir, ya sea de forma local o
en algún servidor remoto.

 Una vez localizado, estará en el estado REALIZED. En él puede comenzar a adquirir el


media, lo cual podrá suponer un consumo de tiempo considerable.

 Una vez conseguido y tras pasar por el estado PREFETCHED (en el cual se llenan
buffers y se reservan recursos en exclusividad) podrá reproducirse, pasando para ello al
estado STARTED.

 Del estado STARTED, al parar la reproducción, se volverá al estado PREFETCHED.

 Cuando se desee cerrar el reproductor y liberar los recursos que ha necesitado, se


pasa éste al estado CLOSED en el cual finaliza su ciclo de vida, del cual ya no podrá
salir.

Aunque éste es el ciclo de vida recomendado, pueden existir saltos en estos cambios de estado
del Player. Con ello, perdemos el control programático que nos da el ciclo de vida original, donde
podemos actuar si no se ha conseguido pasar de un estado determinado a otro. Por ejemplo, el
siguiente código sería válido, pero al no cuidar de pasar programáticamente por REALIZED y
PREFETCHED, no tendremos tanto control ante un posible fallo al lanzar directamente (start()) la
reproducción del media.

try {
Player p = Manager.createPlayer("http://www.site.com/media/media.wav");

- 77 -
p.start();
}
catch (MediaException pe) { }
catch (IOException ioe) { }

Los métodos de la interface, de los que parte nos podremos imaginar su funcionalidad dada la
figura anterior, son:

 void addPlayerListener(PlayerListener playerListener). Asocia el escuchador de


eventos PlayerListener parámetro al Player actual. Si se pasa a null no se hace nada.

 void close(). Liber recursos y cierra el reproductor, pasándolo al estado CLOSED. Si ya


estaba cerrado no se hace nada.

 void deallocate(). Libera los posibles recursos exclusivos de los que se haya podido
apropiar el Player para realizar su reproducción como, por ejemplo, la salida de audio.

En el caso de que la llamada a realize() bloquee el Player, ya que ésta puede ser muy
costosa, una llamada a deallocate() lo desbloqueará y pasará al estado UNREALIZED.

Por otro lado, si se llama a este método desde PREFETCHED devolverá el Player al
estado REALIZED, y si se llama estando en STARTED la llamada derivará a un stop()
internamente.

 String getContentType(). Devuelve el tipo de contenido que está reproduciendo el


Player.

- 78 -
Este tipo de contenido se da con la cadena correspondiente a su tipo MIME. Los más
utilizados son:

audio/x-wav Formato de audio WAV.


audio/mpeg Formato MP3.
audio/midi Formato MIDI.
audio/x-tone-seq Formato de secuencia de tonos.
video/mpeg Formato de vídeo MPEG (A usar con la MMA, no en MIDP)

Dependerán de cada dispositivo los formatos soportados. En el emulador WTK 2.2 que
usamos en nuestras prácticas, los tipos permitidos son: audio/x-tone-seq, audio/x-wav,
audio/midi, audio/sp-midi, image/gif, video/mpeg y video/vnd.sun.rgb565.

 long getDuration(). Obtiene la duración en microsegundos del media, cuando éste se


reproduce a su velocidad por defecto. Si la duración no puede ser determinada
(elementos multimedia "en vivo") se devolverá la constante TIME_UNKNOWN.

 long getMediaTime(). Obtiene el media-time en microsegundos del elemento


multimedia. Si este tiempo no puede ser determinado se devolverá TIME_UNKNOWN.
Este media-time se define como el momento actual de la reproducción y se moverá
desde 0 a la duración total del media.

 int getState(). Estado actual del Player, a saber: UNREALIZED, REALIZED,


PREFETCHED, STARTED y CLOSED.

 void prefetch(). Provocará el paso a PREFETCHED, adquiriendo para ello recursos en


exclusividad y todo lo necesario para que la latencia al comenzar la reproducción
(start() ) sea mínima.

 void realize(). Provocará el paso a REALIZED, trayéndose localmente el media a


reproducir. Esta operación puede ser costosa en tiempo.

 void removePlayerListener(PlayerListener playerListener). Desasocia del Player


actual el escuchador pasado como parámetro. Si no está asociado o el objeto pasado
es nulo, no se hace nada.

- 79 -
 void setLoopCount(int count). Fija cuántas veces se podrá reproducir en bucle el
media. Por defecto, una única vez (1), si damos -1 reproducirá en bucle infinito y si
damos 0 se elevará una IllegalArgumentException. No debe ser usado si el Player se
encuentra en estado STARTED.

 long setMediaTime(long now). Fija el media-time al valor dado en microsegundos, es


decir, varía el momento actual de reproducción cuando el tipo que se reproduce acepta
esto.

El hecho de no ser demasiado preciso este método para algunos formatos, hace
necesario que se devuelva como parámetro de salida el momento actual que finalmente
se ha conseguido fijar tras su invocación.

El media-time toma valores desde 0 a la duración del media a reproducir, por lo que si
damos un valor negativo se fijará 0, y si damos un valor más allá de la duración, ésta
será la fijada. No debe ser usado si el Player se encuentra aún en estado
UNREALIZED.

 void start(). Comenzará la reproducción del media desde el principio o desde donde se
quedó ante una llamada al método stop(), pasando al estado STARTED tan pronto
como pueda. No se garantiza que se pase por este estado de todas formas, ya que, el
media puede tener duración 0 o muy corta y haber finalizado ya (volviendo a
PREFETCHED). Sí se asegura que se genere un evento STARTED.

 void stop(). Parará la reproducción, pausándola en el media-time actual. Esto hará


pasar al Player al estado PREFETCHED.

• PlayerListener

Define un escuchador de eventos asíncronos para un objeto Player. Como es usual, nuestro
MIDlet implementará esta interface y asociaremos ese MIDlet al Player usando el método
addPlayerListener(<MIDlet>).

Un reproductor podrá tener tantos PlayerListener como desee y todos ellos estarán escuchando
los eventos que el Player emita. Además, se garantiza que los eventos son recogidos por el
listener en el orden que son emitidos. Por ejemplo, si una reproducción finaliza muy rápidamente

- 80 -
se garantiza que llegarán en orden un evento STARTED y tras él un END_OF_MEDIA, aunque
quizás no haya dado tiempo a que el Player haya pasado por el estado STARTED.

Reflejados en constantes de la presente interface, estos eventos podrán ser (damos para cada
uno de ellos un valor eventData, ya veremos más adelante para qué):

 static String CLOSED. Evento emitido al cerrar un Player invocando al método close().

eventData: null.

 static String DEVICE_AVAILABLE. Emitido al ser liberado un recurso que el sistema u


otra aplicación de más prioridad que la nuestra tenía asignado en exclusividad, y al cual
nosotros estábamos esperando. Nuestro Player se encontrará en el estado REALIZED
y podrá entonces pasar al siguiente estado invocando al método prefetch() o start().

eventData: cadena identificando el nombre del recurso ya accesible.

 static String DEVICE_UNAVAILABLE. Emitido cuando el sistema u otra aplicación de


más prioridad que la nuestra tiene asignado en exclusividad un recurso que
necesitamos para llevar a cabo nuestra reproducción. Nuestro Player se encontrará en
el estado REALIZED y podrá, al emitirse finalmente un DEVICE_AVAILABLE, pasar al
siguiente estado invocando al método prefetch() o start(). Si no se consigue el recurso
tras un tiempo, un evento ERROR es generado.

eventData: cadena identificando el nombre del recurso no accesible.

 static String DURATION_UPDATED. Emitido cuando la duración del Player es


modificada.

eventData: un objeto Long con la nueva duración del media.

 static String END_OF_MEDIA. Emitido al alcanzar el media el final de su contenido, en


cada bucle de reproducción.

eventData: un objeto Long con el media-time en el cual el objeto ha alcanzado su fin.

- 81 -
 static String ERROR. Emisión de un error en el tratamiento del listener.

eventData: una cadena identificando el error.

 static String STARTED. Enviado cuando comienza la reproducción del Player.

eventData: un objeto Long con el media-time en el cual se ha comenzado (o retomado)


la reproducción.

 static String STOPPED. Enviado cuando termina la reproducción del Player.

eventData: un objeto Long con el media-time en el cual se ha terminado la


reproducción.

 static String VOLUME_CHANGED. Emitido al variar el volumen del dispositivo.

eventData: un objeto VolumeControl con la información del nuevo volumen.

Tras ver los eventos disponibles, el único método que presenta la interface es void
playerUpdate(Player player, String event, Object eventData). A ella daremos cuerpo en la
aplicación que implemente PlayerListener, ya que, él será el encargado de recoger todos los
eventos relacionados con el tratamiento multimedia que sucedan sobre ella. Por este motivo, en él
definiremos qué hacer al recibir alguno de los eventos anteriores.

Los parámetros que recibimos para ello son: el Player al que "escuchamos", el evento que ha sido
lanzado y el eventData asociado a este evento, el cual nos dará información sobre él (para este
método dimos en cada constante el valor de eventData asociado).

− CLASES

• Manager extends Object

Con esta clase a modo de factoría construiremos los objetos Player que vayamos a utilizar en
nuestras aplicaciones para reproducir y controlar los elementos multimedia deseados.
Tendremos una sola constante y varios métodos en esta clase:

- 82 -
 static String TONE_DEVICE_LOCATOR. Constante que daremos como parámetro
locator al ir a crear un reproductor de secuencia de tonos. Por ejemplo, haríamos:

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

 static Player createPlayer(InputStream stream, String type). Con este método


instanciamos un Player tomando como origen un flujo de entrada del que leemos el
media a reproducir del tipo MIME marcado por el parámetro type.

 static Player createPlayer(String locator). Con éste instanciamos un Player ahora


tomando como origen de datos los que descarguemos de la URL dada como
parámetro, vía HTTP. Esto supondrá un coste de tiempo que obliga a llevar esta
comunicación en un hilo de ejecución aparte (recordar tema anterior), además del coste
monetario que le supone al usuario de un dispositivo real acceder a la red.

 static String[] getSupportedContentTypes(String protocol). Nos devuelve los tipos


MIME soportados para el protocolo parámetro o todos los soportados por la
implementación si damos null. Por ejemplo, si damos la cadena "http" como parámetro,
nos devolverá en un array de String los tipos MIME a los que podremos acceder por
medio de una comunicación de este tipo.

 static String[] getSupportedProtocols(String content_type). Estamos ante el caso


contrario al anterior, es decir, dado el tipo MIME al que queremos acceder, este método
nos da los protocolos permitidos para ello.

 static void playTone(int note, int duration, int volume). Este método hace que el
dispositivo emita una única nota de sonido. Por su simpleza, se incluye en la clase
directamente, sin que haya que construir un Player para realizar esta reproducción tan

- 83 -
simple. Damos la nota a reproducir (valor de 0 a 127), la duración en milisegundos y su
volumen (valor de 0 a 100).

Este método puede saturar la CPU del dispositivo si éste no soporta creación de
melodías.

− EXCEPCIONES

• MediaException extends Exception

Excepción que usarán los métodos del API multimedia para reflejar un error en su ejecución, tal
como los errores provocados por los métodos de cambio de estado (realice(), prefetch(), etc.)
cuando no son capaces de llevar a cabo ese cambio o los provocados por el método
setMediaTime() cuando el formato no admite que se modifique el momento actual de la
reproducción, etc.

3.2.1.2. javax.microedition.media.control

Se trata del segundo paquete de MIDP 2.0 orientado al tratamiento multimedia. En él encontraremos los
controles que podremos ejercer sobre los reproductores Player que definamos. En MMA veremos cómo
este paquete será potentemente ampliado.

− INTERFACES

Sólo aparecerán en este paquete elementos de tipo Interface, en concreto, los dos controles
(interfaces hijas de Control) siguientes:

• ToneControl extends Control

Con esta interface damos al Player la secuencia de tonos no polifónicos que debe reproducir.
Dispondremos de múltiples constantes para definir la secuencia y un único método que asocia la
secuencia al objeto ToneControl. En un ejemplo anterior vimos cómo se usaba esta interface, por
lo que estudiaremos ahora cómo se forma el array de bytes que llamábamos allí
secuenciaAReproducir como un conjunto de pares de bytes tono-duración, dispuestos en
bloques definidos por nosotros. Por ejemplo, una secuencia válida sería:

- 84 -
// "Mary Had A Little Lamb" tiene 3 bloques; A, B y C. Definimos el A entre BLOCK_START y BLOCK_END
para así //poderlo llamar de nuevo más adelante.
byte tempo = 30; // tempo a 120 bpm (beats per minute)
byte d = 8; // duración del tono que le precede. En este ejemplo, todos tendrán la misma.
byte C4 = ToneControl.C4;
byte D4 = (byte)(C4 + 2);
byte E4 = (byte)(C4 + 4);
byte G4 = (byte)(C4 + 7);
byte rest = ToneControl.SILENCE;
byte[] secuenciaAReproducir = { ToneControl.VERSION, 1, // versión 1
ToneControl.TEMPO, tempo, // fijamos el tempo
ToneControl.BLOCK_START, 0, // comienza bloque A
E4,d, D4,d, C4,d, E4,d,
E4,d, E4,d, E4,d, rest,d,
ToneControl.BLOCK_END, 0, // fín del bloque A
ToneControl.PLAY_BLOCK, 0, // reproducimos A
D4,d, D4,d, D4,d, rest,d,
E4,d, G4,d, G4,d, rest,d, // reproducimos directamente bloque B
ToneControl.PLAY_BLOCK, 0, // repetimos bloque A
D4,d, D4,d, E4,d, D4,d, C4,d // reproducimos directamente bloque C
};

Los elementos disponibles en la interface son:

 static byte BLOCK_END. Indica el punto final del bloque, identificado éste por el
siguiente byte en la secuencia.

 static byte BLOCK_START. Indica el punto inicial de un bloque. En el byte seguido a


él nominaremos el bloque, dando en ese siguiente byte un identificador de 0 a 127.

 static byte C4. Valor de la nota DO central. Se suele usar como referencia de las
demás notas a utilizar.

 static byte PLAY_BLOCK. Reproduce el bloque identificado por el siguiente byte, el


cual debe estar definido previamente en la secuencia.

- 85 -
 static byte REPEAT. Seguido a esta constante indicaremos el número de repeticiones
para el tono definido justo antes de la ella (valor de 2 a 127 dado en el siguiente byte a
esta constante).

 static byte RESOLUTION. A continuación de esta constante indicaremos la resolución


aplicada a la secuencia. Con valores bajos se observa una reproducción más amplia de
cada nota. Valores de 1 a 127.

 static byte SET_VOLUME. Daremos tras él el volumen a aplicar a las notas que siguen
a este par (ToneControl.SET_VOLUMEN, <nuevoVolumen>,...) en la secuencia.
Valores posibles de 0 a 100.

 static byte SILENCE. Representa un silencio, a usar como otro tono más en la
secuencia (tras él, se indicará su duración).

 static byte TEMPO. Después de él fijamos el tempo de la secuencia definida. Valores


de 5 a 127.

 static byte VERSION. Tras él fijamos la versión de la secuencia definida.

 void setSequence(byte[] sequence). El único método de la interface que permite


asociar la secuencia formada, gracias a las constantes previas, con el ToneControl
actual. Una vez hecho esto, podremos llamar a Player.start() para comenzar su
reproducción, como vimos en un ejemplo anterior.

• VolumeControl extends Control

Con esta interface podremos manipular el volumen de audio de un Player, desde un valor 0 de
silencio hasta 100, que representará el volumen máximo configurado en ese momento para el
dispositivo. Cuando el estado de este objeto cambia (modificadores siguientes), un evento
VOLUME_CHANGED será emitido.

Los métodos que tenemos en esta interface son:

 int getLevel(). Devuelve el nivel de volumen actualmente establecido. Devolverá -1


sólo si el Player está en estado REALIZED y aún no se ha llamado a setLevel().

- 86 -
 boolean isMuted(). Indica si estamos en modo silencio o no.

 int setLevel(int level). Modifica el nivel de volumen de este control del Player. Si se da
un valor menor de 0 se impondrá un 0, si mayor de 100 se impondrá un valor de 100.

 void setMute(boolean mute). Modifica el estado de silencio. Pasando el parámetro a


true se silenciará la señal. El hecho de variar esta característica no afecta al nivel de
volumen que getLevel() devolverá.

3.2.2. MMA 1.1

Seguidamente, estudiaremos el API que nos ofrece la Mobile Media API (JSR 135) para la creación
multimedia que buscamos. Esta nueva especificación viene a aumentar la especificación del perfil MIDP
que acabamos de estudiar, no a modificar ni obviar nada de lo que en ella se ha utilizado. Por tanto,
además de lo ya visto, pasaremos ahora por los nuevos elementos que han sido incorporados por la JSR
135 a partir de la base vista de MIDP 2.0, la cual se puede considerar un subconjunto de MMA 1.1.

Esta innovación aporta a MIDP 2.0:

− Inclusión de controles para vídeo y gráficos, no sólo audio.


− Capacidad de almacenar elementos multimedia del ambiente, vía cámara y/o micrófono.
− Soporte para reproducciones simultáneas sincronizadas de varios Players.
− Definición "a medida" de nuevos protocolos y formatos. Paquete
Javax.microedition.media.protocol.
− Importante ampliación de la clase Manager y del paquete javax.microedition.media.control.

Los paquetes en los que se divide esta API son javax.microedition.media,


javax.microedition.media.control y javax.microedition.media.protocol. Este último no lo estudiaremos
a fondo, sólo comentar de él que consta de una interface (SourceStream) y dos clases
(ContentDescriptor, DataSource) con las cuales definir un acceso abstracto a elementos multimedia,
ofreciendo así al programador que lo desee la capacidad de crear reproductores propios para tipos de
formatos y protocolos no soportados.

En los próximos apartados describiremos las novedades principales introducidas en los otros dos
paquetes. Cabe mencionar que no nos detendremos tanto como en MIDP, excepto en lo relativo a
reproducción de vídeo.

- 87 -
3.2.2.1. javax.microedition.media

Aquí las novedades son escasas: las interfaces Control y Controllable quedan intactas, variando de las
siguientes lo que detallamos:

− INTERFACES

• Player

La interface Player provee ahora mecanismos para sincronizarse con otros Player y reproducirse
conjuntamente a ellos. Para esto, aparece un nuevo concepto que estudiaremos al llegar a la
interface que lo define: el TimeBase.

En MMA todo Player debe tener asociado un TimeBase. Para manejarlo, aparecen en la
interface dos nuevos métodos:

 TimeBase getTimeBase(): obtiene el TimeBase asociado al reproductor.


 void setTimeBase(TimeBase master): modifica el TimeBase asociado al reproductor.

Dos Player podrán sincronizarse obteniendo el TimeBase de uno y asociándolo al otro, si el


segundo Player admite un TimeBase distinto al suyo. En caso contrario, se elevará una
MediaException. Así, el Player cuyo TimeBase es variado sincronizará la frecuencia (rate) de su
reproducción acorde a ese nuevo TimeBase.

Por otro lado, un Player detenía su reproducción al alcanzar el fin del media o ante una llamada
a stop(). Ahora también se detendrá si se alcanza un tiempo stopTime que podremos definir con
la nueva interface StopTimeControl que veremos más adelante.

El resto de la especificación de la interface Player queda invariable respecto a la ya estudiada de


MIDP 2.0.

- 88 -
• PlayerListener

La novedad en esta interface es la aparición de 7 nuevas constantes, representando nuevos


eventos a escuchar:

 static String BUFFERING_STARTED. Emitido cuando el Player entra en modo buffer.

eventData. Long con el media-time en el cual ha entrado en modo buffer.

 static String BUFFERING_STOPPED. Emitido cuando el Player sale del modo buffer.

 eventData. Long con el media-time en el cual ha salido del modo buffer.

 static String RECORD_ERROR. Emitido ante un error en la grabación del media.

eventData. String con la causa del error de almacenamiento.

 static String RECORD_STARTED. Emitido al comenzar a grabar el media del


ambiente.

 eventData. Long con el media-time de inicio de grabación.

 static String RECORD_STOPPED: emitido al parar la grabación del media.

eventData. Long con el media-time de parada de grabación.

 static String SIZE_CHANGED. Para vídeo, es emitido cuando el tamaño de la ventana


donde se muestra es variado.

eventData. Objeto VideoControl del cual poder consultar el nuevo tamaño.

 static String STOPPED_AT_TIME. Emitido cuando el Player se detiene al alcanzar el


stopTime definido con un StopTimeControl.

eventData. Long con el media-time en el cual el Player se detiene.

- 89 -
• TimeBase

Esta interface aparece totalmente nueva, ya que, no existía en MIDP 2.0. Como ya
mencionamos, se usará para sincronizar la reproducción de dos o más objetos Placer y actuará
como fuente de continuos "ticks" de tiempo, los cuales serán usados como medida del progreso
de la reproducción del Player al que le asociemos este "cronómetro". Ésta será la herramienta
base para sincronizar un Player con otro, ofreciendo un único método:

 long getTime(). Obtiene el momento actual del "cronómetro" en microsegundos;


valores no negativos y valores que no se decrecen con el paso del tiempo.

− CLASES

• Manager

La clase Manager presenta importantes novedades. Ahora nos ofrece la construcción de objetos
Player tomando de base un DataSource (propio de la javax.microedition.media.protocol que no
trataremos) que definirá un protocolo y formato personalizado, gracias al cual podremos crear
reproductores que manejen formatos no reconocidos por la especificación o que se nos ofrezcan
mediante protocolos no especificados aún.

Por otra parte, existirán ahora muchos más protocolos predefinidos que podremos utilizar a la
hora de dar la URI de la que recoger el media a reproducir, además de los protocolos ya
disponibles en MIDP 2.0. Algunos son:

 capture://. Con él obtendremos el media capturándolo del ambiente, audio y/o vídeo,
usando el micrófono y/o la cámara del dispositivo, siempre que estén disponibles estos
recursos.

Por ejemplo: capture:// devcam0?encoding=rgb888&width=160&height=120&fps=7.

 rtp://. Con este protocolo se accede a elementos multimedia de tipo streaming por
medio de una sesión RTP.

Un ejemplo sería: rtp://224.1.2.3:12344/audio.

- 90 -
 capture://radio. Utilizado para conseguir la entrada a la aplicación del contenido de
audio ofrecido por una emisora de radio.

Por ejemplo, capture://radio?f=91.9M&st=auto.

Respecto a los elementos de la API, aparecen incorporados a los que ya vimos en MIDP 2.0:

 static String MIDI_DEVICE_LOCATOR. Constante que daremos como parámetro


locator al ir a crear un reproductor MIDI en el cual deseemos tener disponible su control
MIDIControl.

 static Player createPlayer(DataSource source). Método para instanciar un


reproductor tomando de base un DataSource definido por el programador.

 static TimeBase getSystemTimeBase(). TimeBase por defecto, a usar para todo


Player donde no se le dé explícitamente, ya que, en MMA todo Player debe tener un
TimeBase asociado. Dependerá del dispositivo.

3.2.2.2. javax.microedition.media.control

Aquí aparece una gran aportación de MMA al API Multimedia de MIDP 2.0. En total, se crean 10 nuevos
controles, dejando invariables los provenientes de MIDP 2.0 VolumeControl y ToneControl.

A continuación, listamos (sólo nos detendremos en VideoControl y su interface padre GUIControl) cuáles
son estas novedades, en forma de interfaces hijas de Control.

− INTERFACES:

• FramePositioningControl extends Control

Esta interface nos proporciona control para posicionar la reproducción de un vídeo en un frame
determinado. Cada frame del vídeo vendrá determinado por su número, siempre mayor o igual
que 0, correspondiendo este último al media-time 0 inicial (inicio del media).

- 91 -
• GUIControl extends Control

Control a utilizar con elementos multimedia susceptibles de ser presentados en la interface del
usuario (GUI), dentro de un objeto de formulario (por ejemplo, vídeos). Ofrece dos elementos:

 static int USE_GUI_PRIMITIVE. Define el modo en el que el media es reproducido, a


usar con el método initDisplayMode(). Con este modo, al llamar a initDisplayMode()
será devuelto como salida directamente un objeto de la GUI, al cual se asocia el media
a reproducir (vídeo usualmente).

En este modo, el parámetro de entrada dado a este método puede ser null o el nombre
del objeto de la GUI al que se desea asociar.

 Object initDisplayMode(int mode, Object arg). Este método lo veremos en


VideoControl. Allí será heredado al extender VideoControl a GUIControl: Un vídeo
necesitará reproducirse en algún objeto de la interface de usuario (GUI).

• MetaDataControl extends Control

Esta interface es utilizada para recoger metainformación (title, copyright, author, etc.) definida en
el propio elemento multimedia.

• MIDIControl extends Control

Permite acceso al control de los sintetizadores software/hardware del dispositivo, encargados de


la reproducción MIDI.

• PitchControl extends Control

Permite modificar el pitch (escala) actual del audio sin modificar la velocidad ni el volumen de la
reproducción.

• RateControl extends Control

Modifica la frecuencia (rate) de la reproducción del Player, la cual impondrá la relación entre el
TimeBase asociado el Player y su media-time actual.

- 92 -
• RecordControl extends Control

Permite almacenar en el dispositivo el media que actualmente está reproduciendo el Player al


que este control esté asociado.

• StopTimeControl extends Control

Ya comentado anteriormente, nos permite predefinir un tiempo de parada (stopTime) para el


Player asociado. Con esto, tenemos en MMA una nueva forma de detener la reproducción,
además del método stop() o el llegar al fin del media que teníamos en MIDP 2.0.

• TempoControl extends RateControl

Fijará el tempo, en términos musicales, de un audio MIDI. Heredará de RateControl.

• VideoControl extends GUIControl

Controla la reproducción de vídeo por parte de un Player. Los elementos que nos ofrece son:

 static int USE_DIRECT_VIDEO. Define el modo en el que el vídeo es reproducido, a


usar con el método initDisplayMode().

Este modo sólo puede ser usado en sistemas con soporte LCDUI, como es el caso de
los dispositivos MIDP. Aquí al llamar al método initDisplayMode() será siempre devuelto
null como parámetro de salida. No obstante, el parámetro de entrada dado a este
método ahora no podrá ser null (a diferencia de como ocurría en el modo
USE_GUI_PRIMITIVE), ya que debe ser un objeto javax.microedition.lcdui.Canvas
válido o una subclase suya.

En este modo, el vídeo es directamente presentado en el Canvas de la pantalla,


reproduciéndose en la posición que indiquemos con setDisplayLocation().

 int getDisplayHeight(). Método que obtiene la altura de la emisión del vídeo en


píxeles.

 int getDisplayWidth(). Obtiene la anchura de la emisión del vídeo en píxeles.

- 93 -
 int getDisplayX(). Coordenada X de la posición del vídeo respecto a la esquina
superior izquierda del objeto de la interface de usuario (GUI) que lo contiene.

 int getDisplayY(). Coordenada Y de la posición del vídeo respecto a la esquina


superior izquierda del objeto de la interface de usuario (GUI) que lo contiene.

 byte[] getSnapshot(String imageType). Obtiene una captura del instante actual de la


reproducción del vídeo. La imagen capturada se devuelve en forma de array de bytes,
de formato el especificado con el parámetro imageType.

 int getSourceHeight(). Alto del archivo de vídeo fuente.

 int getSourceWidth(). Ancho del archivo de vídeo fuente.

 Object initDisplayMode(int mode, Object arg). Fija el modo en el que el vídeo será
emitido, debiendo ser invocado antes de que el vídeo pueda ser reproducido. Los dos
posibles modos ya han sido estudiados (USE_GUI_PRIMITIVE heredado de la interface
GUIControl y USE_DIRECT_VIDEO), y con ellos el significado de los dos elementos
Object de entrada y de salida que observamos en el prototipo del método.

 void setDisplayFullScreen(boolean fullScreenMode). Presenta el vídeo a pantalla


completa si el parámetro dado es true.

 void setDisplayLocation(int x, int y). Coloca el vídeo en la posición dada respecto a


la esquina superior izquierda del objeto de la interface de usuario (GUI) que lo contiene.
Por defecto, el vídeo aparece en la posición (0,0). Este método sólo es válido en modo
USE_DIRECT_VIDEO, siendo obviado en modo USE_GUI_PRIMITIVE.

 void setDisplaySize(int width, int height). Dado un ancho y un alto, redimensiona el


vídeo que se está reproduciendo. Si estamos en modo USE_DIRECT_VIDEO, este
redimensionamiento no afecta al objeto de la GUI que lo contiene, ocultando la parte del
vídeo que sobresalga de ella, si esto ocurre. Si estamos en modo
USE_GUI_PRIMITIVE el objeto de la GUI sí se ve redimensionado con el vídeo.

- 94 -
 void setVisible(boolean visible). Modifica la visibilidad del vídeo. Por defecto, si
estamos en modo USE_DIRECT_VIDEO, el vídeo no es visible. Será necesaria una
llamada a setVisible(true) para poder visionarlo.

3.3. EJEMPLO MULTIMEDIA

La suite ofrecida en este ejemplo constará de tres MIDlets, uno por familia de elemento multimedia a
reproducir. Comprobamos que la pantalla donde se nos ofrece la elección del MIDlet a ejecutar queda en
manos del dispositivo, presentándosenos en el caso del WTK 2.2 de la siguiente forma:

Para concretar un poco la acción de cada MIDlet en esa lista, hemos usado nombres largos asociados al
nombre de la clase de cada MIDlet, como puede observarse en el contenido del JAD que describe a la
suite:

MIDlet-1: Reproducción de TONOS (código), , MEDIAEjemploMIDlet1


MIDlet-2: Reproducción de AUDIO (vía HTTP), , MEDIAEjemploMIDlet2
MIDlet-3: Reproducción de VÍDEO (vía res del JAR), , MEDIAEjemploMIDlet3
MIDlet-Jar-Size: 100
MIDlet-Jar-URL: ejemploMEDIA.jar
MIDlet-Name: ejemploMEDIA
MIDlet-Vendor: Unknown

- 95 -
MIDlet-Version: 1.0
MicroEdition-Configuration: CLDC-1.1
MicroEdition-Profile: MIDP-2.0

Seguidamente, presentamos ya el código, como siempre profusamente comentado para facilitar su


completo entendimiento, de cada MIDlet de la suite:

MEDIAEjemploMIDlet1.java

import javax.microedition.lcdui.*;
import javax.microedition.midlet.*;
import javax.microedition.media.*;
import javax.microedition.media.control.*;
import java.io.IOException;
import java.util.*;

//Clase del primer MIDlet, el cual reproducirá una secuencia de tonos definida
//en el propio código de la clase. Usaremos un createPlayer(Manager.TONE_DEVICE_LOCATOR )
public class MEDIAEjemploMIDlet1 extends MIDlet implements CommandListener {

// Definimos Pantalla y Formulario


private Display display;
private Form form;

// Definimos comandos de acción para el MIDlet: Salida a lista de la suite y Reproducción


private Command volver;
private Command reproducir;

// Definimos el reproductor y un controlador de tonos para él


private Player player;
private ToneControl tc;

//-------------------------------------------------------------------------------------------------------------------------------------------------------
// Constructor del MIDlet. En él hacemos las inicializaciones previas al arranque del MIDlet
public MEDIAEjemploMIDlet1() {

// Creamos el formulario
form = new Form("Secuencia de TONOS");
// Iniciamos los comandos de la interface

- 96 -
volver = new Command("Volver", Command.EXIT, 1);
reproducir = new Command("Reproducir", Command.SCREEN, 2);
System.out.println("MIDlet TONOS CONSTRUIDO");
}
//-------------------------------------------------------------------------------------------------------------------------------------------------------
//Ciclo de vida del MIDlet
//Al comenzar, presentamos un mensaje al usuario y esperamos un evento de comando
public void startApp() {

System.out.println("COMIENZO DEL MIDLET TONOS POR startup()");


try{
// Capturamos el control de la pantalla
display = Display.getDisplay(this);

//Mostramos un mensaje informativo en la interface


form.append("Reproducción de TONOS: Conecte el Audio del dispositivo y " +
"pulse 'Reproducir'. Cada pulsación reiniciará la secuencia");

//Asociamos comandos y un escuchador para ellos al formulario actual


form.addCommand(volver);
form.addCommand(reproducir);
form.setCommandListener(this);

//Presentamos en la pantalla el formulario


display.setCurrent(form);

//Emitimos una nota simple de aviso de inicio de la app (DO central)


Manager.playTone(ToneControl.C4, 500, 100);
}
catch(Exception e){
System.out.println("EXCEPCIÓN EN startApp():" + e.toString());
}
}

// Los otros dos métodos del ciclo de vida, vacíos---------------------------------------------------------------------------------------


public void pauseApp() {}
public void destroyApp( boolean flag ) {
System.out.println("SALIDA DEL MIDLET TONOS POR destroyApp()");
}

- 97 -
//-------------------------------------------------------------------------------------------------------------------------------------------------------
// Manejador de eventos relacionados con la interface
public void commandAction(Command c, Displayable d) {
try{
//Se ha pulsado Volver
if(c == volver){
// Cerramos Reproductor y su Control de Tonos asociado
if(player != null) { player.close(); player = null; }
if(tc!= null) tc = null;

//Destruimos el MIDlet y lo notificamos al dispositivo para salir de él


destroyApp(true);
notifyDestroyed();
}
//Se ha pulsado Reproducir
else if(c == reproducir) {
//Si se pulsa mientras está reproduciendo, posicionamos la reproducción al principio
if(player!=null && player.getState()==Player.STARTED) player.setMediaTime(0);
//Si aún no está reproduciendo, iniciamos el Player
else this.reproducirTonos();
}
}
catch(Exception e){
System.out.println("EXCEPCIÓN EN commandAction():" + e.toString());
}
}

//-------------------------------------------------------------------------------------------------------------------------------------------------------
// Método encargado de crear la secuencia de tonos y darla al Player para que la reproduzca
public void reproducirTonos()
{
byte dur = 8;
byte[] secuenciaAReproducir = {
ToneControl.VERSION,1,
ToneControl.TEMPO,20,
ToneControl.RESOLUTION, 50,
ToneControl.BLOCK_START,0,
90,dur, 92,dur, 94,dur, 95,dur, ToneControl.REPEAT, 5,
96,dur, ToneControl.SET_VOLUME,50, 97,dur, 98,dur, 96,dur,
ToneControl.C4,dur, ToneControl.SILENCE,dur,

- 98 -
ToneControl.BLOCK_END,0,

ToneControl.BLOCK_START,1,
60,dur, 62,dur, 64,dur, 65,dur,ToneControl.REPEAT, 5,
66,dur, ToneControl.SET_VOLUME, 100, 67,dur, 68,dur, 66,dur,
ToneControl.C4,dur, ToneControl.SILENCE,dur,
ToneControl.BLOCK_END,1,

ToneControl.PLAY_BLOCK,0,
ToneControl.PLAY_BLOCK,1,
ToneControl.SET_VOLUME,100,ToneControl.PLAY_BLOCK,0,
};
try {
// Creamos el reproductor
player = Manager.createPlayer(Manager.TONE_DEVICE_LOCATOR);
System.out.println("RECIÉN CREADO, EL PLAYER ESTÁ EN ESTADO:" + player.getState());

// Iniciamos y acaparamos recursos


player.realize();
// Obtenemos el control de tonos asociado al Player
tc = (ToneControl)player.getControl("ToneControl");
// Asignamos la secuencia a reproducir
tc.setSequence(secuenciaAReproducir);
//Reproducimos dos veces
player.setLoopCount(2);

System.out.println("JUSTO ANTES DE START, EL PLAYER ESTÁ EN ESTADO:" + player.getState());


//Mediante la siguiente orden comenzará la reproducción de la secuencia
player.start();
}
catch( Exception e ) {
System.out.println("EXCEPCIÓN EN reproducirTonos():" + e.toString());
}
}

}//fín clase MEDIAEjemploMIDlet1.java

- 99 -
MEDIAEjemploMIDlet2.java

import javax.microedition.lcdui.*;
import javax.microedition.midlet.*;
import javax.microedition.media.*;
import javax.microedition.media.control.*;
import java.io.IOException;
import java.util.*;

//Clase del segundo MIDlet, el cual reproducirá archivos WAV / MIDI a alcanzar vía HTTP
//del directorio Tomcat dado en /escuchadorDeJ2ME/media/. Usaremos un createPlayer(String locator), el cual se
//ocupará de la comunicación HTTP de forma invisible a nosotros.
public class MEDIAEjemploMIDlet2 extends MIDlet implements Runnable, CommandListener, PlayerListener {

// URLs de los media de audio a reproducir:


static final String URL1 = "http://localhost:8080/escuchadorDeJ2ME/media/ejemplo21.wav";
static final String URL2 = "http://localhost:8080/escuchadorDeJ2ME/media/ejemplo22.mid";

// Pantalla del dispositivo, lista y Formulario


private Display display;
private List itemList;
private Form form;

// Definimos la tarea encargada de la reproducción. Al buscar el media via HTTP, es obligatorio el


//uso de un hilo separado que se encargue de la comunicación en red.
private Thread t;

//Comandos disponibles en la interface. Asociaremos a la pantalla con la Lista el Reproducir


//y el Volver, y una vez dentro de la reproducción asociaremos a la pantalla con el Form
//de la reproducción los comandos Reproducir y el Pausar/Reproducir.
private Command volver;
private Command pausar;
private Command reproducir;

- 100 -
//TAD para elementos de la lista
private Hashtable items;

//Reproductor
private Player player;
//-------------------------------------------------------------------------------------------------------------------------------------------------------
//Constructor del MIDlet. En él hacemos las inicializaciones previas al arranque del MIDlet
//mediante el startup() posterior.
public MEDIAEjemploMIDlet2()
{
//Damos cuerpo a los comandos de la interface
volver = new Command("Volver", Command.STOP, 1);
pausar = new Command("Pausar", Command.ITEM, 1);
reproducir = new Command("Reproducir", Command.ITEM, 1);

//Damos cuerpo al formulario donde presentaremos una imagen mientras se reproduce el media
form = new Form("Reproduciendo archivo de AUDIO");

//Damos cuerpo a la lista donde presentaremos los elementos de AUDIO a seleccionar


itemList = new List("Selección de archivo de AUDIO", List.IMPLICIT);

//Cargamos un conjunto con el que acceder mediante el nombre seleccionado en la lista


//a las URL a reproducir
items = new Hashtable();
System.out.println("MIDlet AUDIO CONSTRUIDO");
}

//-------------------------------------------------------------------------------------------------------------------------------------------------------
//Ciclo de vida del MIDlet
//Al comenzar, presentamos la lista de selección de AUDIO y esperamos un evento de comando
public void startApp()
{
System.out.println("COMIENZO DEL MIDLET AUDIO POR startup()");

//Capturamos la pantalla del dispositivo


display = Display.getDisplay(this);

//Asociamos al formulario los comandos activos mientras él esté en pantalla


form.addCommand(volver);
form.addCommand(pausar);

- 101 -
form.setCommandListener(this);

//Asociamos a la lista los comandos activos mientras ella esté en pantalla


itemList.addCommand(volver);
itemList.addCommand(reproducir);
itemList.setCommandListener(this);

//Cargamos el conjunto de las URL


items.put("Ejemplo WAV por HTTP", URL1);
items.put("Ejemplo MIDI por HTTP", URL2);

//Cargamos la lista con el conjunto anterior y la presentamos en pantalla. Así comenzamos a


//interactuar con el usuario
for(Enumeration en = items.keys(); en.hasMoreElements();)
itemList.append((String)en.nextElement(), null);
display.setCurrent(itemList);
}

//Si pausa externa (ej: llamada), paramos la reproducción del Player


public void pauseApp()
{
try {
if(player != null)
player.stop();
}
catch(MediaException e) {
System.out.println("EXCEPCIÓN EN pauseApp():" + e.toString());
}
}

//Al salir del MIDlet cerramos el Player


public void destroyApp(boolean unconditional)
{
System.out.println("SALIDA DEL MIDLET AUDIO POR destroyApp()");
if(player != null)
player.close();
}

//-------------------------------------------------------------------------------------------------------------------------------------------------------
// Manejador de eventos relacionados con la interface. Según la pantalla en la que estemos (la

- 102 -
//de la lista o la del formulario) actuaremos de una u otra forma
public void commandAction(Command c, Displayable d)
{
try{
//PANTALLA INICIAL: LISTA
if(d == itemList) {
//Se ha pulsado Volver
if(c == volver) {
// Cerramos Reproductor
if( player != null ) { player.close(); player = null; }
//Destruimos el MIDlet y notificamos al dispositivo su destrucción, para que nos vuelva a
//presentar la lista con los elementos de la suite (esa pantalla inicial queda en manos del dispositivo)
destroyApp( true );
notifyDestroyed();
}
//Se ha pulsado Reproducir
else if(c == reproducir) {
// Lanzamos la tarea que establece la conexión y reproduce el media obtenido
t = new Thread( this );
t.start();
//Cambiamos el comando de Reproducir por el de Pausar
form.removeCommand(reproducir);
form.addCommand(pausar);
}

}
//SEGUNDA PANTALLA: REPRODUCCIÓN
else if(d == form) {
//Se ha pulsado Volver mientras se está en la pantalla de Reproducción
if(c == volver) {
//Cerramos el Player y volvemos a presentar la pantalla inicial. Reponemos comandos
player.close();
display.setCurrent(itemList);
form.removeCommand(reproducir);
form.addCommand(pausar);

}
//Se ha pulsado Reproducir. Vendremos de un Pause previo
else if(c == reproducir) {
// Volvemos a reproducir el media desde el momento en el que se quedó, reponemos comandos

- 103 -
player.start();
form.removeCommand(reproducir);
form.addCommand(pausar);
}
//Se ha pulsado Pausar
else if(c == pausar) {
//Paramos el Player. Supondrá una pausa en la reproducción, no un cierre. Reponemos comandos
player.stop();
form.removeCommand(pausar);
form.addCommand(reproducir);
}
}
}
catch(Exception e) {
System.out.println("EXCEPCIÓN EN commandAction():" + e);
}
}

//-------------------------------------------------------------------------------------------------------------------------------------------------------
//Método run del hilo abierto para la comunicación HTTP que obtiene el media
public void run()
{
try{
//Presentamos una espera mientras se obtiene el elemento a reproducir
Alert alert = new Alert("Cargando media. Espere, por favor...");
alert.setTimeout(Alert.FOREVER);
display.setCurrent(alert);

//Obtenemos del conjunto la URL a conectar, sabido el elemento seleccionado en la lista


String urlSelec = (String)items.get(itemList.getString(itemList.getSelectedIndex()));
System.out.println("URL A REPRODUCIR:" + urlSelec);
player = Manager.createPlayer(urlSelec);

//Añadimos un escuchador para recoger cuando empieza, se pausa o se cierra la reproducción


player.addPlayerListener(this);

//Separamos el ciclo de vida del Player:


System.out.println("ESTADO UNREALIZED");
player.realize();
System.out.println("ESTADO REALIZED");

- 104 -
player.prefetch();
System.out.println("ESTADO PREFETCHED");
player.start();
System.out.println("ESTADO STARTED");
}
catch (Exception e){
System.out.println("EXCEPCIÓN EN run():" + e);
}
}

//-------------------------------------------------------------------------------------------------------------------------------------------------------
// Manejador de eventos relacionados con el reproductor
public void playerUpdate(Player player, String event, Object eventData)
{
try{
//Si el Player ha comenzado su reproducción (inicial o tras pausa), mostramos una nota negra
if( event.equals(PlayerListener.STARTED) ) {
//Limpiamos el formulario
form.deleteAll();
//Creamos una imagen de nota negra (activa) y la mostramos en el formulario
Image imagen = Image.createImage(getClass().getResourceAsStream("nota.png"));
ImageItem imagenNota = new ImageItem("REPRODUCIENDO...", imagen,
ImageItem.LAYOUT_CENTER,"Audio", Item.PLAIN);
form.append(imagenNota);
display.setCurrent(form);
}
//Si el Player se ha pausado, mostramos una nota gris
else if(event.equals(PlayerListener.STOPPED)) {
//Limpiamos el formulario
form.deleteAll();
//Creamos una imagen de nota gris (desactiva) y la mostramos en el formulario
Image imagen = Image.createImage(getClass().getResourceAsStream("notaGris.png"));
ImageItem imagenNota = new ImageItem("PAUSA...", imagen,
ImageItem.LAYOUT_CENTER,"Audio", Item.PLAIN);
form.append(imagenNota);
display.setCurrent(form);
}
//Si el Player se cierra, limpiamos el formulario
else if(event.equals(PlayerListener.CLOSED)) {
form.deleteAll();

- 105 -
}
}
catch(Exception e){
System.out.println("EXCEPCIÓN en playerUpdate()" + e.toString());
}
}

}//fín clase MEDIAEjemploMIDlet2.java

- 106 -
MEDIAEjemploMIDlet3.java

import javax.microedition.lcdui.*;
import javax.microedition.media.*;
import javax.microedition.media.control.*;
import javax.microedition.midlet.*;
import java.io.IOException;
import java.util.*;

//Clase del tercer MIDlet, el cual reproducirá vídeo, existentes en el directorio RES del JAR.
//Formatos soportados por el WTK 2.2, MPEG y GIF animado.
//Usaremos un createPlayer(InputStream stream, String type)
public class MEDIAEjemploMIDlet3 extends MIDlet implements CommandListener,PlayerListener {

// Pantalla del dispositivo, lista y Formulario


private Display display;
private List itemList;
private Form form;

//Comandos disponibles en la interface. Asociaremos a la pantalla con la Lista el Reproducir


//y el Volver, y una vez dentro de la reproducción asociaremos a la pantalla con el Form
//de la reproducción los comandos Reproducir y el Pausar/Reproducir.
private Command volver;
private Command pausar;
private Command reproducir;

//TADs para elementos de la lista


private Hashtable items;
private Hashtable itemsInfo;

//Reproductor
private Player player;

//Controlador de Vídeo asociado al player. Propio de MMA


private VideoControl vc;

//-------------------------------------------------------------------------------------------------------------------------------------------------------
//Constructor del MIDlet. En él hacemos las inicializaciones previas al arranque del MIDlet
//mediante el startup() posterior.

- 107 -
public MEDIAEjemploMIDlet3()
{
//Damos cuerpo a los comandos de la interface
volver = new Command("Volver", Command.STOP, 1);
pausar = new Command("Pausar", Command.ITEM, 1);
reproducir = new Command("Reproducir", Command.ITEM, 1);
//Damos cuerpo al formulario donde presentaremos una imagen mientras se reproduce el media
form = new Form("Reproduciendo archivo de VÍDEO");
//Damos cuerpo a la lista donde presentaremos los elementos de VÍDEO a seleccionar
itemList = new List("Selección de archivo de VÍDEO", List.IMPLICIT);
//Cargamos un conjunto con el que acceder mediante el nombre seleccionado en la lista
//a los ficheros a reproducir y a sus tipos MIME
items = new Hashtable();
itemsInfo = new Hashtable();
System.out.println("MIDlet VÍDEO CONSTRUIDO");
}

//-------------------------------------------------------------------------------------------------------------------------------------------------------
//Ciclo de vida del MIDlet
//Al comenzar, presentamos la lista de selección de VÍDEO y esperamos un evento de comando
public void startApp() {
System.out.println("COMIENZO DEL MIDLET VÍDEO POR startup()");

//Capturamos la pantalla del dispositivo


display = Display.getDisplay(this);

//Asociamos al formulario los comandos activos mientras él esté en pantalla


form.addCommand(volver);
form.addCommand(pausar);
form.setCommandListener(this);

//Asociamos a la lista los comandos activos mientras ella esté en pantalla


itemList.addCommand(volver);
itemList.addCommand(reproducir);
itemList.setCommandListener(this);

//Cargamos el conjunto del nombre de los archivos y sus tipos MIME. En esta ocasión, los
//buscaremos en el directorio de recursos (res) del propio JAR de la suite
items.put("Ejemplo GIF ANIMADO vía JAR", "file://ejemplo31.gif");
itemsInfo.put("Ejemplo GIF ANIMADO vía JAR", "image/gif");

- 108 -
items.put("Ejemplo MPEG vía JAR", "file://ejemplo32.mpeg");
itemsInfo.put("Ejemplo MPEG vía JAR", "video/mpeg");

//Cargamos la lista con el conjunto de nombres y la presentamos en pantalla. Así comenzamos a


//interactuar con el usuario
for(Enumeration en = items.keys(); en.hasMoreElements();)
itemList.append((String)en.nextElement(), null);
display.setCurrent(itemList);
}

//Si pausa externa (ej: llamada), paramos la reproducción del Player


public void pauseApp() {
try {
if(player != null)
player.stop();
}
catch(MediaException e) {
System.out.println("EXCEPCIÓN EN pauseApp():" + e.toString());
}
}

//Al salir del MIDlet cerramos el Player


public void destroyApp(boolean unconditional) {
System.out.println("SALIDA DEL MIDLET VÍDEO POR destroyApp()");
if(player != null)
player.close();
}

//-------------------------------------------------------------------------------------------------------------------------------------------------------
//Con este método accedemos al fichero seleccionado y lo reproducimos
private void reproducirVideo(String locator, String key) throws Exception {
try{
//Presentamos una espera mientras se obtiene el elemento a reproducir
Alert alert = new Alert("Cargando media. Espere, por favor...");
alert.setTimeout(Alert.FOREVER);
display.setCurrent(alert);

//Extraemos el nombre del fichero a reproducir


String fich = locator.substring(locator.indexOf("file://") + 6, locator.length());
System.out.println("FICHERO A REPRODUCIR:" + fich);

- 109 -
//Creamos el Player con ese nombre y el tipo MIME que extraemos del conjunto itemsInfo
player = Manager.createPlayer(getClass().getResourceAsStream(fich), (String)itemsInfo.get(key));

//Añadimos un escuchador para recoger cuando empieza, se pausa o se cierra la reproducción


player.addPlayerListener(this);

//Fijamos la reproducción a bucle infinito


player.setLoopCount(-1);

//Separamos el ciclo de vida del Player:


System.out.println("ESTADO UNREALIZED");
player.realize();
System.out.println("ESTADO REALIZED");
player.prefetch();
System.out.println("ESTADO PREFETCHED");
player.start();
System.out.println("ESTADO STARTED");
}
catch (Exception e) {
System.out.println("EXCEPCIÓN EN reproducirVideo():" + e);
}
}

//-------------------------------------------------------------------------------------------------------------------------------------------------------
// Manejador de eventos relacionados con la interface. Según la pantalla en la que estemos (la
//de la lista o la del formulario) actuaremos de una u otra forma
public void commandAction(Command c, Displayable d) {
try{
//PANTALLA INICIAL: LISTA
if(d == itemList) {
//Se ha pulsado Volver
if(c == volver) {
// Cerramos Reproductor
if( player != null ) { player.close(); player = null; }
//Destruimos el MIDlet y notificamos al dispositivo su destrucción, para que nos vuelva a
//presentar la lista con los elementos de la suite (esa pantalla inicial queda en manos del dispositivo)
destroyApp( true );
notifyDestroyed();
}
//Se ha pulsado Reproducir

- 110 -
else if(c == reproducir) {
//Lanzamos la reproducción del vídeo seleccionado
String key = ((List)d).getString(((List)d).getSelectedIndex());
reproducirVideo((String)items.get(key), key);
//Cambiamos el comando de Reproducir por el de Pausar
form.removeCommand(reproducir);
form.addCommand(pausar);
}
}
//SEGUNDA PANTALLA: REPRODUCCIÓN
else if(d == form) {
//Se ha pulsado Volver mientras se está en la pantalla de Reproducción
if(c == volver) {
//Cerramos el Player y volvemos a presentar la pantalla inicial. Reponemos comandos
player.close();
display.setCurrent(itemList);
form.removeCommand(reproducir);
form.addCommand(pausar);

}
//Se ha pulsado Reproducir. Vendremos de un Pause previo
else if(c == reproducir) {
// Volvemos a reproducir el media desde el momento en el que se quedó, reponemos comandos
player.start();
form.removeCommand(reproducir);
form.addCommand(pausar);
}
//Se ha pulsado Pausar
else if(c == pausar) {
//Paramos el Player. Supondrá una pausa en la reproducción, no un cierre. Reponemos comandos
player.stop();
form.removeCommand(pausar);
form.addCommand(reproducir);
}
}
}
catch(Exception e) {
System.out.println("EXCEPCIÓN EN commandAction():" + e);
}
}

- 111 -
//--------------------------------------------------------------------------------------------------------------------------------------------------------
// Manejador de eventos relacionados con el reproductor
public void playerUpdate(Player player, String event, Object eventData) {
try {
//Sólo si es la primera vez que llega el evento STARTED ejecutamos lo siguiente. Esto
//lo tenemos gracias a eventData, el cual si el evento es STARTED nos proporciona
//el media-time en el cual se encontraba el Player al lanzarse el evento STARTED
Long inicioMedia = new Long(0);
if(event.equals(PlayerListener.STARTED) && inicioMedia.equals((Long)eventData)) {
//Vemos si nuestro Player es capaz de asociar un control de vídeo, lo cual ocurrirá si
//el media a reproducir necesita este control (si es un vídeo)
vc = (VideoControl)player.getControl("VideoControl");
if(vc != null) {
//Si entramos, instanciamos un elemento imagen para el formulario y lo presentamos en él
Item eltoVideo = (Item)vc.initDisplayMode(vc.USE_GUI_PRIMITIVE, null);
form.append(eltoVideo);
}
display.setCurrent(form);

}
//Si se cierra el Player, cerramos su control asociado y limpiamos formulario
else if(event.equals(PlayerListener.CLOSED)) {
vc = null;
form.deleteAll();
}

}
catch(Exception e) {
System.out.println("EXCEPCIÓN en playerUpdate()" + e.toString());
}
}

}//fín clase MEDIAEjemploMIDlet3.java

- 112 -
- 113 -
RECUERDE

− El API Multimedia nos permitirá introducir elementos multimedia en nuestras aplicaciones J2ME.
En MIDP 2.0 se introducen, para ello, los elementos pertenecientes a los paquetes
javax.microedition.media y javax.microedition.media.control.

− Para apoyar el API ofrecido por MIDP 2.0 aparece un paquete opcional MMA especificado en la
JSR 135, el cual ampliará los dos paquetes media de MIDP y añadirá uno nuevo
(javax.microedition.media.protocol), para permitir al programador dar cabida a tipos de
formatos y protocolos no soportados.

− El contenido multimedia a reproducir se recogerá localmente (del RMS o del directorio de


recursos del propio JAR) o por conexión en red con otra máquina que lo ofrezca. Con él se dará
cuerpo a un Player, instanciado por medio de la clase Manager, el cual se controlará por medio
de elementos Control.

− Un Player representa un reproductor con el que presentar el contenido multimedia al usuario.


Este ente siempre se encontrará en uno de los siguientes estados: UNREALIZED, REALIZED,
PREFETCHED, STARTED o CLOSED. Estos estados marcan el ciclo de vida del reproductor.

− La especificación MMA 1.1 aporta a MIDP2.0 las principales mejoras:

• Controles para vídeo y gráficos.


• Almacenar elementos multimedia del ambiente.
• Reproducción simultánea de varios Players.
• Definición "a medida" de protocolos y formatos javax.microedition.media.protocol
• Ampliación de Manager y paquete javax.microedition.media.control.

- 114 -
Desarrollo
de juegos
Tema 4
con
J2ME

4.1. INTRODUCCIÓN............................................................................................................................... 117


4.1.1. Características de un juego interactivo........................................................................ 117
4.1.2. Tipos de juegos interactivos ......................................................................................... 120
4.1.3. Conceptos generales de programación de juegos...................................................... 122
4.1.4. Utilizando lo estudiado: RMS, HTTP y MEDIA.............................................................. 126
4.2. ELEMENTOS DEL API IMPLICADOS.............................................................................................. 127
4.2.1. La API de interface de bajo nivel................................................................................... 127
4.2.2. Paquete javax.microedition.lcdui.game........................................................................ 127
4.2.2.1. abstract GameCanvas extends Canvas ............................................................ 128
4.2.2.2. abstract Layer extends Object ........................................................................... 130
4.2.2.3. LayerManager extends Object ........................................................................... 132
4.2.2.4. Sprite extends Layer ........................................................................................... 134
4.2.2.5. TiledLayer extends Layer ................................................................................... 141
4.3. EJEMPLO DE JUEGO INTERACTIVO ............................................................................................. 145

- 115 -
4.1. INTRODUCCIÓN

Para finalizar el curso, estudiaremos en el presente capítulo el desarrollo de juegos en J2ME. En la


actualidad, el campo más explotado en la producción J2ME es el de la creación de juegos interactivos,
convirtiendo con ello a los dispositivos móviles MIDP en elementos de ocio además de simples
dispositivos de comunicación. Asimismo, veremos como la especificación 2.0 de MIDP ha hecho de esta
labor una sencilla tarea.

4.1.1. Características de un juego interactivo

Por juego interactivo se entiende toda aplicación orientada al ocio, con una importante carga gráfica y
multimedia, y que responde activamente a la interacción del usuario. La creación de estas aplicaciones
estará, por lo general, marcada por la plataforma a la que se orienta su uso, sea PC, consola o dispositivo
móvil. Dependiendo de la potencia tecnológica de cada plataforma, el juego podrá explotar unas
características técnicas u otras.

Existen distintas características a tener en cuenta a la hora de desarrollar un juego interactivo. Algunas
de ellas son, por ejemplo:

− Gráficos.

Los gráficos nos ofrecen la forma de ver el juego y, con ello, la principal característica de su
comunicación con el usuario. La calidad visual con la que se aprecian los elementos en la
pantalla es primordial para mantener la satisfacción con el producto que se está utilizando.

En los dispositivos móviles esta calidad está cada día más asegurada, aunque aún no puede
compararse con la dada por tarjetas gráficas de más capacidad, utilizadas en otros sistemas.
Quizás intentando asumir esto y haciendo nuestros juegos más sencillos, sea suficiente para que
la gráfica a ofrecer sea inmejorable en ese juego concreto (caso de juegos tipo tetris, por
ejemplo). En el apartado "mercado" valoraremos esto.

Por otro lado, aunque ya existen dispositivos MIDP con capacidad de ofrecer juegos en 3D
(potenciados con la JSR 184), aún debemos limitarnos a la bidimensionalidad si deseamos llegar
al máximo número de usuarios.

- 117 -
− Jugabilidad.

La jugabilidad es el grado de adicción que somos capaces de provocar en el usuario por medio
de nuestro juego. El control de la interface es una de las facetas más importantes para este
concepto. La forma en que interactuamos con el juego debe ser fácil e intuitiva para que el
aprendizaje del uso de éste no sea costoso y el juego consiga así llegar al usuario rápidamente.
El teclado del dispositivo móvil generalmente será la herramienta con la que contamos para esta
interacción; estando las teclas válidas para acciones de juego, limitadas a las estrictamente
necesarias.

La jugabilidad también estará fuertemente afectada por el grado de comprensión del usuario
de la idea del juego. Hay que evitar que el usuario se pierda en detalles o en conceptos
demasiado complejos que le impidan ver con claridad su rol, el objetivo que debe cumplir. Si el
usuario tiene claro qué debe hacer y qué debe conseguir en el juego, le será mucho más fácil su
inmersión en éste.

Por último, también relacionada está la capacidad de la lógica de la aplicación de "presentarnos


batalla", esto es, el nivel de dificultad que ofrecerá durante la ejecución del juego. Éste no debe
ser ni muy bajo (el usuario puede sentirse aburrido, infravalorado) ni muy alto (que no sea capaz
de resolver una situación repetidamente y llegue a desechar el juego). Para capacitar a la
aplicación de estrategias con que ganarnos la partida, existen diversas tendencias en inteligencia
artificial entre las cuales despuntan las redes neuronales, los algoritmos de búsqueda en grafos
o árboles de opciones posibles, y los sistemas basados en reglas y hechos.

− Multimedia.

En general, la calidad de sonido ha evolucionado paralelamente a los gráficos, siendo ambos


aspectos los decisivos a la hora de crear el contexto del juego en sí. Como decíamos en el
capítulo anterior, los usuarios desean ver y también escuchar lo que está ocurriendo en la
pantalla del dispositivo debiendo, para obtener lo segundo, crear en el juego un contexto sonoro
con el que facilitarle su inmersión en la aplicación.

En los juegos J2ME podremos utilizar una música ambiental para mantener la tensión, provocar
sensaciones en el jugador o simplemente marcar la diferencia entre una y otra situación en su
recorrido por el juego. Por otro lado, también podremos valernos de la emisión de ciertos

- 118 -
sonidos puntuales y vibraciones del dispositivo con los que avisar al usuario de ciertos
eventos o remarcar alguna acción importante que se haya producido.

Asimismo, la emisión de un vídeo de presentación para cada fase del juego, por ejemplo,
también nos será de gran ayuda para introducir al usuario en el objetivo del juego y marcarle el
rol que representa dentro de él. Aunque quizás todo esto no sea necesario en nuestro juego,
tendremos capacidad para ello en MIDP 2.0.

− Mercado.

El mercado potencial de un juego para móvil es mucho más general que el de juegos de PC o
consola, ya que prácticamente todos tenemos ya un móvil. Éste es un público amplio y diverso,
el cual, aunque no esté acostumbrado a utilizar juegos en sistemas de sobremesa, quizás sí
disponga de momentos de espera en su día en los cuales recurrir al ocio que puede
proporcionarles el dispositivo móvil que siempre lleva encima. Un juego como el famoso "Tetris"
es un ejemplo perfecto de éxito en dispositivos móviles. Cuatro simples teclas para controlarlo y
un objetivo perfectamente definido, sencillo y con alta jugabilidad.

Con esto en mente, deberíamos plantearnos el grado de sencillez (en términos generales:
gráfica, control, objetivo, etc.) a aplicar hoy día a un juego MIDP, ya que si alguien realmente
quiere jugar a un juego complejo y potente recurrirá a un PC, consola o sistemas de juego
portables no MIDP (PSP, N-GAGE, etc.).

Aunque todo es cuestión de gustos, la experiencia nos dice que bajo un dispositivo móvil cuya
finalidad es distinta a la del ocio y cuyas restricciones en gráfica, control y sonido son patentes
frente a una consola portable por ejemplo, si elaboramos algo complicado, debemos explicarlo
muy bien y, por supuesto, tiene que manejarse de forma muy sencilla, ya que la mayoría de
usuarios de un móvil no van a perder demasiado tiempo intentando comprender cuál es el
objetivo del juego o cómo se controla éste. En general, la imaginación nos será mucho más útil
que la técnica y en el caso del desarrollo J2ME, mucho más.

- 119 -
4.1.2. Tipos de juegos interactivos

Otra cuestión a abarcar antes de centrarnos en los conceptos técnicos del desarrollo de juegos en J2ME
es los tipos de juegos que podremos generar. Una división común es la siguiente:

− VideoAventura

En los juegos de videoAventura se desarrolla una historia donde el usuario tiene que ir
encontrando pistas que descubrirán misterios y avances en la historia. En este tipo de juegos, los
guiones y diálogos deben estar muy logrados para que la historia mantenga la atención del
usuario. Si se tiene en mente la sencillez anterior y se consigue ofrecer la historia de forma
llamativa, son factibles de llevar a MIDP.

Ejemplos: Day of Tentacle, Monkey Island...

− Arcade

Los juegos arcade fueron los primeros que aparecieron en el mercado, respondiendo a una
estructura fundamentada en actividades de mucha destreza, que permiten al usuario recorrer
distintas pantallas en diferentes niveles. La rapidez en este tipo de juegos es el elemento más
importante, incluso más que la propia estrategia del juego.

En esta división, los arcade engloban tanto a los típicos juegos de plataformas, muy orientados a
dispositivos móviles; como a los de acción en primera persona, los cuales aún no entran
demasiado bien en MIDP.

Ejemplos: Mario Bros, Doom...

− Deportivos

Recrean algún tipo de deporte, requiriendo habilidad, rapidez y precisión. Una de las facetas más
importantes de estos juegos es la jugabilidad. El usuario debe tener control total sobre el
personaje que maneje. Además, también tiene una gran importancia la calidad gráfica,
digitalizando deportistas y recreándolos de una manera muy real.

Ejemplos: Decathlon, Pro soccer...

- 120 -
− Estrategia.

El usuario de juegos estratégicos necesita coordinar acciones y actuar con el fin de conseguir
una finalidad concreta. Ofrecen al jugador la posibilidad de aumentar su capacidad de reflexión
para así conseguir un objetivo propuesto. La mayoría de los juegos de estrategia permiten
manejar más de un personaje, hasta tropas de soldados.

Ejemplos: Comandos, SimCity...

− Rol.

En este tipo de juegos el usuario maneja un personaje, el cual se ha podido crear con unas
cualidades concretas, que va evolucionando durante el juego según las decisiones o caminos
que tome el usuario. Suelen ser juegos en los que el objetivo no es único, sino que hay varias
metas que se entrelazan. Estos juegos suponen muchas horas de juego, al igual que los de
estrategia.

Ejemplos: Diablo, Baldur´s Gate...

En los dos tipos de juegos anteriores se podría aprovechar la capacidad de juego online (estas
modalidades de juego están muy orientados a ello, juegos multijugador con un servidor HTTP
por detrás como organizador) que dispondremos fácilmente con un dispositivo móvil, siempre
que cumplamos ciertas normas de sencillez. Más adelante comentaremos de nuevo lo llamativo
de este tipo de comunicación.

− Simulación.

Los juegos de simulación sumergen al usuario en un mundo donde se reproduce algún tipo de
acción como, por ejemplo, pilotar un avión, conducir un coche de carreras, etc.

Cada vez se les intenta dar mayor realismo, lo que conlleva un tiempo de aprendizaje excesivo
por la complejidad de manejo de este tipo de juegos. Además, pensando en dispositivos móviles,
la gráfica que necesitan (si desean llegar a un cierto realismo) no será posible ofrecerla en un
dispositivo MIDP.

Ejemplos: Flight Simulator, Xpand Rally...

- 121 -
4.1.3. Conceptos generales de programación de juegos

Respecto al componente gráfico de un juego, la idea básica al montarlo es su separación en capas. Esto
nos ayudará a separar los gráficos para así delimitar bien qué elementos y cómo intervendrán en la lógica
del juego, y cómo se relacionarán unos con otros. Estas capas estarán superpuestas, siendo la de índice
menor la más cercana a los ojos del usuario y dejando ver o no el contenido de las capas más profundas
según nos convenga. De cada tipo podrán existir las que deseemos.

Una división en capas, por ejemplo, para un juego de perspectiva cenital (recordemos, por ejemplo, el
juego "1942", de este tipo de vista) es la siguiente:

− Capa de Techos y Nubes. En ella colocaremos los elementos más cercanos a la vista del
usuario, los cuales no serán ocultados jamás por otras capas. Serán los componentes gráficos
más externos de todo el conjunto de la gráfica del juego; elementos como nubes, techos,
pájaros, copas de árboles, etc.

En la siguiente figura observamos los elementos de la capa externa utilizada en el ejemplo que
aparece al final del capítulo. Por ahora nos basta con saber que todos los elementos individuales
a mostrar en toda capa serán dados a la aplicación en un único archivo de imagen, del cual
extraeremos estos elementos dividiendo la imagen convenientemente. Exceptuando la capa de
jugadores, todas las demás veremos que se implementan por medio de capas de tipo
TyledLayer. Más adelante, describiremos más detalladamente cómo usar estas imágenes en
J2ME.

- 122 -
− Capa de Jugadores. En ella colocaremos los elementos gráficos que representan a los
jugadores o avatares del juego. Estas capas en J2ME se implementarán por medio de capas
especiales (Sprite) adecuadas a la fuerte respuesta ante acciones del usuario y la variabilidad
de su apariencia que necesitarán estos componentes gráficos. Tendremos una capa de este tipo
por jugador que aparezca en la pantalla, ya sea éste controlado por el usuario o por el sistema.
Para cada una de ellas, daremos a la aplicación un archivo de imagen distinto.

− Capa de Obstáculos. Colocaremos aquí los elementos gráficos que aparecerán alrededor del
avatar, pudiendo colocarse por delante o por detrás de cada jugador. Usaremos estas capas
para plasmar piedras, paredes, objetos a recoger por el jugador, matorrales, etc., codificando
qué hacer cuando un avatar colisione con cada uno de estos elementos.

- 123 -
− Capa de Suelos y Fondos. Como capa más interna, la más alejada de los ojos del usuario,
colocaremos los elementos gráficos que aparecerán por debajo del avatar y los obstáculos, no
colisionando jamás con ellos y siempre colocados bajo ellos y la capa de techos y nubes. Aquí
representamos los suelos del juego, lagos, fondos, etc.

Respecto a la programación de un juego, un concepto común a todos ellos, sea bajo la tecnología que
sea, es el del BUCLE DE JUEGO (GameLoop). Desde que el usuario pulsa una tecla para realizar una
acción hasta que observa su resultado en la pantalla, la aplicación debe realizar distintas tareas. Todas
ellas aparecen generalmente en un bucle que itera mientras el juego no termine, tan rápido como el
sistema lo permita, comprobando constantemente la pulsación de las teclas del juego y efectuando una u
otra acción dependiendo de ello.

En la figura vemos un ejemplo de bucle de juego. Aunque todos son similares, no tienen por qué seguir
exactamente esta estructura. En ella, comprobamos los siguientes pasos:

1. Lectura de la entrada. La primera acción del bucle será leer los eventos de entrada que pueden
haberse producido desde la iteración anterior, por medio de joystick, ratón, teclado, etc.

- 124 -
2. Proceso de la entrada. Tras lo anterior, si hay acción se procesa, realizando lo que tengamos
dispuesto para ella: mover al avatar, realizar un disparo, etc.

3. Respuesta a la entrada. Ahora se realiza todo lo que provoca la acción y que no queda bajo el
control del usuario; movimientos o reacciones del entorno del avatar provocadas por la acción
anterior: explosiones provocadas por el anterior disparo del avatar, comprobar colisiones ante
movimientos del avatar, reacciones de los enemigos (movimientos, disparos, cambios de
apariencia), etc.

4. Otras tareas. Tras ello, se realizan tareas secundarias como son actualizar la apariencia de la
capa de fondo, activar sonidos o vibraciones, realizar trabajos de sincronización, etc.

5. Presentar resultado. Tras toda la lógica anterior, queda por fin presentarle en pantalla al
usuario el resultado de su interacción con el juego, volcando en pantalla toda la gráfica generada
con las acciones anteriores.

Al bucle entraremos tras inicializar los componentes necesarios para la ejecución del juego y saldremos
de él cuando se considere la partida terminada. Ya veremos en el ejemplo final como codificamos este
bucle en MIDP.

Con respecto a la tecnología que nos ocupa, J2ME, la creación de juegos para dispositivos móviles con
ella se remonta a los inicios de la programación MIDP, existiendo juegos para MIDP 1.0 muy
conseguidos, realizada su gráfica tan sólo con las herramientas que ofrece la API de creación de
interfaces de bajo nivel. Quizás el juego que deseemos implementar podría hacerse bajo MIDP 1.0, pero
es seguro que utilizando MIDP 2.0 su creación será mucho más fácil, rápida y optimizada.

Con esta idea como aportación primordial, apareció la especificación MIDP 2.0, ofreciendo al
desarrollador librerías para crear de forma sencilla interfaces gráficas más rápidas utilizando menos
recursos y con un tamaño menor del JAR a descargar. Además de esta API de juegos, dispondremos de
muchas otras herramientas que hemos ido estudiando en los capítulos anteriores y con las cuales
dotaremos a nuestros juegos de capacidades impresionantes.

- 125 -
4.1.4. Utilizando lo estudiado: RMS, HTTP y MEDIA

Los tres temas anteriores del curso, aunque han sido estudiados desde un punto de vista general, nos
serán de gran utilidad a la hora de crear un juego interactivo. Por supuesto, no quedan limitados al uso en
juegos, ya vimos que no es así, pero sin duda nos aportarán una funcionalidad extra totalmente deseable.

Algunos aspectos tratados en los temas anteriores que aprovecharemos para el desarrollo de juegos son
detallados a continuación:

− RMS. La capacidad de almacenamiento persistente de nuestros dispositivos MIDP puede ser


aprovechada en juegos de múltiples formas. Con el RMS, por ejemplo, podremos:

• Almacenar el estado en el que queda un juego al abandonarlo para volver más tarde a
la misma situación en que nos quedamos (típica acción "guardar la partida").

• Almacenar puntuaciones máximas en nuestro juego, para así compararnos a otros


usuarios que jueguen en nuestro dispositivo.

• Guardar datos que deseemos evitar trasladar en el JAR. Accediendo por HTTP a un
servidor nos los podemos traer para disponer de ellos una vez almacenados en el
dispositivo. Así, con una sola conexión traeríamos esa información para las futuras
ejecuciones del juego. HTTP nos servirá para eso y mucho más.

− HTTP. La idea de tener un sistema externo a nuestro servicio, al cual poder conectar para
recabar información y atacar lógica más compleja (siempre bajo el coste de tiempo y monetario
que conlleva) hace volar nuestra imaginación.

Podríamos acceder al servidor para obtener recursos pesados como vídeos de puesta en
escena, puntuaciones máximas de todo usuario dado de alta en el juego. No obstante, esto no
es todo, se abren las puertas a juegos online donde cada cierto tiempo accedamos al servidor
para traernos las acciones ejecutadas por otros usuarios y que éstas varíen nuestra situación
actual en el juego. Así, por ejemplo, podemos observar cómo afectará esto a juegos de
estrategia o rol, interviniendo en la misma historia múltiples usuarios, afectándose unos a otros.

− MEDIA. El tema 3 será, como ya adelantamos, un elemento casi imprescindible en el desarrollo


de juegos en J2ME. Todo lo visto en multimedia (sonidos, música y animaciones) nos ayudará

- 126 -
aquí, de la forma que ya hemos repetido anteriormente, a comunicarnos con el usuario de una
forma más potente.

Aunque no se vio en el tema anterior por no pertenecer directamente a la API multimedia,


relacionado en cierta forma con la salida que suponen los elementos multimedia, tenemos
disponible en el paquete javax.microeduition.lcdui un elemento muy útil en el desarrollo de
juegos: la vibración del dispositivo por medio del método Display.vibrate(int duration). Con ella
podremos alertar al usuario de ciertas acciones puntuales que le ocurran a su avatar.

4.2. ELEMENTOS DEL API IMPLICADOS

Pasamos a estudiar los elementos disponibles en el perfil MIDP 2.0, no estudiados hasta ahora en
nuestro manual, para la creación de juegos multimedia.

4.2.1. La API de interface de bajo nivel

Un juego interactivo, en términos abstractos, no es más que una interface de bajo nivel orientada al ocio
con ciertas particularidades.

Todos los elementos de dicha API, que consideramos en el tema 1, podrán y serán en algunos casos
imprescindibles para crear un juego. Así, debemos manejar con soltura los elementos del paquete
javax.microedition.lcdui correspondientes a bajo nivel como son: la clase Canvas (de la cual heredará la
GameCanvas base de la interface del juego), Graphics (que emplearemos para dibujar los contenidos),
Image (en la cual cargaremos las imágenes para formar cada capa), etc., para la comprensión total de lo
que estudiaremos a continuación.

4.2.2. El paquete javax.microedition.lcdui.game

Este paquete nace con MIDP 2.0 para hacer más sencilla la labor de crear un juego interactivo. En MIDP
1.0 no se tenía toda esta funcionalidad, haciendo del desarrollo de juegos una ardua tarea basada en los
elementos comentados de construcción de interfaces de bajo nivel.

- 127 -
Ahora dispondremos de cinco nuevos conceptos con los que nos moveremos:

− Un ente base donde dibujaremos los elementos del juego y capturaremos de forma óptima la
interacción del usuario por teclado (GameCanvas).

− Un organizador de las capas disponibles en el juego (LayerManager).

− Un ente abstracto representando una capa (Layer) y dos concreciones de éste para dar cuerpo
a capas de escenario (TiledLayer) y capas de jugadores (Sprite).

Estos conceptos se ven reflejados en las cinco clases que el paquete ofrece y que estudiaremos en los
apartados siguientes.

4.2.2.1. abstract GameCanvas extends Canvas

Como Canvas que es, representará la superficie de dibujo donde plasmaremos los componentes de
nuestro juego.

Esta clase provee los conceptos básicos para manejar la interface del juego, ya que, aparte de lo
heredado de Canvas (comandos, eventos, etc.), ofrece un buffer gráfico oculto asociado a la superficie de
dibujo y además funcionalidad para el acceso inmediato al estado de las teclas físicas del dispositivo
MIDP.

Con estas dos aportaciones es posible refrescar el contenido gráfico de la pantalla con el contenido del
buffer gráfico y responder a la interacción por teclado del usuario que capturamos de forma inmediata,
todo ello en el propio bucle del juego, sin necesitar de las tareas externas de repintado y control de
teclado que debíamos usar con Canvas si no dispusiéramos del API de juegos. Con esto, podremos
hacer que una única tarea o hilo se encargue de la ejecución del bucle de juego al completo (Game
Loop), ganando en velocidad de forma patente.

- 128 -
La clase GameCanvas presenta las siguientes constantes, todas ellas orientadas a un control del
teclado mucho más fino del que teníamos en Canvas:

CONSTANTE VALOR INDICADO


static int DOWN_PRESSED Presión de la tecla DOWN
static int FIRE_PRESSED Presión de la tecla FIRE
static int GAME_A_PRESSED Presión de la tecla GAME_A*
static int GAME_B_PRESSED Presión de la tecla GAME_B*
static int GAME_C_PRESSED Presión de la tecla GAME_C*
static int GAME_D_PRESSED Presión de la tecla GAME_D*
static int LEFT_PRESSED Presión de la tecla LEFT
static int RIGHT_PRESSED Presión de la tecla RIGHT
static int UP_PRESSED Presión de la tecla UP
(*) No tienen por qué tenerla todos los dispositivos.

Los siguientes son los métodos que oferta la clase, siendo el primero de ellos el constructor que ofrece:

− protected GameCanvas(boolean suppressKeyEvents): constructor de GameCanvas que


recibe como parámetro un booleano indicando si suprimimos el mecanismo de eventos natural
para las teclas de juego (los eventos para las demás teclas sí seguirán activos) mientras el
GameCanvas sea mostrado. Es decir, la supresión comienza cuando el método showNotify() es
llamado y termina con la invocación a hideNotify() (métodos heredados de Canvas).

De este modo, nos limitaríamos al uso del método getKeyStates() para tratar con el teclado,
evitando llamadas ya innecesarias del sistema a métodos como keyPressed(), keyRepeated() o
keyReleased() y su tratamiento por nuestra parte. El buffer oculto antes comentado es también
creado al invocar a este constructor formado por píxeles blancos por defecto.

− void flushGraphics(). Refresca la pantalla con el contenido del buffer oculto, sin variar éste. El
tamaño del área refrescada será el del GameCanvas, si éste es visible actualmente (si no, no se
hace nada).

− void flushGraphics(int x, int y, int width, int height). Refresca la pantalla con el contenido del
buffer oculto sin variar éste, en este caso, sólo con la porción delimitada por los parámetros de
entrada. El contenido actualmente visible del GameCanvas en ese rectángulo será machacado

- 129 -
con el correspondiente contenido del buffer oculto. Si la región especificada excede los límites
del GameCanvas, sólo el área válida en la intersección será refrescada.

− protected Graphics getGraphics(). Obtiene un objeto Graphics que representa el contenido del
buffer oculto asociado al GameCanvas. En este Graphics podremos dibujar píxeles, Layers,
Sprites, etc., los cuales no serán presentados en pantalla hasta que un flushGraphics() sea
invocado. Un nuevo objeto Graphics es devuelto cada vez que este método es llamado, aunque
para la misma instancia de GameCanvas, los objetos Graphics devueltos siempre representarán
el mismo buffer oculto. Debemos recordar que el refresco no varía el contenido de este buffer.

− int getKeyStates(). Nos permite saber de una forma óptima e inmediata qué tecla ha sido
pulsada sobre el GameCanvas actual, siempre que hayan sido pulsadas estando éste visible.

El método devuelve un int en el cual cada uno de sus bits representa una tecla del dispositivo.
Por ello, usando el operador AND lógico con cada una de las teclas que deseemos comprobar,
sabremos si ésta ha sido pulsada o no. Un bit de estos será 1 si la tecla que representa está
actualmente pulsada o ha sido pulsada al menos una vez desde la última llamada a este método.
Lo usaremos así, por ejemplo:

int keyState = getKeyStates();


if ( ( keyState & LEFT_KEY ) != 0 ) positionX--;
else if ( ( keyState & RIGHT_KEY ) != 0 ) positionX++;

− void paint(Graphics g). Utiliza el objeto Graphics parámetro para pintar en pantalla el contenido
del buffer oculto del GameCanvas actual, en la posición (0,0). Estará sujeto al área de Clipping
y el origen de translación del objeto Graphics.

4.2.2.2. abstract Layer extends Object

Esta clase representa un elemento abstracto susceptible de ser dibujado en pantalla, dado un tamaño y
una posición determinada. Estos objetos implementan lo que anteriormente denominábamos capas, por
medio de las cuales podremos superponer, mover y ocultar o mostrar ciertos elementos gráficos en el
juego.

- 130 -
La posición de una capa es siempre relativa al sistema de coordenadas del objeto Graphics pasado al
método paint() del Layer. Denominaremos a partir de ahora a este sistema "sistema de coordenadas
global". La posición inicial de una capa será la (0,0).

La clase Layer sólo oferta métodos, los cuales detallamos a continuación:

− int getHeight(). Altura en píxeles de la capa.

− int getWidth(). Anchura en píxeles de la capa.

− int getX(). Posición en el eje X de la capa, relativa al sistema de coordenadas global.

− int getY(). Posición en Y de la capa (recordemos que Y avanza hacia abajo), relativa al sistema
de coordenadas global.

− boolean isVisible(). Devuelve un booleano indicando si la capa está marcada como visible o
invisible.

− void move(int dx, int dy). Mueve la capa la distancia determinada por los parámetros de
entrada. Para X, si el valor dado es negativo la moverá a la izquierda; para Y, si es negativo la
moverá hacia arriba.

− abstract void paint(Graphics g). Pinta el Layer usando el Graphics parámetro. Al ser un
método abstracto, las clases hijas de Layer deben sobrescribirlo. Ellas se ocuparán, por ejemplo,
de comprobar si la capa es visible, no debiendo dibujar nada en ella si no lo es.

− void setPosition(int x, int y). Modifica la posición actual del Layer, siendo la posición por
defecto la (0,0). Los valores dados serán asignados a la posición, relativa al sistema de
coordenadas global, de la esquina superior izquierda de la capa.

− void setVisible(boolean visible). Modifica la visibilidad del Layer actual.

- 131 -
4.2.2.3. LayerManager extends Object

Esta clase representará un "organizador de capas" y nos servirá para manejar las diferentes capas o
Layers con las que compondremos la gráfica de nuestro juego. Con él, podremos elegir la región a dibujar
(ventana de visión) del conjunto de capas superpuestas que nos interese en cada momento, en el orden
de capas definido.

Este orden en profundidad es dado por un índice (z-orden) en el cual el valor 0 es asignado a la capa más
cercana al usuario (la capa de techos y nubes que comentábamos) y así avanza hasta llegar a la más
alejada de los ojos del usuario (la capa de suelos y fondos). El índice siempre es correlativo: si una capa
se elimina, los índices se reasignan de forma que no existan saltos en la numeración de las capas.

La región visible de las capas viene marcada por una ventana de visión que definiremos con
setViewWindow(). Cambiando la posición de ésta, conseguiremos efectos de scroll y cambios en la
panorámica del usuario (si movemos la ventana a la derecha, provocaremos un efecto de movimiento del
conjunto gráfico visible a la izquierda). El tamaño del área visible será ajustado a la capacidad de la
pantalla de cada dispositivo concreto.

Veamos el ejemplo que aparece en la especificación MIDP 2.0 de SUN. En él, la ventana se define en el
punto (52, 11) del sistema de coordenadas del LayerManager, de tamaño 85 x 85 píxeles. Las dos capas
existentes, colocadas respectivamente en los puntos (75, 25) y (18, 37), serán contempladas a través de
la ventana definida.

- 132 -
Todos los elementos de esta clase son métodos, con un solo constructor:

− LayerManager(). Constructor de la clase, el cual crea un nuevo LayerManager vacío.

− void append(Layer l). Añade la capa parámetro al organizador, asignándole automáticamente


un índice superior al de todas las capas existentes hasta el momento. Si la capa ya existe será
borrada e insertada de nuevo para que el índice quede reasignado convenientemente.

− Layer getLayerAt(int index). Obtiene la capa cuyo índice coincide con el pasado como
parámetro. Éste debe ser mayor o igual a 0 y menor estricto que el número de las capas
existentes en el organizador.

− int getSize(). Devuelve el número de capas del LayerManager.

− void insert(Layer l, int index). Añade la capa parámetro al organizador asignándole el índice
dado como segundo parámetro, no debiendo ser este índice ni menor que 0 ni mayor que el
número de capas ya existentes (si hay n capas, el índice de la última de ellas será n-1). Si la
capa ya existe será borrada e insertada de nuevo.

− void paint(Graphics g, int x, int y). Dibuja el conjunto de capas que alberga el LayerManager
utilizando el Graphics parámetro. Este contenido será mostrado a través de la ventana de visión
mostrando las capas en orden ascendente de su z-orden, siempre que la capa sea marcada
visible y al menos una parte de ella caiga dentro de la ventana.

- 133 -
Las coordenadas pasadas como parámetro dan el punto donde fijamos la ventana de visión del
organizador, relativo al origen del objeto Graphics utilizado. Esto será útil si deseamos, por
ejemplo, dejar siempre invariable un marcador de extensión 17 píxeles en la parte superior del
juego, para lo cual mostraríamos la ventana a partir del punto (17, 17) como vemos en la anterior
figura. La traslación del objeto Graphics afectaría a su origen influyendo a su vez a la
presentación anterior, así como también lo hará a ésta, el área de clipping del Graphics: si no es
lo suficientemente grande sólo una parte de la ventana será dibujada en pantalla.

− void remove(Layer l). Elimina la capa dada como parámetro del LayerManager. Si ésta no
existe, no se hace nada.

− void setViewWindow(int x, int y, int width, int height). Fija la ventana por la que
contemplamos el conjunto de capas que alberga el LayerManager, permitiéndonos así controlar
qué región ofrecemos visible al usuario y qué zonas ocultamos del conjunto. Ésta será la región
que el método paint() anterior dibujará. Por defecto, se mostrará desde el punto (0,0) un ancho y
alto ambos dados por Integer.MAX_VALUE, es decir, toda la gráfica posible.

4.2.2.4. Sprite extends Layer

Con esta clase instanciamos un tipo de capa orientada al dibujo de los personajes del juego, al ser éstos
los elementos de la gráfica que van a necesitar de un control de movimiento y animación más potente.

Serán dibujados de forma animada gracias a la variación de sucesivas imágenes que representen
distintas formas de su apariencia (frames), las cuales además podremos trasladar, rotar, reflejar y mostrar
u ocultar a nuestro antojo.

Los frames primitivos que tendremos disponibles son suministrados por medio de una única imagen a
partir de la cual serán extraídos dividiendo ésta en elementos de igual tamaño, según un ancho y alto
determinado para ellos. Tras la división, los frames son insertados en una secuencia y numerados con un
índice único, dándole valor 0 al que ocupa la esquina superior izquierda y siguiendo la numeración
contando frames hacia la derecha y hacia abajo, entrando en la secuencia en el mismo orden. Así, las
tres imágenes originales que vemos en la figura producirían la misma secuencia de frames primitivos.

- 134 -
Ésta sería la secuencia generada por defecto al dividir la imagen. Tras ello, podrá ser variada recolocando
índices, repitiendo algunos o despreciando otros, hasta conseguir una secuencia de frames que animará
nuestro personaje de la forma que deseamos. Así, por un lado, tendremos una lista con los frames
primitivos y, por otro, la secuencia de frames que componen la animación del personaje, ambas
coincidiendo siempre y cuando no se varíe la secuencia generada por defecto.

Otro concepto interesante es el del píxel de referencia. Éste es definido como un punto del Sprite
(aunque puede definirse fuera de los límites de éste) antes de cualquier transformación y su función es
proporcionarnos un punto de referencia a la hora de trasladar, rotar o reflejar el Sprite.

Este píxel se define (defineReferencePixel()) relativo al sistema de coordenadas local al Sprite, tras lo
cual podrá servirnos para colocar la capa (al trasladar su píxel de referencia, trasladamos toda ella) en un
punto del sistema de coordenadas global usando setRefPixelPosition().

En la figura de ejemplo vemos como el píxel de referencia (punto rojo) es declarado en el punto (25, 3)
local al Sprite y, tras ello, es posicionado en el punto (48, 22) del objeto Graphics dibujador (el sistema
que llamamos global), provocando con ello el posicionamiento de la capa completa.

- 135 -
Las transformaciones que podamos hacerle a la capa se harán tomando como centro este píxel. Por este
motivo, tras ellas, la posición de éste no habrá variado. Más adelante veremos todo esto con detalle.

Las constantes que presenta esta clase son:

CONSTANTE INDICACIÓN
static int TRANS_MIRROR Reflejo o rotación de espejo sobre el eje Y para el Sprite.
static int TRANS_MIRROR_ROT180 Reflejo o rotación de espejo sobre el eje Y, tras la cual se aplica
una rotación de 180º en el sentido de las agujas del reloj.
static int TRANS_MIRROR_ROT270 Reflejo o rotación de espejo sobre el eje Y, tras la cual se aplica
una rotación de 270º en el sentido de las agujas del reloj.
static int TRANS_MIRROR_ROT90 Reflejo o rotación de espejo sobre el eje Y, tras la cual se aplica
una rotación de 90º en el sentido de las agujas del reloj.
static int TRANS_NONE No se aplica transformación alguna sobre el Sprite.
static int TRANS_ROT180 Rotación de 180º en el sentido de las agujas del reloj.
static int TRANS_ROT270 Rotación de 270º en el sentido de las agujas del reloj.
static int TRANS_ROT90 Rotación de 90º en el sentido de las agujas del reloj.

A continuación, vemos sus métodos, describiendo, primero, los tres constructores disponibles:

− Sprite(Image image). Primer constructor de la clase Sprite, el cual crea un sprite a partir de la
imagen parámetro, por defecto visible y colocada su esquina superior izquierda en el (0,0). Será
equivalente a usar Sprite(image, image.getWidth(), image.getHeight()). Así, en este caso, el
Sprite estará compuesto de un único frame, no dando opción a su animación por secuencia.

- 136 -
− Sprite(Image image, int frameWidth, int frameHeight). Segundo constructor, el cual crea un
Sprite a partir de un conjunto de imágenes (frames) obtenidas del Image origen. Éste lo
dividimos gracias a los parámetros frameWidth y frameHeight, obteniendo de esta división las
distintas subimágenes que darán cuerpo a cada frame, todas ellas de iguales dimensiones.

En este caso pues, un Sprite estará compuesto de múltiples elementos primitivos dando opción
así a animarlo utilizando una secuencia de frames; ya sea la dada por defecto o alguna definida
posteriormente por medio del método setFrameSequence(<nuevaSecuencia>). Por defecto, el
Sprite se crea visible y colocada su esquina superior izquierda en el (0,0).

Además, el ancho del objeto Image debe ser múltiplo entero de frameWidth y el alto, múltiplo
entero de frameHeight para que así el número de frames obtenido sea un entero correcto (si no,
será elevada una IllegalArgumentException). Como ya adelantamos, la secuencia de frames
coincide por defecto con la lista de los frames primitivos creada al efectuar la división de la
imagen original.

− Sprite(Sprite s). Tercer constructor de Sprite, constructor copia.

− boolean collidesWith(Image image, int x, int y, boolean pixelLevel). Pregunta si el Sprite


actual, siempre que éste sea visible, colisiona (llega a una posición donde se superpone) con la
imagen parámetro, colocada la esquina superior izquierda de ésta en las coordenadas dadas con
el segundo y tercer parámetro.

El cuarto parámetro pixelLevel indica si la colisión es calculada sólo teniendo en cuenta píxeles
opacos no transparentes (pixelLevel a true), tanto en la imagen como en el sprite,
comprobándose, no obstante, en cualquier caso, sólo los píxeles dentro del rectángulo de
colisión del Sprite, dado con defineCollisionRectangle().

Por último, decir que usar el cálculo de colisiones píxel a píxel tendrá un coste mayor que
simplemente calcularlas entre el rectángulo de colisión del sprite y los bordes de la imagen
(pixelLevel a false).

− boolean collidesWith(Sprite s, boolean pixelLevel). Pregunta si el Sprite actual colisiona con


el Sprite parámetro, para lo cual ambos deben ser visibles. El parámetro pixelLevel funciona
como ya explicamos en el método anterior.

- 137 -
− boolean collidesWith(TiledLayer t, boolean pixelLevel). Pregunta si el Sprite actual colisiona
con el TiledLayer parámetro, para lo cual ambos deben ser visibles. El parámetro pixelLevel
funciona como ya explicamos anteriormente.

− void defineCollisionRectangle(int x, int y, int width, int height). Con este método definimos
para el Sprite un rectángulo que lo represente a la hora del cálculo de colisiones, del cual
hablábamos en los métodos anteriores.

Será definido relativo a la esquina superior izquierda (antes de cualquier transformación) del
Sprite, localizado por defecto en el (0,0) y de iguales dimensiones al sprite. Este rectángulo
puede ser mayor o menor que su sprite asociado; así, si es mayor, los píxeles fuera de los
límites del sprite son considerados transparentes a la hora del cálculo de colisiones con
pixelLevel a true.

− void defineReferencePixel(int x, int y). Define la posición dada del píxel de referencia relativa
al sistema de coordenadas local del Sprite.

El píxel puede ser definido fuera del sprite y será el punto base a la hora de calcular las
transformaciones que vimos con las constantes iniciales. Por defecto, el píxel de referencia es el
(0,0): la esquina superior izquierda del Sprite.

Por último, decir que la llamada a este método no tiene efecto alguno sobre la posición del Sprite
respecto al sistema de coordenadas global.

− int getFrame(). Obtiene el índice del frame que actualmente da cuerpo al sprite, índice que lo
identifica en la secuencia de frames asociada al Sprite.

− int getFrameSequenceLength(). Alcanza el número de frames existentes en la secuencia


asociada al Sprite.

− int getRawFrameCount(). Consigue el número de frames primitivos. Éste es el número de


elementos originales que forman el Sprite, no la longitud de la secuencia de frames en la cual
pueden haber sido obviados algunos frames o repetidos otros. Estos dos valores sólo coincidirán
si la secuencia actual es la asignada por defecto.

- 138 -
− int getRefPixelX(). Obtiene el valor de X (en el sistema de coordenadas global) del píxel de
referencia actualmente definido.

− int getRefPixelY(). Obtiene el valor de Y (en el sistema de coordenadas global) del píxel de
referencia actualmente definido.

− void nextFrame(). Selecciona como activo el siguiente frame de la secuencia, sustituyendo al


actual. La secuencia es considerada circular, es decir, la llamada a este método cuando se está
en el último frame de la secuencia impondrá como activo al primero de ésta.

− void paint(Graphics g). Dibuja el Sprite (si éste es visible) utilizando para ello el Graphics
parámetro. La esquina superior izquierda del Sprite es dibujada en la posición actual de éste
relativa al sistema de coordenadas global, la cual puede obtenerse con los métodos Layer.getX()
y Layer.getY().

− void prevFrame(). Selecciona como activo el anterior frame de la secuencia, sustituyendo al


actual. La secuencia es considerada circular.

− void setFrame(int sequenceIndex). Selecciona como activo el frame de la secuencia señalado


por el índice parámetro, sustituyendo al actual.

− void setFrameSequence(int[] sequence). Fija una nueva secuencia para el Sprite, dada por un
array que contiene los índices a utilizar en ella, dejando intacta la lista de frames primitivos del
Sprite. Si se pasa un null se volverá a la secuencia por defecto, formada por los índices en la
lista de frames primitivos. El índice del frame actual es reiniciado a 0 al llamar a este método.

− void setImage(Image img, int frameWidth, int frameHeight). Recibiendo los mismos
parámetros que el segundo constructor estudiado, funciona de forma análoga a aquél pero con el
Sprite ya creado. Reemplazará así la lista de frames primitivos del sprite, debiendo tener en
cuenta ante este acto que:

• Si la nueva lista de frames primitivos es en número mayor o igual a la anterior, el índice


del frame actual no varía; si es menor se reinicia a 0.

• Si la nueva lista de frames primitivos es en número mayor o igual a la anterior y en


aquélla se había variado la secuencia de frames por defecto, la secuencia no variará. Si,

- 139 -
por el contrario, la secuencia era aún la dada por defecto, será reemplazada por la que
marque la división de la nueva imagen. No obstante, si la nueva lista de frames primitivos
es en número menor que la anterior lista, cualquier secuencia será descartada y
sustituida por la secuencia de frames por defecto que implicará la nueva imagen.

• Si el tamaño constante de los nuevos frames ha cambiado, el rectángulo de colisión es


recalculado a los nuevos valores por defecto que le correspondan.

• La posición del píxel de referencia asociado al Sprite no varía ante la llamada a este
método.

− void setRefPixelPosition(int x, int y). Al contrario que ocurría en el método


defineReferencePixel(), donde la posición respecto al sistema no variaba, este método actualiza
la posición del píxel de referencia relativa al sistema de coordenadas global, trasladando con ello
la capa Sprite. Así, en el primer caso, una llamada a Layer.getX() o Layer.getY() daba el mismo
valor antes y después de la llamada a defineReferencePixel(), sin embargo, aquí sí podrá variar.

− void setTransform(int transform). Aplica una transformación al Sprite para cambiar su


apariencia. Las transformaciones disponibles están dadas por medio de las constantes que
vimos al principio (rotaciones y reflejos), siendo pasada una de ellas a este método por medio del
parámetro de entrada transform.

Las transformaciones no son acumulativas; no podrán ser combinadas. La transformación


aplicada por defecto al sprite es TRANS_NONE (muestra el Sprite con su apariencia original).

Ante transformaciones de 90 ó 270 grados, los valores dados por Layer.getWidth() y


Layer.getHeight() se intercambiarán. El rectángulo de colisión también se adecuará a la
transformación, sin embargo, el píxel de referencia permanece invariable respecto al sistema de
coordenadas global, ya que se tomará éste como el centro de la transformación (para ello es
definido). Por lo tanto, los valores dados por getRefPixelX() y getRefPixelY() no varían por
transformaciones; los dados por Layer.getX() y Layer.getY() (posición de la capa) obviamente sí
podrán cambiar.

- 140 -
4.2.2.5. TiledLayer extends Layer

Estando la anterior clase orientada a la capa de personajes, la que estudiamos a continuación nos servirá
para las otras capas del juego. Con esta clase instanciamos un tipo de capa orientada al dibujo de
elementos gráficos que necesitarán menos movimiento y animación, aunque también podrán animarse de
forma menos potente.

Además, esta clase está orientada a la construcción de imágenes virtuales de grandes dimensiones
(pensemos en el escenario del juego, su fondo, etc., los cuales iremos mostrando por medio de scroll) a
partir de imágenes muy sencillas y de pequeño tamaño que repetiremos, animaremos, etc.

Para ello, se recogerán múltiples elementos gráficos a partir de una sola imagen como antes, la cual será
divida de forma análoga a un Sprite dando lugar a un conjunto de subimágenes de igual tamaño, según
un ancho y alto determinado. Por su finalidad, en este caso, serán denominados TILES, en lugar de
FRAMES como en Sprite. Estas subimágenes son numeradas con un índice único, dando valor 1 (en
Sprite comenzaban en 0) a la que ocupa la esquina superior izquierda, y siguiendo la numeración
contando hacia la derecha y abajo. Así, las tres imágenes originales que vemos en la figura producirían el
mismo conjunto de tiles.

- 141 -
Estos tiles así obtenidos son denominados estáticos y tendrán una imagen asociada desde la
instanciación del objeto TiledLayer. Más adelante, veremos que podrán ser variados con el método
setStaticTileSet(), de forma análoga a como variábamos los frames primitivos del Sprite con el método
setImage().

Además de los estáticos, podremos definir tiles animados para dar elementos gráficos que varíen su
apariencia con el tiempo. Un tile animado será un tile virtual, no adquirido a partir de la imagen original, al
que le asignaremos la gráfica de uno u otro tile estático según creamos conveniente. Con ello,
provocamos efectos simples de animación en el juego, por ejemplo, el movimiento del agua, el ondeo de
una bandera, el burbujeo de una copa de champán, etc.

Mientras que los tiles estáticos se numerarán con índices positivos correlativos; los animados lo harán
con índices negativos correlativos y un índice de valor 0 indicará inexistencia de contenido.

Estos índices que comentamos nos servirán para invocar un tile u otro para su presentación en pantalla.
Dicha presentación se define por medio de la estructura siguiente:

− Los tiles son insertados (sus índices en realidad, aunque visualmente presentemos una
estructura de imágenes repetidas) en lo que denominamos una malla. Esta última se define
como una tabla bidimensional de imágenes por filas y columnas que indexan celdas de igual
tamaño.

− El número de filas y columnas es dado al construir el TiledLayer.

− El tamaño gráfico del resultado de dibujar una celda corresponderá con el tamaño del tile que la
ocupa (recordemos que el tamaño es constante para todos los tiles de la capa).

- 142 -
Muchas celdas podrán contener al mismo tile, sin embargo, una celda no puede albergar varios tiles a la
vez, como podíamos comprobar en la figura anterior. Por defecto, al construir el TiledLayer le asignamos
valor 0 a todas las celdas de su malla asociada (transparencia), por lo cual, tras la construcción, debemos
hacer uso de los métodos que veremos a continuación para rellenar la malla de la forma deseada.

Seguidamente, pasamos a estudiar los elementos que nos ofrece esta clase. Todos ellos son métodos,
con un solo constructor:

− TiledLayer(int columns, int rows, Image image, int tileWidth, int tileHeight). Constructor del
TiledLayer, al cual debemos pasar el número de filas y columnas que compondrán su malla
asociada, la imagen original a dividir y el ancho y alto en píxeles de los tiles a obtener.

El ancho del objeto Image debe ser múltiplo entero de tileWidth y el alto, de tileHeight, para que
así el número de tiles obtenido sea un entero correcto (si no, IllegalArgumentException).

Tras la construcción, los tiles de la malla (por defecto, todos 0) podrán ser variados usando los
métodos setCell(), fillCells() o setStaticTileSet(), debiendo limitar el uso de este último por
suponer un coste de memoria y computacional elevado.

− int createAnimatedTile(int staticTileIndex). Crea un tile animado asociado inicialmente al tile


estático cuyo índice pasamos como parámetro (0 o un índice positivo existente). El índice
devuelto corresponde al tile animado, siendo siempre el índice de un tile animado un número
negativo.

− void fillCells(int col, int row, int numCols, int numRows, int tileIndex). Rellena una región de
celdas de la malla con el tile que indica el parámetro tileIndex. Éste puede apuntar a un tile
estático, animado o vacío (dando índice 0).

Para indicar la región damos el elemento superior izquierdo de ella (su fila y columna) y el
número de filas y columnas que la compondrán a partir de éste.

− int getAnimatedTile(int animatedTileIndex). Obtiene el índice del tile estático asociado


actualmente con el tile animado cuyo índice damos como entrada. Recordemos que éste debe
ser un número negativo y que el de salida será positivo.

- 143 -
− int getCell(int col, int row). Obtiene el índice del tile (animado o estático) que actualmente
ocupa la celda indicada por su número de fila y columna. Si la celda está vacía, se devuelve 0.

− int getCellHeight(). Obtiene el alto en píxeles de las celdas de la malla. Será un valor constante
para toda celda.

− int getCellWidth(). Adquiere el ancho en píxeles de las celdas de la malla. Será un valor
constante para toda celda.

− int getColumns(). Alcanza el número de columnas de la malla. Para obtener el ancho en píxeles
de la malla completa no haría falta usar este método junto a getCellWidth(), simplemente nos lo
daría el método heredado Layer.getWidth().

− int getRows(). Obtiene el número de filas de la malla. El alto en píxeles de la malla completa se
obtiene de la misma forma que en el método anterior.

− void paint(Graphics g). Dibuja el TiledLayer (si éste es visible) utilizando para ello el Graphics
parámetro. La esquina superior izquierda del TiledLayer es dibujada en la posición actual de éste
relativa al sistema de coordenadas global, la cual puede obtenerse con los métodos Layer.getX()
y Layer.getY().

− void setAnimatedTile(int animatedTileIndex, int staticTileIndex). Asocia el tile estático, cuyo


índice es el dado en el segundo parámetro, con el tile animado de índice el dado en el primer
parámetro. Ambos tiles deben existir o el estático ser vacío (índice 0).

− void setCell(int col, int row, int tileIndex). Modifica el contenido de una celda, indicando ésta
con su número de fila y columna, y dando en el tercer parámetro el índice del tile que fijaremos
en ella (estático, dinámico o vacío).

− void setStaticTileSet(Image image, int tileWidth, int tileHeight). Recibiendo los mismos
parámetros (salvo los que definen la malla, la estructura de la cual no puede ser variada) que el
constructor visto, funciona de forma análoga a aquél pero con el TiledLayer ya creado.
Reemplazará, por tanto, el conjunto original de tiles estáticos al completo, debiendo tener en
cuenta ante este acto que si el nuevo conjunto de tiles tiene el mismo o mayor número de
elementos que el antiguo conjunto, los tiles animados y el contenido de la malla asociada (los

- 144 -
índices que en realidad almacena) serán conservados. En caso contrario, las celdas de la malla
serán reiniciadas (índice 0) y los tiles animados serán eliminados.

4.3. EJEMPLO DE JUEGO INTERACTIVO1

Para finalizar, describiremos el ejemplo desarrollado en este capítulo, como siempre profusamente
comentado para que su comprensión sea absoluta. Será un juego interactivo tipo arcade de laberinto, en
el cual un personaje (héroe) debe escapar de múltiples enemigos que se irán creando conforme vaya
evolucionando el juego, pudiéndose atacar mutuamente. El juego está en una primera versión muy
básica, aunque nos da una base potente para poder ampliarlo de forma sencilla. Su código es el
siguiente:

GAMEEjemploMIDlet.java

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

//Clase del MIDlet: Ésta es la clase del MIDlet, la cual pasará todo el trabajo
//del juego a la instancia que crea de la clase Principal, quedando aquí básicamente sólo
//el tratamiento comandos
public class GAMEEjemploMIDlet extends MIDlet implements CommandListener {

private Display pantalla;


private Principal juego;
private Command abandonar;

//---------------------------------------------------------------------------------------------------------------------------------------------------------
//Constructor del MIDlet: Capturamos la pantalla, creamos el GameCanvas y el comando de fin de juego
public GAMEEjemploMIDlet() {

pantalla = Display.getDisplay(this);
abandonar = new Command("Abandonar", Command.CANCEL, 1);
juego = new Principal();
juego.addCommand(abandonar);

1Recomendamos la prueba de este ejemplo en el simulador JWT 2.2 así como la revisión y cambios que veamos oportunos en
su código hasta comprenderlo perfectamente.

- 145 -
juego.setCommandListener(this);
}

//---------------------------------------------------------------------------------------------------------------------------------------------------------
//Ciclo de vida del MIDlet:
public void startApp() throws MIDletStateChangeException {
pantalla.setCurrent(juego);
juego.comenzar();
}

public void pauseApp() {}

public void destroyApp(boolean unconditional) {}

//---------------------------------------------------------------------------------------------------------------------------------------------------------
//Escuchador del comando de abandono
public void commandAction(Command c, Displayable s) {
if (c == abandonar) {
//Si no se ha acabado el juego por otro motivo, entrará a cerrar el gameLoop
if (!juego.isFinDelJuego()) juego.cerrarJuego();
destroyApp(false);
notifyDestroyed();
}
}

}//fin del MIDlet

Principal.java

import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
import javax.microedition.lcdui.game.*;
import java.util.*;
import java.lang.*;

//Clase Principal: En ella, dispondremos del organizador de capas (LayerManager) y hace de base

- 146 -
//del juego. Desde ésta se abrirá el hilo que se encarga del gameLoop del juego, para lo cual
//implementa Runnable.
//Es la clase que hereda de GameCanvas para darnos el acceso a eventos de teclas de juego y
//al buffer oculto de dibujo con el que refrescar la pantalla a cada vuelta del gameloop.
public class Principal extends GameCanvas implements Runnable{

//Retraso en el hilo del gameLoop


private static final int SLEEP_HILO = 30;

//Organizador de capas del juego


private LayerManager organizador;

//Instancia del héroe del juego, pública para verla desde el enemigo, para colisiones
public static Heroe heroe;

//Conjunto de enemigos
private EnemigosHash enemigos;

//Capa de cielo. No usada en esta versión del juego


private TiledLayer capaCielo;

//Capa de obstáculos. Pública para verla desde el heroe, para colisiones


public static TiledLayer capaObstaculos;

//Capa del Suelo


private TiledLayer capaSuelo;

//Booleano para finalizar el GameLoop y así forzar el fín del hilo extra
private boolean finDelJuego;

//Hilo dedicado al bucle del juego


private Thread gameLoop;

//Objeto Graphics para dibujar la gráfica por medio del buffer oculto
private Graphics bufferDibujo;

//Ancho y alto de la pantalla del dispositivo (variará de uno a otro previsiblemente)


private int anchoPantalla;
private int altoPantalla;

- 147 -
//-------------------------------------------------------------------------------------------------------------------------------------------------------
//Constructor de la clase: En él construimos el GameCanvas, creamos la base del juego
//como "no activa" (finDelJuego) y capturamos las dimensiones de la pantalla del dispositivo
public Principal(){
super(true); //Constructor del GameCanvas
finDelJuego = true; //Al crearse, el juego aún no está activo
anchoPantalla = this.getWidth();
altoPantalla = this.getHeight();
}

//-------------------------------------------------------------------------------------------------------------------------------------------------------
//Observadora del booleano de cierre de juego
public boolean isFinDelJuego(){
return finDelJuego;
}

//-------------------------------------------------------------------------------------------------------------------------------------------------------
//Este método pone a true el booleano que marca el fin del gameLoop, para llamarlo desde
//la clase del MIDlet cuando se desee salir del juego (o ante cualquier otra circunstancia
//a añadir que indique la muerte del héroe y el consecuente fín de la partida actual)
public void cerrarJuego(){
finDelJuego = true;
}

//-------------------------------------------------------------------------------------------------------------------------------------------------------
//Método de iniciación de capas y lanzamiento del gameLoop. Su llamada desde el MIDlet marca
//el comienzo del juego
public void comenzar(){
try{
//Una vez que esté cada capa construida, la añadiremos al LayerManager
//en el orden apropiado: Primero deben entrar las más cercanas a los ojos del usuario
organizador = new LayerManager();

//Usamos una Clase Factoría para los TiledLayers; los Sprites irán aparte:
FactoriaTiled factoriaTileds = new FactoriaTiled();

//Creamos la capa del cielo y la insertamos en el organizador (AÚN POR HACER)


capaCielo = factoriaTileds.creaCielo();
//organizador.append(capaCielo);

- 148 -
//Creamos la capa de Obstáculos y la insertamos en el organizador:
capaObstaculos = factoriaTileds.creaObstaculos();
organizador.append(capaObstaculos);
//Creamos el Héroe y lo insertamos en el organizador. Tras esta llamada, el organizador
//queda variado con la información del Sprite asociado al Héroe
heroe = new Heroe(organizador);

//Creamos la capa de Suelo y la insertamos en el organizador:


capaSuelo = factoriaTileds.creaSuelo();
organizador.append(capaSuelo);

//Creamos por último los enemigos, inicialmente uno. Se crean e insertan en el organizador
//tras el suelo pues siempre un enemigo entra al LayerManager por encima de las dos últimas
//capas (suelo y obstáculos), para que así podamos generar durante la ejecución del juego
//nuevos enemigos a combatir y todos se coloquen por encima de ellas.
enemigos = new EnemigosHash(organizador);

//Una vez todo iniciado y el LayerManager organizador con las capas


//del juego dadas de alta, comenzamos el bucle del juego:
finDelJuego = false;
gameLoop = new Thread(this);
gameLoop.start(); //Llamamos al run() del hilo
}
catch(Exception e){
System.out.println("EXCEPCIÓN EN MÉTODO Principal.comenzar():" + e.toString());
finDelJuego = true;
}
}

//-------------------------------------------------------------------------------------------------------------------------------------------------------
//Método run del Hilo encargado de llevar el bucle de juego, el GAMELOOP:
public void run(){

//Antes de comenzar, capturamos el buffer oculto asociado al GameCanvas:


bufferDibujo = this.getGraphics();

//Cuenta las iteraciones del GameLoop


int contador = 1;

- 149 -
//GAME LOOP:
while(!finDelJuego)
{
//Capturamos entrada de teclado y variamos el estado del heroe con ella:
heroe.accionTeclado(this.getKeyStates());

//Tras mover al héroe, movemos la ventana de visión para que quede centrado en ella
this.actualizaCentrado();

//En cada vuelta hacemos que los enemigos también actúen. Le pasamos el LayerManager
//pues cada cierto tiempo le asociaremos un nuevo enemigo. La posición del Heroe le ayudará
//a buscarlo de una forma más eficiente, aunque la dejaremos relativamente aleatoria
enemigos.accionLogica(contador, organizador, heroe.getPosicionX(), heroe.getPosicionY());

//Refrescamos la pantalla con el buffer oculto: Vaciamos, pintamos y después volcamos:


bufferDibujo.fillRect(0,0,anchoPantalla, altoPantalla);
organizador.paint(bufferDibujo, 0, 0);
this.flushGraphics();

//Actualizamos el contador circular de vueltas:


if(contador < 10000) contador++;
else contador = 1;

//Imponemos un cierto retraso en el hilo para recibir correctamente los eventos de teclado
try {
Thread.sleep(SLEEP_HILO);
}
catch( InterruptedException e ) {}
}
}

//-------------------------------------------------------------------------------------------------------------------------------------------------------
//Controla la ventana de visión del LayerManager para que siempre mantenga al
//héroe centrado en la pantalla. Al tener la capa asociada al héroe su píxel de
//referencia en su centro, quedará perfectamente centrado.
public void actualizaCentrado(){
organizador.setViewWindow( heroe.getPosicionX() - anchoPantalla / 2,
heroe.getPosicionY() - altoPantalla / 2,
anchoPantalla, altoPantalla );
}

- 150 -
}//fin clase Principal

FactoriaTiled.java

import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
import javax.microedition.lcdui.game.*;
import java.util.*;
import java.lang.*;

//Clase FactoriaTiled, instanciadora de capas TyledLayer. Dependiendo del parámetro pasado


//al constructor, devolvemos uno u otro TiledLayer (codificados por ctes)
class FactoriaTiled{

//Número de filas y columnas de cada malla asociada a cada TiledLayer


private static final int COLUMNAS_OBSTACULOS_MALLA = 20;
private static final int FILAS_OBSTACULOS_MALLA = 20;
private static final int COLUMNAS_SUELO_MALLA = 20;
private static final int FILAS_SUELO_MALLA = 20;

//Imágenes originales de donde obtener los tiles


private static final String IMAGEN_ORIGEN_OBSTACULOS = "/obstaculosTiled.png";
private static final String IMAGEN_ORIGEN_SUELO = "/sueloTiled.png";

//Ancho y alto con los que dividir las imágenes anteriores (dan el ancho y alto de cada tile)
private static final int ANCHO_OBSTACULOS_TILE = 48;
private static final int ALTO_OBSTACULOS_TILE = 50;
private static final int ANCHO_SUELO_TILE = 48;
private static final int ALTO_SUELO_TILE = 50;

//-------------------------------------------------------------------------------------------------------------------------------------------------------
//Constructor vacío de la clase
public FactoriaTiled() {
}

//-------------------------------------------------------------------------------------------------------------------------------------------------------
//Método creador de la capa del Cielo:
public TiledLayer creaCielo() {

- 151 -
//HACER! (Lo veremos como ejercicio complementario)
return null;
}
//-------------------------------------------------------------------------------------------------------------------------------------------------------
//Método creador de la capa del Obstáculos:
public TiledLayer creaObstaculos()
{
TiledLayer capaObstaculos = null;
try{
//Creamos la capa
capaObstaculos = new TiledLayer( COLUMNAS_OBSTACULOS_MALLA, FILAS_OBSTACULOS_MALLA,
Image.createImage(IMAGEN_ORIGEN_OBSTACULOS),
ANCHO_OBSTACULOS_TILE, ALTO_OBSTACULOS_TILE);

//Llenamos la malla de tiles con una función aleatoria:


Random aleat = new Random();
int numAleatorio = 0;
for(int i=0; i<FILAS_OBSTACULOS_MALLA; i++){
for(int j=0; j<COLUMNAS_OBSTACULOS_MALLA; j++){
//Lugar de salida del héroe sin obstáculos
if(i<4 && j<4){
capaObstaculos.setCell(j, i, 0);
//Para los límites siempre aseguramos un bloque de los 4 tiles existentes:
}else if(i==0 || i==FILAS_OBSTACULOS_MALLA-1 ||
j==0 || j == COLUMNAS_OBSTACULOS_MALLA-1){
numAleatorio = (Math.abs(aleat.nextInt()) % 4) + 1;
capaObstaculos.setCell(j, i, numAleatorio);
//Para el interior, alta probabilidad de contenido vacío
}else{
numAleatorio = (Math.abs(aleat.nextInt()) % 100) + 1;
if( numAleatorio < 5 )
capaObstaculos.setCell(j, i, numAleatorio);
else
capaObstaculos.setCell(j, i, 0);
}
}
}
}
catch(Exception e){
System.out.println("EXCEPCIÓN AL CREAR LA CAPA DE OBSTÁCULOS: " + e.toString());

- 152 -
}
return capaObstaculos;
}
//-------------------------------------------------------------------------------------------------------------------------------------------------------
//Método creador de la capa del Suelo:
public TiledLayer creaSuelo()
{
TiledLayer capaSuelo = null;
try{
//Creamos la capa
capaSuelo = new TiledLayer( COLUMNAS_SUELO_MALLA, FILAS_SUELO_MALLA,
Image.createImage(IMAGEN_ORIGEN_SUELO),
ANCHO_SUELO_TILE, ALTO_SUELO_TILE);

//Llenamos la malla aleatoriamente


Random aleat = new Random();
int numAleatorio = 0;
for(int i=0; i<FILAS_SUELO_MALLA; i++){
for(int j=0; j<COLUMNAS_SUELO_MALLA; j++){
//Donde aparece el héroe el suelo lo damos amarillo
if(i<3 && j<3){
capaSuelo.setCell(j, i, 4);
}
//El resto, aleatoriamente uno de los tres tiles verdes
else{
numAleatorio = (Math.abs(aleat.nextInt()) % 3) + 1;
capaSuelo.setCell(j, i, numAleatorio);
}
}
}
}
catch(Exception e){
System.out.println("EXCEPCIÓN AL CREAR LA CAPA DE SUELO: " + e.toString());
}
return capaSuelo;
}

}//fín de la clase FactoriaTiled

- 153 -
Heroe.java

import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
import javax.microedition.lcdui.game.*;
import java.util.*;
import java.lang.*;

//Clase del Heroe. En ella tendremos el Sprite asociado, así como un int estado que marcará en cada
//momento las características del héroe en el juego
class Heroe{

//Atributos del héroe:


private int estado;
private Sprite capa;

//Ancho y Alto en píxeles de los frames a obtener para el heroe (depende de la imagenOrigen)
private static final int ANCHO_HEROE_FRAME = 97;
private static final int ALTO_HEROE_FRAME = 125;
private static final String IMAGEN_ORIGEN_HEROE = "/heroeSprite.png";

//Distancia en píxeles de cada paso del héroe. Cada vez que lo movamos, avanzará esta distancia
private static final int PASO_HEROE = 15;

//El heroe puede encontrarse en 8 estados diferentes, cada uno asociado con una
//secuencia de frames distinta. Siempre variaremos primero el estado y con él su secuencia.
//Las dos últimas constantes son públicas para verlas desde el enemigo. El estado HABLANDO
//no provoca sonido alguno; se deja como posible ampliación.
private static final int ESTADO_HEROE_INICIO = 1;
private static final int ESTADO_HEROE_HABLANDO = 2;
private static final int ESTADO_HEROE_ANDANDO_DCHA = 3;
private static final int ESTADO_HEROE_ANDANDO_IZDA = 4;
private static final int ESTADO_HEROE_ANDANDO_ABAJO = 5;
private static final int ESTADO_HEROE_ANDANDO_ARRIBA = 6;
public static final int ESTADO_HEROE_DISPARO = 7;
public static final int ESTADO_HEROE_MUERTE = 8;

//Constantes para fijar cada secuencia de frames


private static final int SECUENCIA_HEROE_INICIO[] = {0};
private static final int SECUENCIA_HEROE_HABLANDO[] = {0, 2, 3, 1, 0, 1, 2, 3, 0};

- 154 -
private static final int SECUENCIA_HEROE_ANDANDO[] = {5, 0, 4, 0, 5};
private static final int SECUENCIA_HEROE_DISPARO[] = {6};
private static final int SECUENCIA_HEROE_MUERTE[] = {7};

//-------------------------------------------------------------------------------------------------------------------------------------------------------
//Constructor de la clase: Inicia al Héroe, su Sprite asociado y lo coloca en su estado inicial
public Heroe(LayerManager org){
try{
//Sin estado definido. Será necesario para que setEstado funcione correctamente al iniciar
estado = 0;

//Creamos la capa que representará al héroe en el juego


capa = new Sprite(Image.createImage(IMAGEN_ORIGEN_HEROE),
ANCHO_HEROE_FRAME, ALTO_HEROE_FRAME);

//Cambiamos el píxel de referencia al del centro de la capa


capa.defineReferencePixel(ANCHO_HEROE_FRAME/2, ALTO_HEROE_FRAME/2);

//Posicionamos al héroe inicialmente a un paso diagonal


capa.setPosition(PASO_HEROE, PASO_HEROE);

//Definimos un rectángulo de colisión menos restrictivo que el de por defecto (todo el Sprite)
capa.defineCollisionRectangle(20, 10, ANCHO_HEROE_FRAME - 20*2, ALTO_HEROE_FRAME - 10*2);

//Damos de alta la capa en el organizador directamente. Al existir un solo héroe no hay que
//comprobar tanto como al insertar una capa de enemigo.
org.append(this.capa);

//Iniciamos el estado del héroe


this.setEstado(ESTADO_HEROE_INICIO);

}catch(Exception e){
System.out.println("EXCEPCIÓN EN LA CREACIÓN DEL HÉROE: " + e.toString());
}
}

//---------------------------------------------------------------------------------------------------------------------------------------------------------
//Observadoras de los atributos del héroe
public int getEstado(){
return estado;

- 155 -
}

public Sprite getCapa(){


return capa;
}
//---------------------------------------------------------------------------------------------------------------------------------------------------------
//Devuelve la posición en X del píxel de referencia (centro del heroe) en coordenadas globales
public int getPosicionX(){
return capa.getRefPixelX();
}

//Devuelve la posición en Y del píxel de referencia (centro del héroe) en coordenadas globales
public int getPosicionY(){
return capa.getRefPixelY();
}

//-------------------------------------------------------------------------------------------------------------------------------------------------------
//Trae la secuencia de frames a aplicar. Cada vez que vayamos a cambiar la secuencia de frames
//lo haremos por aquí, dependiendo del estado del héroe. Así no perdemos la asociación entre estados y secuencias
public int[] getSecuencia(int estado)
{
if(estado == ESTADO_HEROE_INICIO) return SECUENCIA_HEROE_INICIO;
else if (estado == ESTADO_HEROE_HABLANDO) return SECUENCIA_HEROE_HABLANDO;
else if (estado == ESTADO_HEROE_ANDANDO_DCHA ||
estado == ESTADO_HEROE_ANDANDO_IZDA ||
estado == ESTADO_HEROE_ANDANDO_ABAJO ||
estado == ESTADO_HEROE_ANDANDO_ARRIBA) return SECUENCIA_HEROE_ANDANDO;
else if (estado == ESTADO_HEROE_DISPARO) return SECUENCIA_HEROE_DISPARO;
else if (estado == ESTADO_HEROE_MUERTE) return SECUENCIA_HEROE_MUERTE;
else return SECUENCIA_HEROE_INICIO;
}

//-------------------------------------------------------------------------------------------------------------------------------------------------------
//Modifica el estado del héroe. Al modificar el estado habrá que hacer ciertas cosas, como
//aplicarle transformaciones, moverlo, calcular colisiones, modificar su secuencia de frames,
//etc. Éstas podrán depender del estado en el que se encontraba antes de la llamada a este método.
public void setEstado(int nuevoEstado){
try{
if(nuevoEstado == ESTADO_HEROE_INICIO){
//Nada, simplemente el cambio de secuencia más adelante

- 156 -
}
else if (nuevoEstado == ESTADO_HEROE_HABLANDO){
//Nada, simplemente el cambio de secuencia más adelante. Aquí podríamos insertar
//la emisión de un archivo multimedia al entrar a este estado (hacer que hable)
}
else if (nuevoEstado == ESTADO_HEROE_ANDANDO_DCHA){
//Si no estaba andando para la dcha. ni abajo, repongo la capa con su orientación original:
if (estado != ESTADO_HEROE_ANDANDO_DCHA &&
estado != ESTADO_HEROE_ANDANDO_ABAJO )
capa.setTransform(Sprite.TRANS_NONE);
//Lo movemos:
capa.move(PASO_HEROE, 0);

//Calculamos su colisión con obstáculos. Si se da, volvemos atrás el movimiento


if(capa.collidesWith(Principal.capaObstaculos, true))
capa.move((-1)*PASO_HEROE, 0);
}
else if (nuevoEstado == ESTADO_HEROE_ANDANDO_IZDA){
//Si no estaba andando para la izqda. ni arriba, hacemos reflejo para hacer que
//mire hacia el sentido de su movimiento
if (estado != ESTADO_HEROE_ANDANDO_IZDA &&
estado != ESTADO_HEROE_ANDANDO_ARRIBA )
capa.setTransform(Sprite.TRANS_MIRROR);
//Lo movemos:
capa.move((-1)*PASO_HEROE, 0);

//Calculamos su colisión con obstáculos. Si se da, volvemos atrás el movimiento


if(capa.collidesWith(Principal.capaObstaculos, true))
capa.move(PASO_HEROE, 0);
}
else if (nuevoEstado == ESTADO_HEROE_ANDANDO_ARRIBA){
//Si no estaba andando para la dcha ni abajo, hacemos reflejo para hacer que
//mire hacia el sentido de su movimiento
if (estado != ESTADO_HEROE_ANDANDO_DCHA &&
estado != ESTADO_HEROE_ANDANDO_ABAJO )
capa.setTransform(Sprite.TRANS_MIRROR);

//Lo movemos:
capa.move(0, (-1)*PASO_HEROE);

- 157 -
//Calculamos su colisión con obstáculos. Si se da, volvemos atrás el movimiento
if(capa.collidesWith(Principal.capaObstaculos, true))
capa.move(0, PASO_HEROE);
}
else if (nuevoEstado == ESTADO_HEROE_ANDANDO_ABAJO){
//Si no estaba andando para la izqda. ni arriba, repongo la capa con su orientación original:
if (estado != ESTADO_HEROE_ANDANDO_IZDA &&
estado != ESTADO_HEROE_ANDANDO_ARRIBA )
capa.setTransform(Sprite.TRANS_NONE);
//Lo movemos:
capa.move(0, PASO_HEROE);

//Calculamos su colisión con obstáculos. Si se da, volvemos atrás el movimiento


if(capa.collidesWith(Principal.capaObstaculos, true))
capa.move(0, (-1)*PASO_HEROE);
}
else if (nuevoEstado == ESTADO_HEROE_DISPARO){
//Nada, simplemente el cambio de secuencia más adelante. Si quisiéramos que
//el héroe disparara balas por ejemplo (nuevas capas), lo provocaríamos aquí
}
else if (nuevoEstado == ESTADO_HEROE_MUERTE){
//Nada, simplemente el cambio de secuencia más adelante. Aquí podríamos cerrar
//el bucle de juego si insertamos funcionalidad para hacer que el héroe muera.
}

//Actualizamos la secuencia del Sprite si el estado ha variado. Si no, pasamos al


//siguiente frame de la secuencia actual
if(nuevoEstado != this.estado)
capa.setFrameSequence(this.getSecuencia(nuevoEstado));
else
capa.nextFrame();

//Finalmente, machacamos el estado anterior con el nuevo:


this.estado = nuevoEstado;

}catch(Exception e){
System.out.println("EXCEPCIÓN EN setEstado() DEL HÉROE: " + e.toString());
}
}

- 158 -
//-------------------------------------------------------------------------------------------------------------------------------------------------------
//Este método actualiza el estado del héroe dependiendo de la tecla pulsada, la cual
//nos pasan como parámetro. Ya en el método setEstado se provocarán las modificaciones
//pertinentes en el Sprite del héroe.
public void accionTeclado(int keyStates)
{
if((keyStates & GameCanvas.RIGHT_PRESSED)!=0) this.setEstado(ESTADO_HEROE_ANDANDO_DCHA);
else if((keyStates & GameCanvas.LEFT_PRESSED)!=0) this.setEstado(ESTADO_HEROE_ANDANDO_IZDA);
else if((keyStates & GameCanvas.DOWN_PRESSED)!=0) this.setEstado(ESTADO_HEROE_ANDANDO_ABAJO);
else if((keyStates & GameCanvas.UP_PRESSED)!=0) this.setEstado(ESTADO_HEROE_ANDANDO_ARRIBA);
else if((keyStates & GameCanvas.FIRE_PRESSED)!=0) this.setEstado(ESTADO_HEROE_DISPARO);
//Las siguientes teclas no están aseguradas en todo dispositivo MIDP. Podría pensarse en una
//interacción del usuario alternativa para provocar el paso a este estado
else if((keyStates & GameCanvas.GAME_A_PRESSED)!=0 ||
(keyStates & GameCanvas.GAME_B_PRESSED)!=0) this.setEstado(ESTADO_HEROE_HABLANDO);
//Si no se encuentra ninguna pulsación, se para.
else
this.setEstado(ESTADO_HEROE_INICIO);
}

}//fín de la clase Heroe

EnemigosHash.java

import javax.microedition.lcdui.*;
import javax.microedition.lcdui.game.*;
import java.util.*;
import java.lang.*;

//Clase de Enemigos. Representará al conjunto de enemigos, con una tabla hash como atributo donde
//almacenarlos y la imagen origen con la que crear los Sprites de cada uno de ellos.
public class EnemigosHash{

//Número máximo de enemigos que permitimos en el juego


private static final int MAXENEMIGOS = 7;

//Número de vueltas del gameLoop que se espera para crear un nuevo enemigo
private static final int NUMVUELTAS_CREACION = 250;

- 159 -
//Imagen origen de todos los Sprites asociados a los enemigos a crear
private static final String IMAGEN_ORIGEN_ENEMIGO = "/enemigoSprite.png";

//Atributos del conjunto de enemigos


private Hashtable enemigos;
private Image imagenOrigen;

//-------------------------------------------------------------------------------------------------------------------------------------------------------
//Constructor de la clase: sólo será invocado al inicio del juego, creando
//la tabla hash y llenándola con un primer enemigo por defecto
public EnemigosHash(LayerManager organizador){
try{
enemigos = new Hashtable(MAXENEMIGOS);
imagenOrigen = Image.createImage(IMAGEN_ORIGEN_ENEMIGO);
this.insertaEnemigo(new Enemigo(imagenOrigen),organizador);

}catch(Exception e){
System.out.println("EXCEPCIÓN EN LA CREACIÓN DE LOS ENEMIGOS: " + e.toString());
}
}

//-------------------------------------------------------------------------------------------------------------------------------------------------------
//Inserta un enemigo en la tabla hash y lo da de alta en el LayerManager. A usar con el
//enemigo inicial y con cada enemigo a crear nuevo
public void insertaEnemigo(Enemigo enem, LayerManager org)
{
//Si con ello no se excede el número máximo de enemigos que permitimos
if(this.enemigos.size() < MAXENEMIGOS) {
//Insertamos en el hash
this.enemigos.put(new Integer(this.enemigos.size() + 1), enem);
//Damos de alta su capa en el organizador
enem.altaEnemigo(org);
}
}

//-------------------------------------------------------------------------------------------------------------------------------------------------------
//Este método recorre todos los enemigos y hace que actúen. Cada NUMVUELTAS_CREACION vueltas
//al gameLoop, además creamos un nuevo enemigo.
public void accionLogica(int contador, LayerManager org, int posXHeroe, int posYHeroe)
{

- 160 -
//Hacemos actuar a los enemigos existentes:
Enumeration conjuntoEnemigos = this.enemigos.elements();
while (conjuntoEnemigos.hasMoreElements()) {
Enemigo enemAux = (Enemigo)conjuntoEnemigos.nextElement();
//Un enemigo sólo actuará si aún no ha sido eliminado
if(enemAux.getEstado() != Enemigo.ESTADO_ENEMIGO_MUERTE)
enemAux.accionLogica(posXHeroe, posYHeroe);
}
//Vemos si podemos o tenemos que crear un enemigo nuevo:
if( (contador%NUMVUELTAS_CREACION == 0) && (this.enemigos.size() < MAXENEMIGOS) )
insertaEnemigo(new Enemigo(imagenOrigen), org);
}

}//fin clase EnemigosHash

Enemigo.java

import javax.microedition.lcdui.*;
import javax.microedition.lcdui.game.*;
import java.util.*;
import java.lang.*;

//Clase del Enemigo. En ella tendremos el Sprite asociado, así como un int estado que marcará en cada
//momento las características de cada enemigo en el juego
class Enemigo{

//Atributos del enemigo:


private int estado;
private Sprite capa;

//Ancho y Alto en píxeles de los frames a obtener para el enemigo (depende de la imagenOrigen)
private static final int ANCHO_ENEMIGO_FRAME = 97;
private static final int ALTO_ENEMIGO_FRAME = 130;

//Distancia en píxeles de cada paso del enemigo. Cada vez que lo movamos, avanzará esta distancia
private static final int PASO_ENEMIGO = 10;

- 161 -
//Distancia al heroe en píxeles a la que el enemigo se acerca directamente a él. Aumentándola,
//entorpeceremos al enemigo en su misión de llegar al héroe.
private static final int DISTANCIA_IA = 100;
//El enemigo puede encontrarse en 7 estados diferentes, cada uno asociado con una
//secuencia de frames distinta. La última es public para verla desde EnemigosHash
//Siempre variaremos primero el estado y con él su secuencia.
//El estado HABLANDO no provoca sonido alguno; se deja como posible ampliación.
private static final int ESTADO_ENEMIGO_INICIO = 1;
private static final int ESTADO_ENEMIGO_HABLANDO = 2;
private static final int ESTADO_ENEMIGO_ANDANDO_DCHA = 3;
private static final int ESTADO_ENEMIGO_ANDANDO_IZDA = 4;
private static final int ESTADO_ENEMIGO_ANDANDO_ABAJO = 5;
private static final int ESTADO_ENEMIGO_ANDANDO_ARRIBA = 6;
public static final int ESTADO_ENEMIGO_MUERTE = 7;

//Constantes para fijar cada secuencia de frames


private static final int SECUENCIA_ENEMIGO_INICIO[] = {0};
private static final int SECUENCIA_ENEMIGO_HABLANDO[] = {0, 2, 1, 3, 0, 1, 3, 2, 0};
private static final int SECUENCIA_ENEMIGO_ANDANDO[] = {2, 3, 1};
private static final int SECUENCIA_ENEMIGO_MUERTE[] = {4};

//-------------------------------------------------------------------------------------------------------------------------------------------------------
//Constructor de la clase: Inicia al Enemigo, su Sprite asociado y lo coloca en su estado inicial
//Recibe una imagen al contrario del Héroe para evitar acceder al fichero de imagen cada vez
//que se crea un enemigo nuevo (ya estará almacenada en memoria por la clase EnemigosHash).
//El alta en el organizador de su capa se producirá en la clase EnemigosHash por medio del
//método altaEnemigo() que veremos aquí.
public Enemigo(Image origen){
try{
//Sin estado definido. Será necesario para que setEstado funcione correctamente al iniciar
estado = 0;

//Creamos la capa que representará a este enemigo en el juego


capa = new Sprite(origen, ANCHO_ENEMIGO_FRAME, ALTO_ENEMIGO_FRAME);

//Cambiamos el píxel de referencia al del centro de la capa


capa.defineReferencePixel(ANCHO_ENEMIGO_FRAME/2, ALTO_ENEMIGO_FRAME/2);

//Colocamos inicialmente al enemigo en la esquina opuesta a la que aparece el héroe,


//el otro extremo de la capa de obstáculos

- 162 -
capa.setPosition(Principal.capaObstaculos.getWidth(), Principal.capaObstaculos.getHeight());

//Iniciamos el estado del enemigo


this.setEstado(ESTADO_ENEMIGO_INICIO);

}catch(Exception e){
System.out.println("EXCEPCIÓN EN LA CREACIÓN DEL ENEMIGO: " + e.toString());
}
}

//-------------------------------------------------------------------------------------------------------------------------------------------------------
//Observadoras de los atributos del enemigo
public int getEstado(){
return estado;
}

public Sprite getCapa(){


return capa;
}

//-------------------------------------------------------------------------------------------------------------------------------------------------------
//Trae la secuencia de frames a aplicar. Cada vez que vayamos a cambiar la secuencia de frames lo
//haremos por aquí, dependiendo del estado del enemigo. Así no perdemos la asociación entre estados y
//secuencias
public int[] getSecuencia(int estado) {
if(estado == ESTADO_ENEMIGO_INICIO) return SECUENCIA_ENEMIGO_INICIO;
else if (estado == ESTADO_ENEMIGO_HABLANDO) return SECUENCIA_ENEMIGO_HABLANDO;
else if (estado == ESTADO_ENEMIGO_ANDANDO_DCHA ||
estado == ESTADO_ENEMIGO_ANDANDO_IZDA ||
estado == ESTADO_ENEMIGO_ANDANDO_ABAJO ||
estado == ESTADO_ENEMIGO_ANDANDO_ARRIBA) return SECUENCIA_ENEMIGO_ANDANDO;
else if (estado == ESTADO_ENEMIGO_MUERTE) return SECUENCIA_ENEMIGO_MUERTE;
else return SECUENCIA_ENEMIGO_INICIO;
}

//-------------------------------------------------------------------------------------------------------------------------------------------------------
//Modifica el estado del enemigo. Al modificar el estado habrá que hacer ciertas cosas, como
//aplicarle transformaciones, moverlo, modificar su secuencia de frames, etc.
//Éstas podrán depender del estado en el que se encontraba antes de la llamada a este método.
public void setEstado(int nuevoEstado){

- 163 -
try{
if(nuevoEstado == ESTADO_ENEMIGO_INICIO){
//Nada, simplemente el cambio de secuencia más adelante
}
else if (nuevoEstado == ESTADO_ENEMIGO_HABLANDO){
//Nada, simplemente el cambio de secuencia más adelante. Aquí podríamos insertar
//la emisión de un archivo multimedia al entrar a este estado (hacer que hable)
}
else if (nuevoEstado == ESTADO_ENEMIGO_ANDANDO_DCHA){
//Si no estaba andando para la dcha ni abajo, repongo la capa con su orientación original:
if (estado != ESTADO_ENEMIGO_ANDANDO_DCHA &&
estado != ESTADO_ENEMIGO_ANDANDO_ABAJO )
capa.setTransform(Sprite.TRANS_NONE);

//Lo movemos:
capa.move(PASO_ENEMIGO, 0);
}
else if (nuevoEstado == ESTADO_ENEMIGO_ANDANDO_IZDA){
//Si no estaba andando para la izqda. ni arriba, hacemos reflejo para hacer que
//mire hacia el sentido de su movimiento
if (estado != ESTADO_ENEMIGO_ANDANDO_IZDA &&
estado != ESTADO_ENEMIGO_ANDANDO_ARRIBA )
capa.setTransform(Sprite.TRANS_MIRROR);

//Lo movemos:
capa.move((-1)*PASO_ENEMIGO, 0);
}
else if (nuevoEstado == ESTADO_ENEMIGO_ANDANDO_ARRIBA){
//Si no estaba andando para la dcha ni abajo, hacemos reflejo para hacer que
//mire hacia el sentido de su movimiento
if (estado != ESTADO_ENEMIGO_ANDANDO_DCHA &&
estado != ESTADO_ENEMIGO_ANDANDO_ABAJO )
capa.setTransform(Sprite.TRANS_MIRROR);

//Lo movemos:
capa.move(0, (-1)*PASO_ENEMIGO);
}
else if (nuevoEstado == ESTADO_ENEMIGO_ANDANDO_ABAJO){
//Si no estaba andando para la izqda. ni arriba, repongo la capa con su orientación original:
if (estado != ESTADO_ENEMIGO_ANDANDO_IZDA &&

- 164 -
estado != ESTADO_ENEMIGO_ANDANDO_ARRIBA )
capa.setTransform(Sprite.TRANS_NONE);

//Lo movemos:
capa.move(0, PASO_ENEMIGO);
}
else if (nuevoEstado == ESTADO_ENEMIGO_MUERTE){

//Reponemos su orientación original para que aparezca el texto "MIAU" correctamente


capa.setTransform(Sprite.TRANS_NONE);
}

//Actualizamos la secuencia del Sprite si el estado ha variado. Si no, pasamos al


//siguiente frame de la secuencia actual
if(nuevoEstado != this.estado)
capa.setFrameSequence(this.getSecuencia(nuevoEstado));
else
capa.nextFrame();

//Finalmente, machacamos el estado anterior con el nuevo:


this.estado = nuevoEstado;

}catch(Exception e){
System.out.println("EXCEPCION EN setEstado() DEL ENEMIGO: " + e.toString());
}
}

//-------------------------------------------------------------------------------------------------------------------------------------------------------
//Este método da de alta en el organizador de E/S la capa (Sprite) asociada al enemigo actual.
//Los enemigos deben aparecer siempre por encima del suelo y los obstáculos, así que los
//insertamos en el LayerManager en una posición de 2 menos que las capas que existan. Suponemos
//que las capas de suelo y obstáculos ya están insertadas.
public void altaEnemigo(LayerManager org) {
int numCapas = org.getSize();
org.insert(this.capa, numCapas-1);
}

//-------------------------------------------------------------------------------------------------------------------------------------------------------
//Este método actualiza el estado del enemigo. Éste no tendrá entrada de teclado pues no es
//manejado por el usuario; variará su estado por medio de la lógica que demos al método

- 165 -
//En este método pues es donde le decimos cuándo debe moverse y hacia donde, o si ha colisionado
//con el héroe provocándole la muerte.
//Recibe la posición del héroe para ayudarle a encontrarlo, aunque le provocaremos una
//cierta discapacidad aleatoria para hacerle más difícil llegar a él.
public void accionLogica(int posXHeroe, int posYHeroe)
{
//Calculamos la colisión con el héroe: Si llegamos a él, lo hacemos pasar
//a su estado MUERTE Aunque si se encuentra en estado DISPARO nos hace pasar
//él a nosotros (como enemigo) al estado MUERTE.
if(this.capa.collidesWith(Principal.heroe.getCapa(), true))
{
if(Principal.heroe.getEstado() == Heroe.ESTADO_HEROE_DISPARO)
this.setEstado(ESTADO_ENEMIGO_MUERTE);
else
Principal.heroe.setEstado(Heroe.ESTADO_HEROE_MUERTE);
}
//Si tras la colisión no hemos muerto, seguimos moviéndonos (aunque el héroe haya muerto)
if(this.estado!=ESTADO_ENEMIGO_MUERTE)
{
//Si no hemos muerto, generamos un número aleatorio entre 1 y 8 con el que calcular su nuevo estado
//(o se para, o habla, o anda)
Random aleat = new Random();
int numAleatorio = (Math.abs(aleat.nextInt()) % 8) + 1;

//Por defecto asociamos el numAleatorio al estado a pasar. Si el héroe está muy


//lejos, el valor de estado se cambiará para acercarlo directamente. En otro caso,
//lo acercamos con más probabilidad que el resto de estados pero menos directamente.
int estado = numAleatorio;
if (this.capa.getX() - posXHeroe < (-1)*DISTANCIA_IA) estado = 3;
else if(this.capa.getX() - posXHeroe > DISTANCIA_IA) estado = 4;
else if (this.capa.getY() - posYHeroe < (-1)*DISTANCIA_IA) estado = 5;
else if (this.capa.getY() - posYHeroe > DISTANCIA_IA) estado = 6;
else if (this.capa.getX() < posXHeroe){ if(numAleatorio > 6) estado = 3; }
else if (this.capa.getX() > posXHeroe){ if(numAleatorio > 6) estado = 4; }
else if (this.capa.getY() < posYHeroe){ if(numAleatorio > 6) estado = 5; }
else if (this.capa.getY() > posYHeroe){ if(numAleatorio > 6) estado = 6; }

//Ya capturado el estado a aplicar, llamamos al método que provocará este paso
if(estado == 1) this.setEstado(ESTADO_ENEMIGO_INICIO);
else if(estado == 2) this.setEstado(ESTADO_ENEMIGO_HABLANDO);

- 166 -
else if(estado == 3) this.setEstado(ESTADO_ENEMIGO_ANDANDO_DCHA);
else if(estado == 4) this.setEstado(ESTADO_ENEMIGO_ANDANDO_IZDA);
else if(estado == 5) this.setEstado(ESTADO_ENEMIGO_ANDANDO_ABAJO);
else if(estado == 6) this.setEstado(ESTADO_ENEMIGO_ANDANDO_ARRIBA);
}
}

}//fín de la clase Enemigo

Una vez visto el código, algunas capturas de pantalla del juego son:

- 167 -
- 168 -
- 169 -
RECUERDE

− Las características principales que hemos estudiado de un juego interactivo son: gráficos,
jugabilidad, multimedia y mercado.

− Una posible clasificación de la temática a ofrecer por un juego es: videoAventura, arcade,
deportiva, estrategia, rol y simulación.

− La separación en capas de la gráfica de un juego interactivo nos ayudará a programarlos más


fácilmente.

− El concepto de bucle de juego (Game Loop) es una característica de programación de juegos


común en todo ámbito de creación de estos.

− Tanto las APIs de J2ME estudiadas en los capítulos anteriores como son RMS, Comunicación
(HTTP) y Multimedia; como la API de construcción de interfaces de bajo nivel, nos serán de
gran utilidad, y en algunos casos imprescindibles, a la hora de crear un juego interactivo.

− Respecto al API dedicada a juegos (javax.microedition.lcdui.game), las 5 clases vistas nos


proporcionan: un ente base donde dibujar los elementos del juego y capturar la interacción por
teclado (GameCanvas); un organizador de las capas disponibles en el juego (LayerManager),
un ente abstracto representando una capa (Layer) y dos concreciones de éste para dar cuerpo a
capas de escenario (TiledLayer) y capas de jugadores (Sprite).

- 170 -
GLOSARIO

AMS (Application Management Software). Elemento perteneciente al software del dispositivo encargado
de la descarga, gestión, eliminación, etc. de los MIDlets a ejecutar en el sistema.
API (Application Programing Interface). Son conjuntos de elementos JAVA (clases, interfaces, constantes,
etc.) agrupados que podremos utilizar en nuestras aplicaciones. Llamadas también librerías o módulos,
nos permitirá escribir código sin reinventar lo que ya esté hecho, simplemente importando los elementos
de ellas que necesitemos en nuestra aplicación.
ATRIBUTO. Elemento en memoria encargado de definir parte del estado de la instancia de una clase.

CDC (Connected Device Configuration). Es la especificación de una configuración J2ME pensada para
dispositivos con más memoria y capacidad de proceso que los orientados a CLDC; dispositivos con
conexión permanente a la red y un mínimo de 2Mb de memoria disponible para el sistema JAVA.
CLASE. Ente con un estado dado por el valor que contengan en cada momento sus atributos y un
comportamiento definido por medio de la lógica contenida en sus métodos.
CLDC (Connected Limited Device Configuration). Es la especificación de una configuración J2ME
pensada para dispositivos con menores prestaciones, con conexión intermitente a la red y menos de 512
Kb de memoria disponible para el sistema JAVA. Esto obliga a usar una máquina virtual menor que la
JVM, por ejemplo la KVM.
CONFIGURACIÓN. En J2ME se denomina configuración al mínimo entorno de ejecución JAVA para una
familia concreta de dispositivos: La combinación de una máquina virtual JAVA (la estándar JVM
disponible para J2SE o alguna más limitada como la KVM) y un conjunto básico de APIs.
COOKIE. Cadena corta de datos codificados utilizada para almacenar información proveniente del
servidor en el cliente, dentro de una comunicación HTTP.

GIF (Graphics Interchange Format). Formato de imagen de 256 colores con compresión. Los archivos GIF
utilizan un algoritmo de compresión patentado, al contrario que PNG.

- 171 -
GCF (Generic Connection Framework). Es una API para establecimiento de conexiones de red a manos
de dispositivos móviles. Parte de las configuraciones CDC y CLDC, y se encuentra en el paquete
javax.microedition.io.
GUI (Graphic User Interface). Siglas para designar la interface gráfica de usuario por medio de la cual
éstos se pondrán en contacto con la aplicación.

HILO. Línea de ejecución distinta a la principal donde corre la aplicación, la cual se ejecuta
simultáneamente a ésta y de forma paralela.
HTTP (Hyper Text Transfer Protocol). Especifica un mecanismo de transferencia de cualquier tipo de
información en forma de texto a través de una red de comunicaciones, siguiendo el paradigma cliente-
servidor.
HTTPS (Hyper Text Transfer Protocol Secure sockets). Protocolo para transmisión de hipertexto
encriptado sobre SSL.

I-MODE. Estándar usado por dispositivos móviles japoneses para acceder a sitios Web cHTML (HTML
compacto).
INTERFACE. Elemento JAVA que indica qué lógica ofrecerá toda clase que la implemente. Serán
elementos vacíos, a los cuales cada clase les dará cuerpo como crea conveniente, eso si, respetando
cada prototipo indicado en la interface.

JAD. En J2ME, formato de fichero con el cual informamos al AMS del dispositivo de las características de
una suite.
JAR. En J2ME, formato de fichero en el cual empaquetamos una suite (sus MIDlets, recursos, etc.) para
su distribución.
JDBC (Java DataBase Connectivity). API perteneciente a J2SE que nos permite el acceso y gestión de
bases de datos relacionales desde JAVA. En J2ME no dispondremos de ella.
JSR (Java Specification Request). Propuesta de nuevas aportaciones a alguna de las plataformas JAVA
existentes, sea J2EE, J2SE o alguna configuración o perfil J2ME.
JVM (Java Virtual Machina). Máquina virtual JAVA interprete de J2SE.

- 172 -
K

KVM (Kilobyte Virtual Machina). Máquina virtual JAVA compacta diseñada para interpretar bytecode en
dispositivos pequeños. La configuración CLDC usa esta máquina virtual.

LCDUI. Forma abreviada de referirse a la API de creación de la interface de usuario en MIDP, contenida
en el paquete javax.microedition.lcdui. Concretamente, está pensada para dar soporte de creación de
GUIs en pantallas de cristal líquido propias de dispositivos pequeños.

MÉTODO. Elemento que alberga parte de la lógica del comportamiento de una clase.
MIDI (Musical Instrument Digital Interface). Protocolo industrial estándar que permite a diferentes
dispositivos compartir información. MIDI no transmite señales de audio, sino datos de eventos y mensajes
controladores que se pueden interpretar de manera arbitraria, de acuerdo con la programación del
dispositivo que los recibe. Es decir, MIDI es una especie de "partitura", contiene las instrucciones sobre
cuándo generar cada nota de sonido y las características que debe tener; el aparato al que se envíe dicha
partitura la transformará en música audible.
MIDLET. Aplicación orientada al perfil MIDP. Todas ellas heredarán de la clase
javax.microedition.midlet.MIDlet.
MIDP (Mobile Information Device Profile). Es un perfil J2ME bajo configuración CLDC orientado a
dispositivos móviles de pequeñas prestaciones.
MMA (Mobile Media API). Paquete opcional que aparece para mejorar los elementos disponibles en MIDP
2.0 para la creación multimedia J2ME. Especificado en la JSR 135.
MPEG. (Moving Picture Experts Group - grupo de expertos de imágenes en movimiento). Es un grupo de
empresas y universidades encargado del desarrollo de normas de codificación para audio y vídeo.
Numerosos formatos llevan sus siglas (MPEG-1, MPEG-2,...).

PAQUETE. Mecanismo por el cual agrupar clases y otros elementos JAVA en un ente común.
PERFIL. En J2ME, un perfil es un conjunto de APIs añadido a una configuración para soportar
funcionalidad extra. Un perfil bajo la configuración a la que pertenezca define un completo entorno de

- 173 -
aplicación de propósito general. Los perfiles pueden ser superconjuntos o subconjuntos de otros perfiles;
el Personal Basis Profile es subconjunto del Personal Profile y superconjunto del Foundation Profile.
PNG (Portable Network Graphics). Es un formato de imagen que ofrece compresión sin pérdidas y
flexibilidad de almacenamiento. MIDP exige a sus implementaciones que soporten al menos este formato.

RMS (Record Management System). Es una simple base de datos orientada a registros que permite a un
MIDlet almacenar información persistentemente para luego recuperarla. Distintos MIDlets pueden usar el
RMS para compartir información.
RTP (Real-time Transport Protocol - Protocolo de Transporte de Tiempo real. Es un protocolo de nivel de
transporte utilizado para la transmisión de información en tiempo real como, por ejemplo, el audio y vídeo
de una videoconferencia.

SERVLET. Módulos JAVA a ejecutar en un servidor de aplicaciones J2EE como Tomcat, con los cuales
generar una respuesta HTTP de este servidor al cliente que lo haya requerido.
SSL (Secure Sockets Layer). Es un protocolo que encripta los datos enviados por la red y provee
autenticación a ambos lados de la comunicación.
SUITE. Los MIDlets son empaquetados y distribuidos como suites de MIDlets, las cuales pueden contener
uno o más MIDlets. Están formadas por dos ficheros: uno describiendo su contenido, nombres de los
MIDlets, etc., de extensión .JAD y otro con las clases de los MIDlets y los recursos que usarán en su
ejecución, de extensión .JAR.

THREAD. Ver HILO.


TOMCAT. Implementación J2EE de las especificaciones Java Servlets y Java Server Pages (JSP). A
utilizar como plataforma para desarrollo y contención de servlets.

URL (Uniform Resource Locutor). Es una secuencia de caracteres, de acuerdo a un formato estándar,
que se usa para nombrar recursos como documentos e imágenes en Internet por su localización. El
formato general de una URL es: protocolo://máquina/directorio/recurso

- 174 -
W

WAP (Wireless Application Protocol). Es un protocolo para transmisión de datos entre servidores y
clientes (usualmente pequeños dispositivos móviles). WAP es análogo a HTTP en el caso de WWW.
WAV (WAVEform Audio Format). Es un formato de audio digital sin compresión de datos utilizado
normalmente para almacenar sonidos puntuales.
WTK (J2ME Wireless Toolkit). Es un conjunto de herramientas que provee a los desarrolladores J2ME de
un emulador, documentación y ejemplos para construir aplicaciones JAVA para dispositivos pequeños.
Orientado a la configuración CLDC y el perfil MIDP.
WWW (World Wide Web). Telaraña mundial de información que da cuerpo a lo que conocemos hoy como
Internet.

- 175 -
BIBLIOGRAFÍA

Froufe Quintas, Agustín y Jorge Cárdenas, Patricia. J2ME Manual de usuario y tutorial. Ed. RAMA.
Gálvez Rojas, Sergio y Ortega Díaz, Lucas. Java a Tope: J2ME. Universidad de Málaga.
García Serrano, Alberto. Programación de juegos para móviles con J2ME.
Rodríguez Millán, Daniel. Programación de videojuegos en JAVA. Ed. Ediversitas.

- 177 -
REFERENCIAS WEB

http://java.sun.com/j2me/docs/ Web de SUN microsystems, documentación


J2ME.

http://www.javahispano.org/forums.list.action?forum=3 Foro J2ME en javaHispano.

http://www.programacion.com/java/foros/44/ Foro J2ME en programacion.com.

http://www.microjava.com/ Micro JAVA Network.

http://sourceforge.net/ Proyectos de código abierto.

- 179 -

Anda mungkin juga menyukai