|
PROGRAMACIÓN DEL API DE JUEGOS |
|
El API de juegos de MIDP 2.0 ofrece una serie de capacidades que no ofrecía MIDP 1.0 y que simplifican enormemente la creación de juegos 2D. Una de sus principales y más ventajosas características es que es enormemente compacto ya que, a pesar de ser una herramienta muy robusta y potente, tan solo consta de cinco nuevas clases que podemos encontrar en javax.microedition.lcdui.game. Estas cinco clases extienden las capacidades gráficas de MIDP de modo que se convierten en una herramienta fundamental para el programador de juegos. El objetivo de este tutorial es familiarizar al lector con estas nuevas clases y con la funcionalidad que ofrecen. Como primer paso para conseguirlo introducimos en este punto cuál es el concepto fundamental que introduce el API de juegos: la pantalla puede estar descompuesta en distintas capas o estratos (layers) de tal forma que podemos tratar cada una de ellas como una entidad independiente. Conociendo a fondo las clases que se exponen a continuación podremos comprender esta nueva filosofía y ponerla en práctica. |
|||
|
GameCanvas es una subclase de
Canvas que ofrece, además de las heredadas de
Canvas, nuevas capacidades específicas para la programación de juegos en J2ME. Por ejemplo, ofrece la
posibilidad de crear animaciones rápidas y libres de parpadeo o la posibilidad de examinar el estado de las teclas
del dispositivo.
Antes de conocer con más detalle qué nos ofrece la clase GameCanvas veamos como era la tarea de programar un juego con animaciones en MIDP 1.0, es decir, utilizando la clase Canvas. A lo más que podríamos llegar es a una solución que presentase el siguiente esquema: public class JuegoCanvas extends Canvas implements Runnable { public void run(){ while (true) { // Actualizar el estado del juego repaint(); // Esperar } } public void paint(Graphics g) { // Pintar imagenes } protected void keyPressed(int keyCode) { // Codigo de respuesta a la // pulsacion de una tecla } }La solución es cuando menos poco eficiente y difícilmente obtendremos el resultado deseado. El método run(), ejecutado por un hilo, actualiza el estado del juego. Además, al llamar al método repaint() el sistema llamará al método paint() con lo que se ejecutará otro hilo encargado de pintar las imágenes correspondientes en la pantalla. Por otro lado, cuando se produzca la pulsación de alguna tecla, el sistema llamará a keyPressed() y lo hará utilizando otro hilo independiente de los dos anteriores. Por tanto, tenemos tres hilos diferentes y no habrá manera de que uno de ellos conozca que está ocurriendo con los otros dos en otras partes de la aplicación, con lo que el resultado puede ser una animación defectuosa. Además, si el tiempo que necesita la pantalla para refrescarse es mayor que el tiempo entre llamadas al método repaint(), el resultado será completamente inaceptable para el jugador. GameCanvas ofrece la posibilidad de implementar un juego completo, incluyendo toda su funcionalidad, con un simple bucle controlado por un único hilo. Esto es así ya que permite que los procesos de pintado y gestión de eventos de teclado tengan un nivel de transparencia mayor para el programador. Los mecanismos mediante los cuales GameCanvas nos permite abstraernos en cierta medida de estas tareas son:
OFF-SCREEN BUFFERPara poder entender los métodos de la clase GameCanvas debemos introducir primero el concepto de off-screen buffer. Es una representación de la pantalla que no es visible sino que está almacenada en memoria. A cada instancia de GameCanvas se le asigna en exclusiva un buffer, por lo que es preferible reutilizar un GameCanvas para no desperdiciar la memoria del dispositivo. El tamaño del buffer es el mismo que el del GameCanvas.El buffer se rellena con pixels en blanco al inicializarlo y sólo el objeto GameCanvas puede modificar su contenido. Estas modificaciones se ejecutan sobre el contenido del buffer, pero no son visibles hasta que se indica que su contenido se vuelque a la pantalla, es decir, funciona como una "pantalla virtual". Veremos a continuación como podemos llevar a cabo estas acciones utilizando los métodos de la clase GameCanvas. Los métodos de la clase GameCanvas son:
Una vez conocemos todos los métodos de los que dispone la clase GameCanvas podemos ver el esquema de un juego genérico utilizando esta clase en lugar de Canvas. Conviene prestar atención a las ventajas que presenta este código respecto al que vimos anteriormente: public class JuegoGameCanvas extends GameCanvas implements Runnable { public void run() { Graphics g = getGraphics(); while (true) { // Actualizar el estado del juego. int keyState = getKeyStates(); // Responder a la pulsación de teclas, // repintando aquí por ejemplo. flushGraphics(); // Esperamos. } } } |
|||
|
Layer es la clase básica en el API de juegos de MIDP 2.0. Es una clase abstracta que representa un elemento visual
cualquiera del juego y que tiene como propiedades la posición, el tamaño y la posibilidad de hacerla o no visible.
Si consideramos que un juego consiste básicamente en un fondo con una serie de elementos animados, resulta muy cómodo
crear este tipo de escenas utilizando "capas"(Layers), desplazándolas y haciéndolas visibles o invisibles de manera
independiente. Además, el hecho de ser una clase abstracta permite al programador implementar subclases que ofrezcan una funcionalidad
más específica.
Métodos en la clase Layer:
|
|||
|
La clase
LayerManager
es la que nos permite controlar y tratar conjuntamente con una serie de objetos
Layer
que formen parte de la misma aplicación. La manera de hacerlo es manteniendo una lista ordenada en la que los
Layers pueden ser insertados, accedidos o eliminados.
Esta clase complementa todas las facilidades que Layer nos ofrecía, como la de poder tratar cada elemento gráfico del juego como un estrato o capa independiente del resto. Si no existiera algún mecanismo que permitiese gestionar de forma ordenada tantos elementos gráficos independientes, el concepto de estrato supondría un aumento en la complejidad del proceso de programación de juegos. LayerManager es una superestructura que almacena una serie de objetos Layer y tiene acceso a todos los datos de cada uno de ellos: posición, profundidad (es decir, si se superpone a otro Layer o al revés), estado, etc.
La forma en que se almacenan los objetos Layer no es arbitraria. Están indexados, garantizando así su ordenación en función de la profundidad de cada uno. Es decir, la posición 0 corresponde al Layer más superficial, el más cercano al usuario. Del mismo modo, la última posición corresponderá a la capa más profunda sobre la que se superponen las demás. Esta ordenación se mantendrá incluso en el caso de que se elimine un Layer almacenado, en cuyo caso se reajustan las posiciones de manera que no existan huecos. De acuerdo con esto, el objeto LayerManager representado en la figura anterior daría lugar a una imagen como la siguiente:
VENTANA VISIBLE (View Window) Este nuevo concepto permite controlar el tamaño y la posición (relativa al sistema de coordenadas del objeto LayerManager) de la región visible. Es especialmente interesante cuando una capa contiene una imagen mayor que el tamaño de la pantalla del dispositivo, como por ejemplo un fondo. De esta manera se podrán generar con gran facilidad efectos de barrido (scrolling) y panorámica (panning). Se puede especificar el tamaño de la ventana visible, determinando así el tamaño de la imagen que verá el usuario.
Para mostrar la ventana visible utilizaremos el método paint(Graphics g, int x, int y) que permite indicar en qué posición queremos que pinte la ventana visible. No se modifica el contenido de ésta, simplemente se le indica una posición relativa distinta a la que tiene por defecto (0,0). Este método se explica con más detalle más adelante. Los métodos de la clase LayerManager son:
|
|||
|
TiledLayer es una clase que hereda de
Layer
y que representa un elemento visual compuesto por un conjunto de celdas, cada una de las cuales tiene asociada una imagen
que denominaremos baldosa (tile). Tener un
objeto
TiledLayer
equivale a tener una serie de piezas de un rompecabezas que podremos colocar a
nuestro gusto para obtener una imagen con el aspecto deseado. Esto resulta especialmente útil para imágenes de gran
tamaño, como puede ser el fondo de un juego ya que no será necesario tener una imagen extremadamente grande, sino que
podremos obtenerlo con una serie de celdas combinadas y repetidas convenientemente.
CÓMO CREAR UN OBJETO TiledLayer. CELDAS Y BALDOSAS El método constructor de la clase TiledLayer es TiledLayer(int m, int n, Image i, int tileWidth, int tileHeight). Al invocarlo se divide la imagen indicada en baldosas de un tamaño indicado por tileWidth y tileHeight. Al mismo tiempo se genera un conjunto de celdas que conforman una matriz con m filas y n columnas. El siguiente código muestra cómo sería el proceso: Image image = Image.createImage("/board.png"); TiledLayer tiledLayer = new TiledLayer(10, 10, image, 16, 16);
Cabe hacer una clasificación importante:
Los métodos que podemos encontrar en la clase TiledLayer son:
// Imagen que contiene las baldosas imagen = Image.createImage(< nombre imagen >); // Inicializacion del objeto TiledLayer tl = new TiledLayer(20,11,imagen,16,16); // Asignacion de contenido celda por celda int[] map = {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 1,1,6,1,1,1,1,6,6,1,1,1,1,1,1,1,1,1,1,6, 1,5,3,4,1,1,5,3,3,4,1,1,1,1,1,1,1,1,5,3, 5,3,3,3,4,5,3,3,3,3,4,1,1,1,5,3,3,3,3,3, 3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, 3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2} for (int i = 0; i < map.length; i++) { int column = i % 20; int row = (i - column) / 20; tl.setCell(column, row, map[i]); } // Se fija el estado visible tl.setVisible(true); |
|||
|
Sprite es otra subclase de
Layer y se podría decir que es el concepto dual de
TiledLayer. Si antes generábamos a partir de una imagen varias baldosas, ahora, a partir de varias imágenes (frames)
construirmos una única imagen animada.
CÓMO CREAR UN OBJETO Sprite. GENERANDO UNA ANIMACIÓN Para generar el objeto Sprite que nos proporcione la animación deseada utilizaremos la siguiente expresión: Sprite sprite = new Sprite( imagen, frameWidth, frameHeight); Siendo: imagen: objeto Image creado a partir de un fichero fuente en el que estarán todos los frames que vayamos a necesitar para crear la animación. frameWidth,frameHeight: dimensiones de cada frame. Lo que hacemos al crear el Sprite es dividir la imagen en fragmentos (frames) del tamaño especificado. Por tanto, al diseñar el fichero fuente debemos colocar adecuadamente, controlando la posición y las dimensiones, los que posteriormente serán frames independientes a los que podremos acceder. Para permitir dicho acceso, a cada frame le es asignado un índice al ser creado (comenzando por el 0). Cabe mencionar aquí que podemos construir un Sprite a partir de otro anterior o crear un objeto no animado mediante la expresión: Sprite sprite = new Sprite(Image imagen); Crea el objeto Sprite definiendo un tamaño de frame igual a imagen.getWidth() e imagen.getHeight(), es decir, crea un único frame por lo que el Sprite será no animado.
Una vez descompuesta la imagen fuente podemos definir la forma de la animación. El hecho de crear un objeto Sprite no significa que hayamos creado una animación fija e invariable. Podemos darle forma o modificarla a nuestro parecer indicando cuál es la secuencia de frames a reproducir refiriéndonos a cada frame por su índice. Por ejemplo, para el Sprite de la figura anterior podríamos crear el efecto de avanzar ({0,1,2,3}) o retroceder ({0,3,2,1}). MANIPULACIÓN DE IMÁGENES UTILIZANDO Sprite La clase Sprite nos permite manipular a más bajo nivel las imágenes que posteriormente mostraremos en la pantalla. Las actuaciones que podemos llevar a cabo son:
// Crear un Sprite a partir de una imagen que contiene los frames sprite = new Sprite( |
|||
|
Aplicaciones ejemplo | |||
|
API MIDP/CLDC | |||
|