martes, 11 de noviembre de 2014

J2ME Tutorial Parte IX

PROGRAMACIÓN DEL API DE JUEGOS

 OBJETIVOS
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
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:
  • La posibilidad de acceder directamente al objeto Graphics y de disponer de un buffer específico en el que se podrán representar y tratar las imágenes antes de mostrarlas en la pantalla del dispositivo.
  • La técnica de "polling" (encuesta). Permite acceder directamente al estado de las teclas del dispositivo.

OFF-SCREEN BUFFER

Para 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:
  • flushGraphics(): vuelca el contenido del buffer a la pantalla sin modificar el contenido de éste. El tamaño del área volcada es el mismo que el del GameCanvas.
    El método no finaliza hasta que la imagen ha sido volcada a la pantalla así que la aplicación puede mostrar inmediatamente otra imagen para continuar con la animación. Si el GameCanvas no está visible o no se puede realizar el volcado porque el sistema está ocupado, el método finalizará inmediatamente.

  • flushGraphics(int x, int y, int width, int height): El funcionamiento es idéntico al del método anterior con la salvedad de que en este caso se vuelca en la pantalla únicamente el contenido de la región del buffer especificada. Si especificamos una zona que sale fuera de los límites del GameCanvas sólo se volcará la parte que esté dentro de dichos límites. Si la altura o anchura especificadas son menores que 1 pixel no se volcará ninguna imagen.

  • getGraphics(): devuelve el objeto Graphics que representa la imagen almacenada en el off-screen buffer. Cada vez que se invoca este método sobre una misma instancia de GameCanvas se devolverá un objeto Graphics diferente aunque todos representarán al mismo buffer.
    Sobre este objeto Graphics podemos utilizar todos los métodos que están disponibles en dicha clase, al igual que hacíamos en MIDP 1.0, con una importante diferencia: ahora podemos pintar o modificar imágenes en segundo plano, de manera totalmente transparente al jugador, y cuando hayamos terminado mostrarlas en la pantalla observando tan solo el resultado final. Si se tratase de un objeto Graphics perteneciente a un objeto Canvas las acciones que se realizaran se mostrarían inmediatamente en la pantalla del dispositivo.

  • getKeyStates(): devuelve un entero en el que se codifica qué teclas están pulsadas y cuáles no. Cada bit del entero devuelto representa una tecla y se codificará de la siguiente manera:

    • 1 - Si la tecla está siendo pulsada o lo ha sido alguna vez desde que se hizo la última llamada a este método.
    • 0 - Si la tecla no está siendo pulsada y no se ha pulsado ninguna vez desde la última llamada a este método.

    Este comportamiento de "cerrojo" asegura que cualquier pulsación, por rápida que sea, será capturada independientemente del intervalo de tiempo que utilicemos para verificar periódicamente si ha habido alguna pulsación.

  • paint(Graphics g):pinta el GameCanvas, más concretamente su objeto Graphics. Como ya se ha explicado anteriormente al invocar este método se pintará en el GameCanvas lo que hayamos especificado y esto se ejecutará sobre el contenido del buffer. Para que sea visible debemos invocar el método flushGraphics().
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
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:

  • getHeight(): devuelve la altura del objeto Layer en pixels.

  • getWidth(): devuelve la anchura del objeto Layer en pixels.

  • getX(): devuelve la coordenada horizontal de la esquina superior izquierda del objeto Layer.

  • getY(): devuelve la coordenada vertical de la esquina superior izquierda del objeto Layer.

  • isVisible(): indica si el objeto Layer es visible (true) o no (false).

  • move(int distX, int distY): Mueve el objeto Layer horizontalmente una distancia distX verticalmente una distancia distY. Las coordenadas del objeto están sujetas a "¿wrapping?" si las distancias especificadas hacen que se sobrepasen los valores Integer.MIN_VALUE o Integer.MAX_VALUE.

  • setPosition(int x, int y): mueve el Layer de tal manera que su esquina superior izquierda se situe en el punto (x,y). Si no invocamos este método por defecto estará situado en (0,0).

  • paint(Graphics g): pinta el objeto Layer si está en modo visible, no haciendo nada en caso contrario. Todas las subclases de Layer deben implementar este método. Dichas implementaciones son las responsables de comprobar si el objeto Layer está o no visible. Los atributos del objeto Graphics no se ven alterados como resultado de invocar este método.

  • setVisible(boolean visible): indica si el objeto Layer es visible o no. Es importante principalmente a la hora de invocar el método paint(Graphics g) ya que como se acaba de ver este método sólo se ejecutará cuando el Layer sea visible.


 LayerManager
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.

Figura1
Figura 1: Layer Manager


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:

Figura2
Figura 2: Imagen construida con un objeto Layer Manager




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.

