Escolar Documentos
Profissional Documentos
Cultura Documentos
2. COMUNICACIONES DE RED..........................................................................................................27
3. EL API MULTIMEDIA.......................................................................................................................67
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.
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.
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.
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.
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.
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.
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).
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 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.
-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.
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 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.
• 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.
- 10 -
• byte[] previousRecord(). Análogo al método nextRecord, para el registro anterior al
actual en el enumerador.
• void rebuild(). Refresca el enumerador con los datos actuales del RecordStore que lo
originó.
− 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:
− RecordListener
Con él implementamos un escuchador de eventos (listener) para capturar qué ha sucedido sobre
un registro dado:
- 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.
• 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 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.
- 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.
- 13 -
• int getSizeAvailable(). No atributo de la cabecera, da la cantidad de bytes disponible
para que el almacén actual crezca.
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).
- 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
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.*;
- 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());
}
- 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());
}
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
- 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
- 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
}//fin FiltroRMS
Para probar esta clase, bastará cambiar en el código del MIDlet anterior la línea:
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.*;
- 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:
- 25 -
Por:
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
− 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.
− 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.
- 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.
Las instancias de esta interface, por tanto, nos servirán para almacenar datos a leer o escribir
por medio de una conexión DatagramConnection.
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.
- 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.
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:
- 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).
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.
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:
- 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
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:
É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.
- 39 -
public static DataInputStream openDataInputStream(String name) throws IOException {
InputConnection con = (InputConnection)Connector.open(name, Connector.READ);
try { return con.openDataInputStream(); }
finally { con.close(); }
}
2.2.2.3. Excepciones
Dicha excepción será elevada para señalar que el objetivo a conectar no es encontrado o que el
protocolo requerido no está soportado.
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.
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().
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>
- 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();
...
}
− 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:
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.
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.
- 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.
- 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 getURL(). Cadena con la URL completa que se ha intentado alcanzar con
esta conexión.
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:
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 (;).
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.
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.
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>
- 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:
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:
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:
- 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).
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.
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:
- 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);
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);
- 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:
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.
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.*;
//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: ","");
- 54 -
cerrar = new Command("Cerrar",Command.EXIT, 1);
preguntar = new Command("Preguntar",Command.OK, 1);
formulario.addCommand(cerrar);
formulario.addCommand(preguntar);
formulario.setCommandListener(this);
//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();
Comunicador.java
import java.io.*;
import javax.microedition.io.*;
//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);
//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();
//Una vez conectados, recibimos la respuesta a leer del flujo de entrada que hemos obtenido
leerRespuesta(flujoRespuesta);
//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());
}
}
HTTPEjemploServlet.java
package servlets;
import java.io.*;
import javax.servlet.http.*;
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;
}//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:
<!--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.
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).
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.
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.
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.
2. En el startApp() del MIDlet abrimos el RecordStore y leemos la cookie almacenada (si existe):
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:
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:
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
− 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:
− 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.
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:
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.
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.
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.
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.
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.
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í:
- 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.
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.
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.
- 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.
Tras ello, el reproductor necesita localizar el media a reproducir, ya sea de forma local o
en algún servidor remoto.
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.
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 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.
- 78 -
Este tipo de contenido se da con la cadena correspondiente a su tipo MIME. Los más
utilizados son:
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.
- 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.
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.
• 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.
- 81 -
static String ERROR. Emisión de un error en el tratamiento del listener.
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
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 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
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:
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
};
static byte BLOCK_END. Indica el punto final del bloque, identificado éste por el
siguiente byte en la secuencia.
static byte C4. Valor de la nota DO central. Se suele usar como referencia de las
demás notas a utilizar.
- 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 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).
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.
- 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.
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.
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:
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.
- 88 -
• PlayerListener
static String BUFFERING_STOPPED. Emitido cuando el Player sale del modo buffer.
- 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:
− 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.
rtp://. Con este protocolo se accede a elementos multimedia de tipo streaming por
medio de una sesión RTP.
- 90 -
capture://radio. Utilizado para conseguir la entrada a la aplicación del contenido de
audio ofrecido por una emisora de radio.
Respecto a los elementos de la API, aparecen incorporados a los que ya vimos en MIDP 2.0:
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:
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:
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.
Esta interface es utilizada para recoger metainformación (title, copyright, author, etc.) definida en
el propio elemento multimedia.
Permite modificar el pitch (escala) actual del audio sin modificar la velocidad ni el volumen de la
reproducción.
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
Controla la reproducción de vídeo por parte de un Player. Los elementos que nos ofrece son:
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.
- 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.
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.
- 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.
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:
- 95 -
MIDlet-Version: 1.0
MicroEdition-Configuration: CLDC-1.1
MicroEdition-Profile: MIDP-2.0
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 {
//-------------------------------------------------------------------------------------------------------------------------------------------------------
// 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() {
- 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;
//-------------------------------------------------------------------------------------------------------------------------------------------------------
// 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());
- 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 {
- 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");
//-------------------------------------------------------------------------------------------------------------------------------------------------------
//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()");
- 101 -
form.setCommandListener(this);
//-------------------------------------------------------------------------------------------------------------------------------------------------------
// 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);
- 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());
}
}
- 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 {
//Reproductor
private Player player;
//-------------------------------------------------------------------------------------------------------------------------------------------------------
//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()");
//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");
//-------------------------------------------------------------------------------------------------------------------------------------------------------
//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);
- 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));
//-------------------------------------------------------------------------------------------------------------------------------------------------------
// 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());
}
}
- 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.
- 114 -
Desarrollo
de juegos
Tema 4
con
J2ME
- 115 -
4.1. INTRODUCCIÓN
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.
− Multimedia.
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.
− 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.
− 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.
- 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.
− 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.
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.
- 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:
• 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").
• 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.
- 126 -
aquí, de la forma que ya hemos repetido anteriormente, a comunicarnos con el usuario de una
forma más potente.
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.
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.
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 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.
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:
Los siguientes son los métodos que oferta la clase, siendo el primero de ellos el constructor que ofrece:
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:
− 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.
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).
− 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.
- 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:
− 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.
− 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.
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.
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.
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).
- 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.
- 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 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 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:
- 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.
• La posición del píxel de referencia asociado al Sprite no varía ante la llamada a este
método.
- 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 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.
− 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.
- 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 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.
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 {
//---------------------------------------------------------------------------------------------------------------------------------------------------------
//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();
}
//---------------------------------------------------------------------------------------------------------------------------------------------------------
//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();
}
}
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{
//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;
//Booleano para finalizar el GameLoop y así forzar el fín del hilo extra
private boolean finDelJuego;
//Objeto Graphics para dibujar la gráfica por medio del buffer oculto
private Graphics bufferDibujo;
- 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();
- 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 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);
//-------------------------------------------------------------------------------------------------------------------------------------------------------
//Método run del Hilo encargado de llevar el bucle de juego, el GAMELOOP:
public void run(){
- 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());
//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.*;
//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);
- 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);
- 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{
//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;
- 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;
//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);
}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 -
}
//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);
//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);
}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);
}
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 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";
//-------------------------------------------------------------------------------------------------------------------------------------------------------
//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);
}
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{
//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;
//-------------------------------------------------------------------------------------------------------------------------------------------------------
//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;
- 162 -
capa.setPosition(Principal.capaObstaculos.getWidth(), Principal.capaObstaculos.getHeight());
}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;
}
//-------------------------------------------------------------------------------------------------------------------------------------------------------
//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){
}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;
//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);
}
}
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.
− 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.
- 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.
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
- 179 -