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:
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); | |||
|
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.
Figura 5: Sprite |
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:
- Definir un píxel de referencia
Habitualmente, cuando queremos colocar en la pantalla cualquier elemento gráfico, especificamos su posición indicando las coordenadas dónde se situará la esquina superior izquierda de éste. Ahora que podemos trabajar con distintas capas (objetos Layer, ya sean de tipo TiledLayer o Sprite) y crear escenarios más complejos en los que intervienen un mayor número de elementos visuales independientes, resultaría una labor de encaje el conseguir que la ordenación de todos estos elementos fuera la adecuada para llegar al resultado deseado.La clase Sprite simplifica esta tarea ya que nos permite definir un pixel de referencia y colocarlo en el punto del espacio deseado. También permite conocer cuál es la posición del pixel de referencia y de esta manera concretar, sin más complicaciones, lo que se está mostrando en la pantalla. - Realizar transformaciones sobre la imagen fuente
La posibilidad de modificar ciertas propiedades de la imagen resulta muy útil para aumentar el número de animaciones que podemos crear a partir de una única imagen fuente con un número concreto de frames.Podemos invertir las imágenes según distintos ejes de simetría (horizontal o vertical) o rotarlas un ángulo determinado (90º, 180º, 270º).
- collidesWith (Image image,int x, int y, boolean pixelLevel): comprueba si se ha producido una colisión entre el objeto Sprite y la imagen especificada cuya posición es indicada mediante los enteros x e y. El argumento pixelLevel
indica si se debe utilizar o no detección por niveles de píxel. Esto
es, la colisión sólo se detectará en caso de que los pixels tanto del Sprite
como de la imagen sean opacos. En caso de no utilizar este tipo de
detección tan sólo se comprobará si la intersección del área de
colisión del Sprite y la imagen es no nula.
Para que la colisión pueda ser detectada el objeto Sprite debe ser visible.
- collidesWith(Sprite sprite, boolean pixelLevel): método análogo al anterior con la salvedad de que en este caso la colisión se detecta entre dos objetos Sprite. El funcionamiento de este método es idéntico al anterior.
- collidesWith(TiledLayer tiledLayer, boolean pixelLevel): método idéntico a los dos anteriores, en este caso aplicado a la colisión con un objeto TiledLayer
- defineCollisionRectangle(int x, int y, int width, int height): método que permite definir cuál es el tamaño y la posición del área de colisión (o rectángulo de colisión) del Sprite.
A la hora de comprobar si existe o no colisión con algún otro elemento
visual (utilizando cualquiera de los tres métodos anteriores) sólo se
realizará la comprobación sobre el área que hayamos definido con este
método. Por defecto este rectángulo está definido respecto a la
posición (0,0) (esquina superior izquierda) y con un tamaño igual al
del objeto.
- defineReferencePixel(int x, int y): método con el que podemos definir cuál será el pixel de referencia del objeto Sprite.
Para hacer referencia a un pixel concreto lo haremos a partir de su
posición relativa respecto a la esquina superior izquierda. Por defecto
el pixel de referencia será el (0,0). El hecho de cambiarlo no hará
que el Sprite cambie de posición en la pantalla.
Al llevar a cabo una transformación sobre el objeto Sprite el pixel de referencia se define en relación a la que inicialmente era la esquina superior izquierda.
- getFrame(): devuelve el índice del frame que está siendo mostrado en ese instante.
- getFrameSequenceLength():
devuelve el número de frames que forman la secuencia de imágenes. No
es necesariamente el número de frames distintos que se generaron a
partir de la imagen fuente ya que en una secuencia pueden aparecer
frames repetidos.
- getRawFrameCount(): devuelve el número de frames creados ("frames crudos") a partir de la imagen fuente.
- getRefPixelX(): devuelve la coordenada x del pixel de referencia.
- getRefPixelY(): devuelve la coordenada y del pixel de referencia.
- nextFrame(): selecciona el siguiente frame en la secuencia del objeto Sprite.
Es importante tener en cuenta que el funcionamiento de la secuencia de
frames es circular por lo que si se ejecuta este método cuando nos
encontremos en la última posición de la secuencia se pasará de nuevo a
la primera.
- paint(Graphics g): pinta el objeto Sprite en cuestión. Para que al invocar este método se dibuje deberá estar definido como visible.
- prevFrame(): selecciona el frame correspondiente a la posición index de la secuencia de frames. anterior frame en la secuencia del objeto Sprite.
Dado que, como ya dijimos, la secuencia de frames es circular, al
invocar este método cuando nos encontremos en la primera posición de la
secuencia se pasará directamente a la última.
- setFrame(int index):
selecciona un frame determinado que será indicado por el índice que le
fue asignado al ser creado. Esta acción no será visible hasta que no se
invoque el método paint(Graphics g).
- setFrameSequence(int[] sequence): indica cuál es la secuencia de frames a reproducir. Todos los objetos Sprite
son creados con una secuencia que por defecto reproduce los frames en
orden creciente. Si al invocar este método lo hacemos pasando un
argumento null se volverá a esta secuencia por defecto.
- setImage(Image image, int frameWidth, int frameHeight): cambia la imagen fuente asociada al objeto Sprite.
Se especifica el tamaño de los nuevos frames que se deben crear con lo
que el número de éstos que se generen puede variar respecto al que ya
existía. Se pueden dar dos casos:
- Que la nueva imagen genere un número MAYOR o IGUAL de frames:
En este caso la secuencia de frames que ya existía permanece invariable. Además, el frame que en ese momento esté seleccionado no se modificará hasta que no se invoque el método correspondiente. - Que la nueva imagen genere un número MENOR de frames: En este caso se pasará inmediatamente al frame que ahora tenga el índice 0. Adicionalmente, si la secuencia de frames que existía estaba definida por el programador se reseteará volviendo a la secuencia por defecto.
- Que la nueva imagen genere un número MAYOR o IGUAL de frames:
- setRefPixelPosition(int x, int y): sitúa el objeto Sprite en una posición concreta de manera que su pixel de referencia se sitúe en la posición indicada por (x,y).
- setTransform(int transform): aplica la transformación especificada al objeto Sprite.
A la hora de aplicar una transformación se debe tener en cuenta que las propiedades del Sprite se verán afectadas. Por ejemplo cambián las dimensiones (lo que conocíamos como anchura ahora es altura y viceversa), varía el rectángulo de colisión ya que este permanece "anclado" al pixel de referencia, puede variar la posición del pixel de referencia, etc.
// Crear un Sprite a partir de una imagen que contiene los frames sprite = new Sprite(,15,25); // Declarar cual es la secuencia de frames int[] sequence = {0,1,2,3}; sprite.setFrameSequence(sequence); // Determinar el pixel de referencia y su posicion sprite.defineReferencePixel(refX,refY); sprite.setRefPixelPosition(coordXSprite,coordYSprite); // Hacer el Sprite visible sprite.setVisible(true);
No hay comentarios.:
Publicar un comentario