Figura3
Figura 3: Ventana visible (View Window)


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:

  • append(Layer l) añade el Layer especificado a un LayerManager. Le asigna el mayor índice, el que correspondea las capas más profundas. si el LayerManager ya contenía el mismo Layer éste será borrado antes de almacenarlo.

  • getLayerAt(int): devuelve el objeto Layer almacenado en la posición especificada.

  • getSize() devuelve el número de Layers almacenados en un LayerManager.

  • insert(Layer l, int index): inserta el Layer especificado en un LayerManager en la posición indicada por index. Si ya había sido añadido con anterioridad será borrado antes de insertarlo.

  • paint(Graphics g, int x, int y): como ya vimos este método dibuja en la pantalla la actual ventana visible del LayerManager en la posición indicada con las coordenadas x e y. Estas coordenadas son relativas a la posición del objeto Graphics, por lo que desplazarlo supone que la ventana visible se desplace también (pero no su contenido).
    Representará todos los objetos Layer almacenados en orden decreciente, componiendo la escena completa tal y cómo fue diseñada. Las imágenes que estén totalmente fuera de la ventana visible o que pertenezcan a un Layer en estado invisible no serán pintadas.

  • remove(Layer l): elimina del LayerManager el Layer especificado. Si dicho Layer no se encuentra este método no hace nada.

  • setViewWindow(int x, int y, int width, int height): indica cuál es la ventana visible desde el momento que se invoca este método. Será lo que se dibujará en la pantalla al invocar el método paint(....). Hasta que se ejecuta este método por primera vez la ventana visible empieza en (0,0) con una anchura y una altura igual a Integer.MAX_VALUE.

 TiledLayer
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);
  • Celdas
El tamaño de todas las celdas es el mismo (fijado por el tamaño de las baldosas) y la manera de acceder a cada una de ellas es igual a la de cualquier matriz, especificar la fila y la columna en la que se encuentra. El contenido de cada una de estas celdas se indicará mediante un entero que hace referencia a una baldosa en concreto (más adelante veremos cómo se numeran las baldosas). Al inicializarse todas las celdas contienen un 0 lo que significa que está vacía y que será transparente al representarla en la pantalla.

  • Baldosas
Como ya se ha dicho, la imagen fuente se divide en baldosas. No se especifica el número de éstas que queremos sino el tamaño que deben tener. Una vez que hemos descompuesto una imagen en una serie de imágenes más pequeñas necesitamos un mecanismo para acceder a cada una de ellas. Al invocar el método constructor de TiledLayer se asigna inmediatamente un entero a cada baldosa generada. La correspondiente a la esquina superior izquierda es la número 1 y se siguen asignando números consecutivos por filas.

Figura4
Figura 4: Tiled Layer


Cabe hacer una clasificación importante:
    • Baldosas estáticas: son aquellas que tienen asociada una imagen fija que no cambiará durante toda la ejecución.
    • Baldosas dinámicas o animadas: son aquellas que están asociadas a una imagen que puede variar durante el juego. Es muy útil para variar el aspecto de una porción importante de la pantalla sin necesidad de ir cambiando las celdas una por una. Tan solo hay que declarar varias baldosas dinámicas y variar cuando convenga la imagen a la que hacen referencia.
      IMPORTANTE: los identificadores de baldosas dinámicas son siempre enteros NEGATIVOS.


Los métodos que podemos encontrar en la clase TiledLayer son:

  • createAnimatedTile(int staticTileIndex): crea una baldosa dinámica nueva a partir de una estática. El método devuelve el índice que identifica a la nueva baldosa.

  • fillCells(int col, int row, int numCols, int numRows, int tileIndex): rellena una región de la matriz de celdas con la baldosa especificada. Puede ser una baldosa animada, estática o transparente (identificador 0).

  • setAnimatedTile(int animatedTileIndex, int staticTileIndex): asocia a una baldosa animada una determinada baldosa estática. Como ya se ha dicho, a lo largo de la ejecución del programa podemos cambiar el contenido de una baldosa dinámica y la manera de hacerlo es utilizando este método.

  • getAnimatedTile(int animatedTileIndex): devuelve la baldosa a la que hace referencia una determinada baldosa dinámica.

  • setCell(int col, int row, int tileIndex): fija el contenido de una celda asignándole una baldosa que puede ser de cualquier tipo.

  • getCell(int col, int row): devuelve el índice de la baldosa (sea del tipo que sea) que se encuentra en la celda especificada.

  • getCellHeight(): devuelve la altura de las celdas (recordad que es igual para todas ellas).

  • getCellWidth(): devuelve la anchura de las celdas (también idéntica para todas las celdas). Para obtener la altura o anchura total del objeto TiledLayer hay que invocar los métodos Layer.getHeight() y Layer.getWidth();

  • getColumns(): devuelve el número de columnas de la matriz de celdas del objeto TiledLayer.

  • getRows(): devuelve el número de filas de la matriz de celdas del objeto TiledLayer.

  • setStaticTileSet(Image i, int tileWidth, int tileHeight): cambia la imagen fuente sobre la que se creó el objeto TiledLayer. Si el número de baldosas que se generan con la nueva imagen es el mismo que ya existía, los identificadores serán los mismos por lo que el contenido de la matriz de celdas se conservará. Si no es así, todas las celdas perderán su contenido que será puesto a cero (como si se creara un nuevo objeto TiledLayer).

  • paint(Graphics g): dibuja en pantalla el contenido del objeto TiledLayer. No presenta ninguna variación respecto al método paint(....) que vimos en la clase Layer puesto que TiledLayer es una subclase.
El siguiente código muestra un ejemplo de como crear y asignar contenido a un objeto TiledLayer:
// 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
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.
Figura5
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º).
Los métodos que nos ofrece la clase Sprite para ejecutar las acciones anteriomente citadas y algunas más son los siguientes:
  • 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.


  • 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.
El código mostrado a continuación crea y dota de contenido a un Sprite:
// 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