Programar juegos 2d para andorid

He encontrado el siguiente tutorial que explica muy detalladamente cómo crear juegos para Android. El tutorial aún no está terminado del todo, existen secciones todavía a desarrollar, por eso insto a que visitéis la web del autor, que lo irá completando, mientras tanto, dejo aquí lo que lleva de tutorial para ir comenzando a estudiarlo.


Enlazar el proyecto con la librería

AndEngine está generado para funcionar con la versión de Android 1.6 y posteriores. A la hora de crear un proyecto, selecciona un API Level mayor o igual a 4. Recuerda que cuanto más bajo sea ese valor, más versiones de Android podrán ejecutar tu juego.

Bájate la librería de AndEngine desde este enlace:

Una vez creado un proyecto en blanco de Android:

  1. Desde Eclipse, crear un directorio llamado "lib" dentro del proyecto y poner ahí el archivo "andengine.jar" descargado.
  2. Haz clic con el botón derecho del ratón sobre el archivo "andengine.jar" del proyecto y selecciona "Build Path" → "Add to Build Path". Nos aparecerá este archivo también dentro de "Referenced Libraries".
Si tienes alguna duda, en el siguiente vídeo puedes ver cómo se crea un proyecto desde cero:

¡Ya estamos preparados para empezar a programar videojuegos!


La estructura básica de un juego

Para empezar a programar un videojuego necesitas definir una clase que herede de BaseGameActivity.BaseGameActivity es el corazón del juego. Contiene un Engine y se encarga de crear el SurfaceView en el que el contenido del Engine será dibujado. Hay exáctamente un Engine para un BaseGameActivity. Podrás saltar de un BaseGameActivity a otro utilizando los mecanismos de control de Android: Intents.

Veamos un código completamente funcional simplificado al máximo. (Lo único que hace es mostrar la pantalla en color negro)


public class HelloWorld extends BaseGameActivity {
 private static final int CAMERA_WIDTH = 720;
 private static final int CAMERA_HEIGHT = 480;

 private Camera mCamera;

 @Override
 public Engine onLoadEngine() {
  this.mCamera = new Camera(0, 0, CAMERA_WIDTH, CAMERA_HEIGHT);
  return new Engine(new EngineOptions(true, ScreenOrientation.LANDSCAPE, new RatioResolutionPolicy(CAMERA_WIDTH, CAMERA_HEIGHT), this.mCamera));
 }

 @Override
 public void onLoadResources() {
 }

 @Override
 public Scene onLoadScene() {
  final Scene scene = new Scene(1);
  return scene;
 }

 @Override
 public void onLoadComplete() {
 }
}

Por el mero hecho de heredar de BaseGameActivity nos vemos obligados a dar una implementación de:onLoadEngineonLoadResourcesonLoadScene onLoadComplete.

Veamos qué lógica tenemos que dar a cada método:

  • onLoadEngine
    • Nos encargaremos de inicializar el Engine del juego, ya que lo debemos devolver.
  • onLoadResources
    • Aquí cargaremos las imágenes, música, sonidos, fuentes, etc.
  • onLoadScene
    • Aquí prepararemos todos los objetos que entraran en juego.
  • onLoadComplete
    • No he visto ningún ejemplo que lo utilice todavía. Supongo que se ejecutará una vez haya finalizado el método anterior.

Vamos a analizar el código anterior...

Lo primero que definimos son dos constantes que contendrán el ancho (CAMERA_WIDTH) y alto (CAMERA_HEIGHT) de la pantalla de nuestro juego, así como la variable de instancia que hará referencia a la cámara. 
private static final int CAMERA_WIDTH = 720;
 private static final int CAMERA_HEIGHT = 480;

 private Camera mCamera;
Lo bueno de programar un juego en OpenGL es que podemos definir el alto y ancho que queramos. Será OpenGL quien lo reescale para que entre en la pantalla de nuestro movil manteniendo las proporciones. Si quieres que tu juego ocupe la pantalla por completo y no aparezcan ningunas bandas negras arriba y abajo, o en los laterales, asegúrate que estás indicando un alto y ancho proporcional a la resolución de tu pantalla. (Podemos complicarlo un poco más, pero ahora no merece la pena.)

Veamos ahora qué se hace en onLoadEngine.

@Override
 public Engine onLoadEngine() {
  this.mCamera = new Camera(0, 0, CAMERA_WIDTH, CAMERA_HEIGHT);
  return new Engine(new EngineOptions(true, ScreenOrientation.LANDSCAPE, new RatioResolutionPolicy(CAMERA_WIDTH, CAMERA_HEIGHT), this.mCamera));
 }

Creamos la cámara con la resolución que le habíamos dado a la pantalla de juego.
Devolvemos el Engine creado, al cual le hemos indicado que el juego se desarrollará sosteniendo el movil en posición horizontal (ScreenOrientation.LANDSCAPE), además de indicarle que queremos que la pantalla de juego no se deforme si se ejecuta en un terminal con una tasa de aspecto distinta. (Pueden aparecer bandas negras, como muchas películas de cine cuando se visualizan en televisores 4:3)

Si el juego se jugara con la pantalla del movil en posición vertical, indicaríamos ScreenOrientation.PORTRAIT.

Engine hace que el juego se desarrolle en pequeños pasos discretos de tiempo. El Engine se encarga de sincronizar los refrescos en pantalla y de actualizar la escena, la cual contiene todo el contenido que tu juego está manejando activamente. Normalmente hay una escena por Engine, exceptuando cuando utilizamosSplitScreenEngines.

Vamos a terminar con onLoadScene

@Override
 public Scene onLoadScene() {
  final Scene scene = new Scene(1);
  return scene;
 }

Aquí crearemos todos los objetos que aparecerán en el juego. En este caso, crearemos una escena vacía y la devolvemos (al Engine).

Scene es el contenedor principal de todos los objetos que serán dibujados en la pantalla. Una escena tiene una cantidad específica de capas (Layers), las cuales contienen una cantidad (fija o dinámica) de entidades (Entity). Hay subclases como CameraScene HUD MenuScene que siempre se dibujan en la misma posición de la escena sin importar la posición de la cámara.

Vamos a darle un poco de color a la escena poniendo de color verde el fondo. Añadimos una nueva línea aonLoadScene.

@Override
 public Scene onLoadScene() {
  final Scene scene = new Scene(1);
  scene.setBackground(new ColorBackground(0f,1f,0f));
  return scene;
 }

ColorBackground recibe el total de porcentaje del color rojo (0), del verde (1) y del azul (0). Los porcentajes indicados son valores reales que van de 0 a 1, así que es correcto indicar un total de 0.5 para el color rojo. (También podemos establecer un valor para el Alpha como cuarto parámetro.) Te recomiendo que hagas unas cuantas pruebas cambiando los valores con valores decimales entre 0 y 1.

Podemos establecer como fondo de la escena, del más simple al más avanzado: 

  • ColorBackground.
    • Establecemos un color como fondo.
  • EntityBackground.
    • Establecemos una entidad (algo dibujable) como fondo. Como normalmente lo que vamos a utilizar serán sprites, en vez de esta clase, utilizaremos SpriteBackground, que la hereda.
  • SpriteBackground.
    • Para establecer como fondo un sprite (una imagen).
  • RepeatingSpriteBackground.
    • Para establecer un fondo con una imagen que se repite hasta rellenar la pantalla por completo.
  • ParallaxBackground.
    • Para añadir varias capas de fondo y hacer scroll parallax controlado por nosotros.
  • AutoParallaxBackground.
    • Igual que el anterior, sólo que estará siempre en movimiento.

Espero tratar posteriormente estos elementos con mucho más detalle.



Dibujar líneas y rectángulos

En AndEngine tenemos dos tipos de entidades primitivas: Line Rectangle

Vamos a modificar el onLoadScene de la sección "La estructura básica de un juego" para dibujar unas cuantas líneas y rectángulos.

@Override
 public Scene onLoadScene() {
  final Scene scene = new Scene(1);
  
  // Ponemos el fondo de color blanco
  scene.setBackground(new ColorBackground(1f,1f,1f));
  
  // Creamos una línea y un rectándulo
  final Line linea = new Line(0f, 0f, 720f, 480f, 1);
  final Rectangle rectangulo = new Rectangle(180f, 60f, 360f, 360f);
  
  // Establecemos los colores
  linea.setColor(1f, 0f, 0f);
  rectangulo.setColor(0f, 0f, 1f);
  
  // Añadimos las primitivas a la capa superior de la escena
  scene.getTopLayer().addEntity(linea);
  scene.getTopLayer().addEntity(rectangulo);
  
  return scene;
 }

Para construir la línea, especificamos la 'x' e 'y' del punto inicial de la línea y la 'x' e 'y' del punto final, indicando como quinto parámetro el grueso de la línea en píxeles. (En los HTC G1 todas las líneas que se dibujen tendrán un grueso de 1 pixel ya que el adaptador de gráficos del G1 no soporta glLineWidth)

Para crear el rectángulo, indicamos la coordenada de la esquina superior izquierda y el ancho y alto del mismo.

Luego establecemos los colores: La línea con el color rojo y el rectángulo con color azul.

Añadimos los objetos a la capa superior de la escena. ¡Muy importante! El orden en que los añadamos será el orden en el que se dibujarán. Esto será así mientras no toquemos la propiedad Z de los objetos en la escena.

Prueba a cambiar el orden para que primero se dibuje el rectángulo y verás como la línea ya no aparece cortada.

Otra prueba que puedes hacer es: Dibujando primero la línea y luego el rectángulo, hacer el rectángulo transparente al 50%. Esto lo lograrás estableciendo el valor Alpha:

// Puedes establecer el color y el valor alpha en una sola línea.
rectangulo.setColor(0f, 0f, 1f, 0.5f);

// o establecer el color y luego el valor del alpha
rectangulo.setColor(0f, 0f, 1f);
rectangulo.setAlpha(0.5f);

Gracias al método setAlpha puedes cambiar el valor del Alpha cada vez que quieras.



Mostrar textos

// TODO: Aquí va todo lo relativo a cargar fuentes y mostrar texto en la pantalla de juego.



Texturas e Interpolación

Texture es una "imagen" en la memoria del chip gráfico (las imágenes en disco se cargarán en esta zona de memoria). En OpenGL el ancho y alto de una textura tiene que ser potencia de 2 (...32, 64, 128, 256, 512, 1024, etc). Aunque en los ejemplos tiendo a definir la textura cuadrada, no tiene por qué ser así. Puedes tener una textura de 128x32, 32x256, etc.

Por ejemplo, el HTC G1 soporta como mucho, una textura de 1024x1024 (Aunque puedes tener más de una textura a la vez). Intentar utilizar texturas más grandes en este dispositivo provocarán errores en el juego. Recuerda que la primera tarjeta aceleradora gráfica para PC sólo soportaba texturas de 256x256. A pesar de esto Quake 2 se veía bastante bien. ¿No? Mi recomendación es que no pases del valor 1024 para establecer el ancho o alto de tus texturas.

Un parámetro muy importante que hay que indicar a la hora de crear un objeto Texture es un valor TextureOptions que indicará qué método se utilizará para redimensionar las imágenes que tenemos dentro de la textura. (Para no complicar las cosas, dejaremos esa explicación).

Tienes para elegir los siguientes modos:

  • NEAREST:
    La imagen aparecerá pixelada. Es mucho más rápido, pero la imagen tiene menor calidad.

    Original:



    Ampliada con NEAREST:
  • BILINEAR:
    La imagen aparece más difuminada. Es un poco más lento, pero no verás píxeles en la misma.

    Original:


    Ampliada con BILINEAR:
  • REPEATING:
    Si la textura es de 32x32 y tiene que dibujarse sobre una superficie de 64x64, veremos 4 imágenes rellenando ese hueco. Como si pusiéramos la misma imagen una y otra vez repitiéndola. Este modo no funciona con los Sprites normales, así que por ahora, vamos a ignorarlo.

Cuando utilizas imágenes que contienen valores alpha (transparencia) puedes encontrarte con que te aparezcan "halos" que rodean los bordes de las mismas. Aquí es donde entra en juego la técnica "premultiplied alpha", la cual corrige ese problema. Intenta evitar utilizar "premultiplied alpha" a menos que veas cosas raras en los bordes de tus imágenes, como sobras o halos resplandecientes que no deberían estar.

Si quieres una interpolación bilineal, estableces:

TextureOptions.BILINEAR

Si quieres una interpolación bilineal con "premultiplied alpha", estableces:

TextureOptions.BILINEAR_PREMULTIPLYALPHA

Lo mismo ocurre si quieres interpolación NEAREST con o sin "premultiplied alpha". Es cosa tuya explorar los valores que puedes elegir de TextureOptions

Para finalizar, decir que TextureOptions.DEFAULT TextureOptions.NEAREST_PREMULTIPLYALPHA



Cargar los gráficos en memoria

Si las texturas tienen dimesiones en potencia de dos... ¿Entonces no puedo tener en mi juego un gráfico de 5x5? Me explico: Lo que se hace es definir una textura con un ancho y alto (potencia de dos) que tenga el espacio suficiente para contener todos los gráficos de nuestro juego. Podremos tener todos y cada uno de los gráficos por separado, pero luego, a la hora de cargarlos en memoria, los meteremos todos en una textura (cuantas menos texturas tengamos, mejor). Un TextureRegion define un rectángulo en una textura y es utilizado por los Sprites para que el sistema sepa que rectángulo de la textura grande contiene la imagen que representa.

Un Sprite representa a un elemento del juego: El personaje, el fondo, los enemigos, las explosiones, etc. Podemos crear Sprites sin animación (Sprite), sprites con varios estados controlados con código (TiledSprite) o animados (AnimatedSprite). Estos contendrán información sobre su posición (x, y), aceleración, velocidad, rotación etc. de tal forma que sepan donde dibujarse y cómo en cualquier momento. Todo esto se verá posteriormente. Ahora nos vamos a centrar en los Sprite normales.

Estas son las tres imágenes de 32x32 con las que vamos a trabajar en los próximos ejemplos:

          

Arrástralas al algún directorio de tu disco para guardarlas.

Tenemos dos formas de cargar las imágenes dentro de la textura:

  1. Nosotros tenemos que preocuparnos de poner cada imagen dentro de la textura teniendo en cuenta de que entren todas y no se pongan unas encima de otras.
  2. Dejamos que AndEngine las coloque como mejor vea.
    Utilizará este algoritmo:
    http://www.blackpawn.com/texts/lightmaps/default.html

De lo que no nos libramos es de indicar el tamaño suficiente para que la textura pueda albergar todas las imágenes. Recuerda que cuanto más ajustado definas los tamaños, menos recursos desperdiciarás.

Vamos a ver cada forma por separado. Serás tú el que decida con cual te quedas.



Cargar las imágenes en la textura posicionándolas de forma manual

Si tenemos tres imágenes de 32x32, definiremos una textura de 64x64. Recuerda que los valores tienen que ser potencia de dos.

Cargaremos las imagenes en memoria (en la Texture) de esta forma:


Mientras utilicemos interpolación NEAREST no tendremos ningún problema.

Si utilizamos interpolación BILINEAR (de más calidad) tendremos el problema de que a la hora de ampliar las imágenes, los bordes de estas serán afectados por los colores de las imágenes adyacentes. Si ampliáramos la imagen del cesped, nos encontraríamos con esto:


Fíjate en el borde derecho e inferior.

Para evitar esto cuando utilizamos interpolación BILINEAR, lo que hacemos es cargar las imágenes en la textura de forma que haya una separación entre cada imagen. Por ahora no he tenido problemas dejando sólo un pixel de separación entre imágenes.

¿Con qué problema nos encontramos ahora? Pues que las dos imágenes y el pixel de separación suman un ancho de 65 y no entra en una textura de 64, así que el siguiente valor que tendremos que darle a la textura de ancho es de 128. (Lo mismo ocurre para el alto)

Así quedarán las imágenes cargadas en memoria (en la textura de 128x128):


He pintado el fondo con líneas rojas para que te hagas una idea del espacio que estamos desaprovechando en esta textura de 128x128. 

Una vez tenemos todos los conceptos claros, vamos a ver cómo es el código para cargar estas tres imágenes dentro de la textura en memoria. (Nos centraremos en onLoadResources)

Recuerda colocar los tres archivos de imágenes que presentamos antes por separado dentro del directorio de tu proyecto "/assets/gfx". (Si no existiera esta ruta dentro de tu proyecto, créala y pones ahí las imágenes.)
public class Hello extends BaseGameActivity {
  private static final int CAMERA_WIDTH = 720;
  private static final int CAMERA_HEIGHT = 480;
  
  Texture mBloquesTexture;
  TextureRegion mCespedTextureRegion;
  TextureRegion mCajaTextureRegion;
  TextureRegion mCaracajaTextureRegion;

  private Camera mCamera;

  @Override
  public Engine onLoadEngine() {
    this.mCamera = new Camera(0, 0, CAMERA_WIDTH, CAMERA_HEIGHT);
    return new Engine(new EngineOptions(true, ScreenOrientation.LANDSCAPE, new RatioResolutionPolicy(CAMERA_WIDTH, CAMERA_HEIGHT), this.mCamera));
  }

  @Override
  public void onLoadResources() {

    // Creamos la textura donde alojar todas las imágenes
    this.mBloquesTexture = 
      new Texture(128, 128, TextureOptions.BILINEAR_PREMULTIPLYALPHA);

    // Establecemos la ruta dentro del "assets" donde están las imágenes
    TextureRegionFactory.setAssetBasePath("gfx/");

    // Definimos donde cargar cada imagen dentro de la textura
    this.mCespedTextureRegion = 
      TextureRegionFactory.createFromAsset(
        this.mBloquesTexture, this, "backgroundgrass.png", 0, 0);
    this.mCajaTextureRegion = 
      TextureRegionFactory.createFromAsset(
        this.mBloquesTexture, this, "box.png", 32, 0);
    this.mCaracajaTextureRegion = 
      TextureRegionFactory.createFromAsset(
        this.mBloquesTexture, this, "facebox.png", 0, 32);    
    
    // Cargamos las imágenes en memoria (dentro de la textura)
    this.mEngine.getTextureManager().loadTextures(this.mBloquesTexture);  }

  @Override
  public Scene onLoadScene() {
    final Scene scene = new Scene(1);
    scene.setBackground(new ColorBackground(1f,1f,1f));
    return scene;
  }

  @Override
  public void onLoadComplete() {
  }
}
Fíjate en la definición de las variables que van a referenciar la textura (Texture) y las que contendrán información sobre donde están las imágenes dentro esta textura (TextureRegion).
Texture mBloquesTexture;
TextureRegion mCespedTextureRegion;
TextureRegion mCajaTextureRegion;
TextureRegion mCaracajaTextureRegion;
El método onLoadResources es en el que haremos la carga de imágenes, sonidos, música, fuentes, etc.

Lo primero que hacemos es crear un Texture de 128x128 con interpolación bilineal y con "premultiplied alpha". La referenciaremos desde la variable mBloquesTexture. Aquí cargaremos las tres imágenes organizadas tal y como hemos especificado.
this.mBloquesTexture = 
  new Texture(128, 128, TextureOptions.BILINEAR_PREMULTIPLYALPHA);
Mediante la siguiente línea establecemos desde donde se cargarán las imágenes dentro del directorio "assets". Se recomienda poner aquí todas las imágenes que no requieran de "Localización" (que no dependan del idioma). La ruta de carga por defecto es el directorio "assets".
TextureRegionFactory.setAssetBasePath("gfx/");
A continuación establecemos donde se cargará cada imagen dentro de la textura y guardaremos una referencia a la misma como TextureRegion en mCespedTextureRegionmCajaTextureRegion ymCaracajaTextureRegion.

Fíjate que la primera la cargamos en la posición [0,0] de la textura. La segunda en la posición [33,0] (recuerda que estamos dejando un pixel de separación entre imágenes). La tercera en la posición [0,33].

TextureRegionFactory.createFromAsset requiere como primer parámetro, la textura (memoria de vídeo) en la que se cargará la imagen, luego el Context, el nombre del archivo, la coordenada 'x' y la coordenada 'y' donde querremos situar la imagen. (Conociendo la 'x' y la 'y', y el ancho y alto de la imagen que queremos cargar, sabremos el area exacta que ocupará en la textura.)

Si queremos cargar un Drawable desde el directorio "/res/drawable", utilizaremos:TextureRegionFactory.createFromResource(pTexture, pContext, pDrawableResourceID, pTexturePositionX, pTexturePositionY)
this.mCespedTextureRegion = 
  TextureRegionFactory.createFromAsset(
    this.mBloquesTexture, this, "backgroundgrass.png", 0, 0);
this.mCajaTextureRegion = 
  TextureRegionFactory.createFromAsset(
    this.mBloquesTexture, this, "box.png", 33, 0);
this.mCaracajaTextureRegion = 
  TextureRegionFactory.createFromAsset(
    this.mBloquesTexture, this, "facebox.png", 0, 33);    
Una vez tenemos todo definido, hacemos una llamada al TextureManager del Engine pasándole una lista de todas las texturas de las que queramos que inicie la carga. En este caso, sólo tenemos mBloquesTexturecomo textura.

En el caso de tener más de una textura (porque no queramos todos los gráficos con las mismas opciones de textura, o por agrupar los gráficos categorizándolos en texturas genéricas o de un nivel de juego específico), al loadTextures le pasaríamos tantos parámetros como texturas tenemos ya que admite (Texture... pTextures).
this.mEngine.getTextureManager().loadTextures(this.mBloquesTexture);
Con esto tenemos las imágenes cargadas en memoria. Antes de ver cómo mostrarlas en la pantalla del móvil te voy a enseñar la otra forma de cargar las imágenes dentro de una textura.



Cargar las imágenes en la textura posicionándolas de forma automática

En el método anterior, para cargar las imágenes en la memoria de vídeo (Texture) hemos tenido que especificar la posición donde queremos cargarlas dentro de la textura. Vamos a ver ahora una forma de ahorrarnos eso.

La magia de todo esto está en BuildableTexture, que hereda de Texture y la hace un poco más inteligente  ya que sabrá ir colocando las imágenes en su interior según las vaya cargando.

Como comenté antes, no nos seguimos librando de establecer un ancho y alto lo suficientemente grande para que puedan cargarse todas las imágenes y lo suficientemente ajustado al tamaño ideal para no desaprovechar memoria.

Veamos cómo cargar las imágenes en la BuildableTexture.
Recuerda colocar los tres archivos de imágenes que presentamos antes por separado dentro del directorio de tu proyecto "/assets/gfx". 

public class Hello extends BaseGameActivity {
  private static final int CAMERA_WIDTH = 720;
  private static final int CAMERA_HEIGHT = 480;
  
  BuildableTexture mBloquesTexture;  TextureRegion mCespedTextureRegion;
  TextureRegion mCajaTextureRegion;
  TextureRegion mCaracajaTextureRegion;

  private Camera mCamera;

  @Override
  public Engine onLoadEngine() {
    this.mCamera = new Camera(0, 0, CAMERA_WIDTH, CAMERA_HEIGHT);
    return new Engine(new EngineOptions(true, ScreenOrientation.LANDSCAPE, new RatioResolutionPolicy(CAMERA_WIDTH, CAMERA_HEIGHT), this.mCamera));
  }

  @Override
  public void onLoadResources() {
  
    // Creamos la textura donde alojar todas las imágenes
    this.mBloquesTexture = 
      new BuildableTexture(
        128, 128, TextureOptions.BILINEAR_PREMULTIPLYALPHA);   
    // Establecemos la ruta dentro del "assets" donde están las imágenes
    TextureRegionFactory.setAssetBasePath("gfx/");

    // Indicamos las imágenes que queremos cargar
    this.mCespedTextureRegion = 
      TextureRegionFactory.createFromAsset(
        this.mBloquesTexture, this, "backgroundgrass.png");
    this.mCajaTextureRegion = 
      TextureRegionFactory.createFromAsset(
        this.mBloquesTexture, this, "box.png");
    this.mCaracajaTextureRegion = 
        TextureRegionFactory.createFromAsset(
          this.mBloquesTexture, this, "facebox.png");

    // Le indicamos a la textura que gestione donde cargará cada imagen
    try {
      this.mBloquesTexture.build(new BlackPawnTextureBuilder(0));
    } catch (final TextureSourcePackingException e) {
      Debug.e(e);
    }
    
    // Cargamos las imágenes en memoria (dentro de la textura)
    this.mEngine.getTextureManager().loadTextures(this.mBloquesTexture);
  }

  @Override
  public Scene onLoadScene() {
    final Scene scene = new Scene(1);
    scene.setBackground(new ColorBackground(1f,1f,1f));
    return scene;
  }

  @Override
  public void onLoadComplete() {
  }
}
Lo primero que podemos observar es que en vez de Texture, estamos declarando un BuildableTexture.
BuildableTexture mBloquesTexture;TextureRegion mCespedTextureRegion;
TextureRegion mCajaTextureRegion;
TextureRegion mCaracajaTextureRegion;
La construcción del objeto mBloquesTexture tiene exactamente los mismos parámetros que tiene el constructor de la clase Texture. En este caso estamos definiendo en memoria de vídeo una zona de 128x128.

this.mBloquesTexture = 
  new BuildableTexture(
    128, 128, TextureOptions.BILINEAR_PREMULTIPLYALPHA);
La instrucción para indicar el directorio donde están las imágenes no cambia.

A continuación creamos los TextureRegion que contendrán los nombres de los archivos a cargar junto con la posición de estos archivos cargados dentro de la textura. En este momento no le hemos indicado a la textura que gestione donde colocará cada imagen, así que estos TextureRegion no tienen todavía información sobre donde estará cargada la imagen a la que referencia.

Fíjate bien en que esta vez no tenemos que indicar ninguna coordenada ya que BuildableTexture es lo suficientemente inteligente como para ir posicionando las imágenes en la textura según las va cargando.
this.mCespedTextureRegion = 
  TextureRegionFactory.createFromAsset(
    this.mBloquesTexture, this, "backgroundgrass.png");
this.mCajaTextureRegion = 
  TextureRegionFactory.createFromAsset(
    this.mBloquesTexture, this, "box.png");
this.mCaracajaTextureRegion = 
    TextureRegionFactory.createFromAsset(
      this.mBloquesTexture, this, "facebox.png");
En el código siguiente es donde le decimos a la textura que gestione donde poner cada una de las imágenes que se cargarán en ella. A partir de este momento, si todas las imágenes entran correctamente dentro de la textura, todos los TextureRegion contendrán la información de posición de la imagen en la textura.

try {
  this.mBloquesTexture.build(new BlackPawnTextureBuilder(0));
} catch (final TextureSourcePackingException e) {
  Debug.e(e);
}
Si obtuvieras un TextureSourcePackingException significa que tienes que hacer la textura más grande ya que no se pueden cargar todas las imágenes en ella. (Recuerda que las dimensiones de la textura tienen que ir en potencia de dos.)

La línea en la que se cargan todas las imágenes en la textura no cambia con respecto al método manual.

this.mEngine.getTextureManager().loadTextures(this.mBloquesTexture);
Ya conoces las dos formas de cargar las imágenes en memoria. Creo que va siendo hora de mostrar algo en pantalla. ¿No?



Mostrar Sprites en pantalla

Un sprite es una imagen que representa a un elemento del juego con una rotación, unas dimensiones y posición específica en la escena del juego. Tenemos tres tipos de sprites: Sprite (estáticos),AnimatedSprite (animados) TiledSprite (construídos con celdas). Nos centraremos en el primero que es el más básico.

Para el siguiente ejemplo utilizaremos una de las imágenes con las que hemos estado trabajando. En este caso, necesitas poner el archivo "facebox.png" dentro del directorio "/assets/gfx" de tu proyecto Android. Es recomendable acostumbrarse a colocar de forma ordenada los recursos del juego.

Este es el código que carga una imagen y la muestra como objeto en pantalla. Lo único nuevo interesante es lo que está marcado en negrita.
public class Hello extends BaseGameActivity {
  private static final int CAMERA_WIDTH = 720;
  private static final int CAMERA_HEIGHT = 480;
  
  Texture mBloquesTexture;
  TextureRegion mCaracajaTextureRegion;

  private Camera mCamera;

  @Override
  public Engine onLoadEngine() {
    this.mCamera = new Camera(0, 0, CAMERA_WIDTH, CAMERA_HEIGHT);
    return new Engine(new EngineOptions(true, ScreenOrientation.LANDSCAPE, new RatioResolutionPolicy(CAMERA_WIDTH, CAMERA_HEIGHT), this.mCamera));
  }

  @Override
  public void onLoadResources() {
    this.mBloquesTexture = 
      new Texture(32, 32, TextureOptions.BILINEAR);
    this.mCaracajaTextureRegion = 
      TextureRegionFactory.createFromAsset(
        this.mBloquesTexture, this, "gfx/facebox.png", 0, 0);
    this.mEngine.getTextureManager().loadTexture(this.mBloquesTexture);
  }

  @Override
  public Scene onLoadScene() {
    final Scene scene = new Scene(1);
    scene.setBackground(new ColorBackground(0.09804f, 0.6274f, 0.8784f));

    // Creamos el Sprite y lo añadimos a la capa superior de la escena
    final Sprite face = new Sprite(344, 224, this.mCaracajaTextureRegion);
    scene.getTopLayer().addEntity(face);
    
    return scene;
  }

  @Override
  public void onLoadComplete() {    
  }
}
Veamos las partes a destacar en el código...

Observa como definimos un objeto Texture. Esto quiere decir que el posicionamiento de la imagen en la textura cuando la carguemos en memoria será manual. (Para cargar una sola imagen con el método de posicionamiento manual escribes mucho menos código)
Texture mBloquesTexture;
TextureRegion mCaracajaTextureRegion;
A la hora de cargar las imágenes, establecemos la textura con un ancho y alto de 32, exáctamente igual que el de la imagen a cargar.

Por simplicidad tampoco establecemos desde donde se cargarán las imágenes. Por defecto se buscarán en "assets", así que las rutas que especifiquemos serán relativas a este directorio. Observa como en vez de poner "facebox.png" tenemos que poner "gfx/facebox.png" cuando creamos el TextureRegion.

Por lo demás, estamos relizando la carga de imágenes en memoria como explicamos en los capítulos anteriores.
public void onLoadResources() {
    this.mBloquesTexture = 
      new Texture(32, 32, TextureOptions.BILINEAR);
    this.mCaracajaTextureRegion = 
      TextureRegionFactory.createFromAsset(
        this.mBloquesTexture, this, "gfx/facebox.png", 0, 0);
    this.mEngine.getTextureManager().loadTexture(this.mBloquesTexture);
  }
Una vez la imagen está cargada en memoria, vamos a ver qué hacemos en onLoadScene para preparar todos los elementos que aparecerán en la escena.

Creamos una escena con una sola capa. En el constructor del objeto Scene establecemos el número de capas que quieres que tenga.

¿Qué son todos esos números decimales en el constructor de ColorBackground? Para cambiar un poco el color de fondo, lo establezco a un azul muy parecido al color de fondo de escritorio de Windows.

Ahora viene lo interesante: Creamos un Sprite pasándole en su constructor la posición que queremos que tenga en la escena [344, 224] (Esta posición será el centro de la pantalla para el ancho y alto de la cámara establecido), y el TextureRegion que hace referencia a la imagen cargada que queremos que tenga esteSprite.

Una vez tenemos el Sprite creado, lo añadimos a la escena del juego.

El método de instancia getTopLayer de Scene devuelve la capa superior de la escena. Para obtener la capa que está abajo del todo utilizaremos el método de instancia getBottomLayer de Scene. En este ejemplo, nuestra escena la hemos creado sólo con una capa, así que esta será la que está arriba del todo y la que está abajo del todo.
@Override
  public Scene onLoadScene() {
    final Scene scene = new Scene(1);
    scene.setBackground(new ColorBackground(0.09804f, 0.6274f, 0.8784f));

    final Sprite face = new Sprite(344, 224, this.mCaracajaTextureRegion);    scene.getTopLayer().addEntity(face);
    
    return scene;
  }

  @Override
  public void onLoadComplete() {    
  }
}
Ejecutamos la aplicación y veremos cómo aparece el Sprite en el centro de la pantalla.



Backgrounds

Son los fondos sobre los que se dibujarán los personajes y objetos del juego. A la hora de dibujar la pantalla de juego, AndEngine dibujará el Background que hayamos especificado antes que ningún otro gráfico. Recuerda que depende de la complejidad de la escena, organizaremos los gráficos en capas (Layer): La capa que muestra los personajes, la capa que muestra los objetos, la capa de las puntuaciones y el nivel de vida, etc. De hecho, puedes pensar en el Background como el caso especial de la capa que está más abajo. Teniendo en cuenta que la última capa que se dibuja será la que está más arriba.

Si quisiéramos gestionar el fondo de nuestro juego como si fuera una capa más en la escena y trabajar solo con las capas de la misma, llamaríamos al método de instancia setBackgroundEnabled(false) del objetoScene creado. De esta forma, AndEngine hará como si no hubiera Background y empezaría a dibujar la escena por la capa más baja. (Esto no nos impide utilizar la capa más baja para dibujar ahí los gráficos que formen el fondo de pantalla.)

AndEngine nos permite utilizar esta característica, así que tendremos que aprovecharla. 

Tal y como hemos visto en ejemplos anteriores, el Background se asocia a la escena de juego actual en el método onLoadScene. Vamos a ver uno por uno los diferentes tipos de fondos que podemos mostrar:


ColorBackground

Nos será últil cuando queramos mostrar un fondo de color uniforme. Como ya lo hemos visto en varios ejemplos, nos centraremos en los demás tipos de Backgrounds.


SpriteBackground

Este tipo de Background nos será util cuando queramos mostrar una imagen estática en el fondo de la pantalla. Normalmente esta imagen tendrá las mismas dimensiones que la cámara que establezcamos para el juego. Es decir, que a la hora de dibujarse ocupe toda el área de la pantalla.

Supongamos que tenemos una imagen cargada en memoria e identificada con un objeto TextureRegion llamado mBackgroundTextureRegion.
@Override
public Scene onLoadScene() {
  final Scene scene = new Scene(1);
  scene.setBackground(
    new SpriteBackground(
      new Sprite(0, 0, this.mBackgroundTextureRegion)));
    
  return scene;
}
Como se puede observar en el código anterior, establecemos el Background pasándole unSpriteBackground construído a partir de un Sprite en el cual establecemos en la posición [0,0] haciendo referencia a la imagen cargada mBackgroundTextureRegion.

Anteriormente hemos hablado de la posibilidad de desactivar esta la característica del Background en la escena. Veamos un ejemplo de cómo se haría visualmente lo mismo sin tratar con un Background. (Trabajando sólo con las capas)
@Override
public Scene onLoadScene() {
  final Scene scene = new Scene(2);
  scene.setBackgroundEnabled(false);
  scene.getBottomLayer().addEntity(new Sprite(0, 0, this.mBackgroundTextureRegion));
    
  return scene;
}
Lo que hemos hecho ha sido:

  1. Crear el objeto Scene con dos capas. Puedes crear tantas como te hicieran falta. En este caso una para el fondo y otra para todo lo demás, si fuéramos a dibujar algo mas que el fondo.
  2. Deshabilitar el Background en la escena.
  3. Obtener la capa que se encuentra más abajo de las dos (la primera que se dibuja) y añadir un Spritecuya esquina superior izquierda se encontrará en la posición [0,0] de la escena y que representa la imagen que queremos de fondo de pantalla.

Esto es como en todo: Muchos caminos llevan hasta Roma. ¿Cual utilizar? Yo recomiendo utilizar siempre que sea posible un Background (del tipo que más te convenga) y olvidarse de trabajar los fondos de pantalla dentro de capas. Esta sería una forma de abstraer sutilmente la tarea de dibujar el fondo de la pantalla de las demás tareas de dibujado.


RepeatingSpriteBackground

Este Background nos vendrá muy bien cuando queramos mostrar una imagen que se repita tantas veces hasta rellenar la pantalla. (Es útil para dibujar agua, suelos de tierra, de cesped, etc)

Por ejemplo, imagínate que queremos rellenar el fondo de la pantalla completamente de cesped teniendo como imagen esto: (Para guardarla en disco, arrástrala en cualquier carpeta. Recuerda que para probar los ejemplos, deberás incluirla en el proyecto de Android dentro del directorio "/assets/gfx" )

El tamaño de esta imagen es de 32x32, así que para rellenar la pantalla completa se tendrá que repetir tantas veces como sea necesario. Algo que se repite muchas veces para cubrir un área específica se le conoce por el nombre de Textura. Vamos a dejar esta tarea al procesador gráfico, ya que la realizará de forma bastante eficiente. Esto quiere decir que en este caso no trabajaremos con Sprites, sino que trabajaremos con la texturas en sí. Los valores de las dimensiones de las texturas tienen que ser potencia de dos, y no tienen por qué ser cuadradas.

Recuerda que para cargar las imágenes que luego utilizábamos dentro de Sprites, creábamos primero un objeto Texture. Luego creábamos objetos TextureRegion para asignar imágenes (en archivos) a una zona concreta de esa textura y para finalizar le decíamos al TextureManager que cargara en la textura todas las imágenes que habíamos especificado anteriormente en los TextureRegion.

Para este Background sólo tenemos que cargar una imagen, que será la que ocupe por completo la textura que nos servirá para rellenar toda la pantalla. Esto lo haremos en onLoadResources cargando la imagen dentro de un objeto RepeatingSpriteBackground. Luego, podemos pasar este objeto al métodosetBackground del objeto Scene.

Veamos un ejemplo completo:
public class Hello extends BaseGameActivity {
  private static final int CAMERA_WIDTH = 720;
  private static final int CAMERA_HEIGHT = 480;
  
  RepeatingSpriteBackground fondo;

  private Camera mCamera;

  @Override
  public Engine onLoadEngine() {
    this.mCamera = new Camera(0, 0, CAMERA_WIDTH, CAMERA_HEIGHT);
    return new Engine(new EngineOptions(true, ScreenOrientation.LANDSCAPE, new RatioResolutionPolicy(CAMERA_WIDTH, CAMERA_HEIGHT), this.mCamera));
  }

  @Override
  public void onLoadResources() {
    this.fondo = new RepeatingSpriteBackground(
      CAMERA_WIDTH, CAMERA_HEIGHT, this.mEngine.getTextureManager(), 
      new AssetTextureSource(this, "gfx/backgroundgrass.png"));
  }

  @Override
  public Scene onLoadScene() {
    final Scene scene = new Scene(1);
    scene.setBackground(this.fondo);
    
    return scene;
  }

  @Override
  public void onLoadComplete() {    
  }
}
A la hora de crear el RepeatingSpriteBackground le pasamos:

  • Las dimensiones. En este caso, como queremos que ocupe la pantalla completa, serán las mismas dimensiones que la cámara.
  • El TextureManager, ya que será necesario para cargar la textura en memoria.
  • La referencia a la imagen que queremos cargar en la textura. La textura se generará con las mismas dimensiones que la imagen.

Para terminar, le indicamos al objeto Scene que queremos utilizar este RepeatingSpriteBackground.


ParallaxBackground


// TODO:


AutoParallaxBackground


// TODO:



Sprites animados

Hasta el momento hemos visto como podemos mostrar imágenes estáticas en nuestro juego. Con esto ya se podría abarcar un buen número de videojuegos pero, no nos engañemos, las necesidades de un producto multimedia van más allá. ¿Qué sería de un videojuego sin unas realistas e impactantes animaciones?

AndEngine pone a nuestra disposición la infraestructura necesaria para cargar y ejecutar animaciones y, como verás lo hace de una manera realmente fácil e intuitiva.

A efectos normales un sprite animado no deja de ser un sprite, así que es necesario que hayas leído y entendido qué son los sprites, cómo funcionan y cómo los gestiona AndEngine.

A modo de repaso rápido, podemos decir que el ciclo de vida de un sprite se podría resumir en:

  1. Carga de la imagen. (Usando una textura como contenedor).
  2. Creación.
  3. Manipulación. (Transformaciones, animaciones externas, animaciones internas, etc...).
  4. Muerte. (Liberación de recursos).

Para un sprite animado también este será nuestro guión.

Antes de nada vamos a presentar nuestro personaje a animar. Tomaremos una animación de ejemplo deAndEngine.

  

Como podrás observar AndEngine hace uso de la animación por frames. Un frame es la unidad mínima de una animación, es decir, será la imagen que se verá en un determinado tiempo.

La imagen de ejemplo tiene cuatro frames dispuestos en dos columnas y dos filas. Es importante esta organización, puesto que según sea construiremos nuestro sprite animado de una forma u otra. Como comentario sobre la imagen, no tiene por qué ser una única imagen para cada sprite. En una misma imagen pueden convivir varios sprites distintos con sus respectivas animaciones. Aún así, es recomendable que cadasprite tenga su propia zona común en el fichero, si se decide usar uno único; es decir, los frames de cadasprite estarían dispuesto de forma consecutiva.

Vamos a ver un pequeño ejemplo en el que crearemos un sprite, crearemos una animación asociada a esesprite y la mostraremos en pantalla (por comodidad sólo se muestra la parte del código interesante, obviando importaciones, métodos no implementados, etc..).
public class AndEnginePruebas extends BaseGameActivity {

  private static final int CAMERA_WIDTH = 320;
  private static final int CAMERA_HEIGHT = 200;

  private Camera mCamera;
  private Texture mTexture;
  private TiledTextureRegion helicoptero;

  @Override
  public Engine onLoadEngine() {
    mCamera = new Camera(0, 0, CAMERA_WIDTH, CAMERA_HEIGHT);

    return new Engine(new EngineOptions(true, ScreenOrientation.LANDSCAPE,
      new RatioResolutionPolicy(CAMERA_WIDTH, CAMERA_HEIGHT),
      this.mCamera));
  }

  @Override
  public void onLoadResources() {
    mTexture = new Texture(128, 128,
      TextureOptions.BILINEAR_PREMULTIPLYALPHA);

    helicoptero = TextureRegionFactory.createTiledFromAsset(this.mTexture,
      this, "gfx/helicopter_tiled.png", 0, 0, 2, 2);

    mEngine.getTextureManager().loadTexture(this.mTexture);
  }

  @Override
  public Scene onLoadScene() {
    final Scene scene = new Scene(1);
    scene.setBackground(new ColorBackground(0.09804f, 0.6274f, 0.8784f));

    float centerX = (CAMERA_WIDTH - (this.helicoptero.getWidth() / 2)) / 2;
    float centerY = (CAMERA_HEIGHT - (this.helicoptero.getHeight() / 2)) / 2;
    final AnimatedSprite heliVolador = new AnimatedSprite(centerX, centerY, this.helicoptero);

    heliVolador.animate(new long[]{100L, 100L}, 1, 2, true);

    scene.getTopLayer().addEntity(heliVolador);

    return scene;
  }
}
La estructura del código es exactamente la misma que la que hemos visto en temas anteriores. De hecho, si no fuera por una nueva clase, el código sería idéntico al que vimos cuando tratamos el tema de sprites.

Para cargar la imagen de nuestro helicóptero, hacemos uso de la factoría TextureRegionFactory y de uno de sus métodos estáticos: createTiledFromAsset

Referenciaremos nuestra imagen con un objeto TiledTextureRegion. Este objeto se utiliza cuando cargamos una imagen formada por muchas imágenes. Crearemos este objeto dándole indicaciones de las filas y columnas que forman la matriz de imágenes. A partir de ese momento, podremos acceder a cada una de las imágenes que contiene de forma independiente.

Este método (hay varios, os invitamos a que miréis el código fuente de la librería para ver las distintas opciones) nos fabrica el contenido de una textura con la imagen, por lo tanto los parámetros más interesantes serán la textura donde se cargará la imagen (el objeto mTexture), la ruta donde está la imagen a cargar (debe estar en el directorio de recursos /assets/ a no ser que le hayamos indicado a AndEngine otro lugar), la posición 'x' e 'y' donde cargar esta imagen en la textura y el número de columnas y de filas que tiene la imagen que cargamos. (En este caso, tenemos cuatro helicópteros en la imagen organizados en dos filas y dos columnas.)

AndEngine automaticamente dividirá esta imagen en el número de filas y columnas indicados para extraer cada fotograma (frame)La numeración de cada fotograma será de izquierda a derecha y de arriba hacia abajo. De esta forma, el fotograma número cero hace referencia al helicóptero que está detenido (La imagen superior izquierda). El número uno (superior derecha) y el número dos (inferior izquierda) a diferentes fotogramas del helicóptero en funcionamiento, y el forograma número cuatro (inferior derecha) hace referencia al helicóptero accidentado.
Una vez tenemos la imagen cargada, creamos un objeto AnimatedSprite que representará a nuestro helicóptero, pudiendo tener 4 estados. En este caso, aprovechamos el constructor del objeto para indicarle la posición donde queremos que aparezca en la escena. (tantos como fotogramas hayamos cargado)

En cualquier momento, podemos cambiar la posición de un Sprite utilizando el método setPosition del mismo. (Tambien aplicable a AnimatedSprite)

Finalizamos indicando al helicóptero que queremos que se visualice animado llamando al método animate. Esta animación tendrá dos fotogramas que durarán cada uno 100ms. La animación irá del fotograma número 1 al número 2 y se repetirá indefinidamente o hasta que se llame otra vez al método animate.

La base de uso de los sprites animados que ofrece AndEngine es la que hemos visto. Como complemento podemos añadir que podemos detener la animación de nuestro sprite en cualquier momento con hacer la llamada al método stopAnimation. De este método disponemos de una especialización que nos permite indicar el frame que queremos que muestre el sprite una vez detenida la animación en curso. Esto nos puede ser especialmente útil para crear transiciones entre distintas animaciones cuando estas depende de entradas del usuario.

Además, el API nos ofrece un método para determinar el estado de la animación, es decir, saber si nuestrosprite está mostrando una animación o ya ha finalizado. Para esto último, disponemos del métodoisAnimationRunning, que nos devuelve un valor lógico: true false.

Por lo tanto, si deseáramos saber cuándo ha finalizado la animación de un sprite (por supuesto que no tenga activada la iteración infinita) podríamos preguntar en cada iteración de nuestro juego si la animación sigue en curso. Este proceso, puede resultar bastante engorroso, por lo que AndEngine acude a nuestra ayuda para facilitarnos las cosas: listener.

Como comentamos al comienzo de esta sección, la clase AnimatedSprite ofrece en su interfaz una serie de métodos animate. Dependiendo de las necesidades de nuestro código, algunos nos servirán mejor que otros. De entre esos métodos, existe un grupo que tiene como parámetro de entrada un interfaz declarado como interfaz interno de AnimatedSpriteIAnimationListener.

Mediante la implementación de este interfaz podemos indicar qué hacer cuando la animación asociada a unsprite ha finalizado. Como puedes ver, es lo que obteníamos con nuestro interrogatorio a la clase medianteisAnimationRunningsin embargo al usar este pequeño patrón de diseño nos evitamos el tener que estar pidiendo a la clase que compruebe su estado. Al indicarle nosotros qué tiene que hacer cuando acaba la animación, le pasamos todo el trabajo (y la responsabilidad) a la clase. Fácil, ¿verdad?.

Vamos a ver todo esto que hemos dicho con un pequeño ejemplo. Usaremos como código base el ejemplo que hemos visto en esta sección. Por lo tanto, sólo indicaremos las partes de código que hay que añadir. En el ejemplo se mostrará a nuestro helicóptero haciendo un vuelo de dos segundos ( 2 frames * 100 ms * 10 iteraciones). Cuando se llegue al fin de la animación, nuestro helicóptero detendrá los rotores y descenderá en caída libre. Cuando llegue al suelo quedará destrozado.

La imagen que usaremos en el ejemplo será la misma que hemos visto en la sección. Este es el código con lo más interesante resaltado en negrita.
    heliVolador.animate(new long[]{100L, 100L}, 1, 2, 10, 
      new AnimatedSprite.IAnimationListener(){

        @Override
        public void onAnimationEnd(final AnimatedSprite sprite) {
          sprite.setVelocityY(40.0f);
        }
      }
    );

    scene.registerUpdateHandler(new IUpdateHandler() {

      @Override
      public void onUpdate(float arg0) {

        //Si hemos llegado al suelo... crash.
        if (heliVolador.getY() + heliVolador.getHeight() >= CAMERA_HEIGHT){
          heliVolador.setVelocityY(0.0f);
          heliVolador.animate(new long[]{100L, 100L}, 3, 4, false);
        }
      }

      @Override
      public void reset() {}

    });
Sólo hemos cambiado el método animate que vamos a usar y hemos registrado en la escena un nuevo actualizador, para que se ejecute en cada iteración. Como ves, el nuevo método tiene un parámetro adicional, que es la implementación del interfaz IAnimationListener. Este interfaz dispone únicamente de un método:onAnimationEnd, que es donde debemos indicar qué va a ocurrir cuando la animación termine su ejecución. Este método nos ofrece como parámetro de entrada el sprite animado. Para nuestro ejemplo, lo único que haremos será aplicar un movimiento de caída al helicóptero (sprite.setVelocity(40.0f)).

Por lo tanto, cuando finalice las diez iteraciones de las que está compuesta nuestra animación de dosframes, nuestro objeto heliVolador ejecutará el código del método del interfaz: onAnimationEnd.

Para finalizar, en el bucle de nuestro "juego", miramos cuándo el helicóptero llega al suelo. Cuando esto ocurra (heliVolador.getY() + heliVolador.getHeight() >= CAMERA_HEIGHT), detenemos el movimiento de caída del helicóptero (heliVolador.setVelocityY(0.0f)) y mostranos una nueva animación con nuestro helicóptero destrozado. Como es una animación única, desactivamos la repetición de esta.


Interactuando con el juego

// TODO: Aquí se listarán todas las formas de interactuar con el juego.


Interactuando con pulsaciones de teclas

// TODO: Aquí se explicará cómo detectar pulsaciones de teclas.


Interactuando con la pantalla tactil

// TODO: Aquí se explicará cómo detectar pulsaciones en la pantalla. (No multitactil)


Interacciones multitáctiles

// TODO: Aquí se explicará cómo gestionar cuando hay más de un dedo en la pantalla.


Utilizando un gamepad virtual

// TODO: Aquí se explicará cómo crear un gamepad virtual para manejar el juego.



Música y efectos de sonido

Si consideramos un videojuego como un programa multimedia debemos equiparar el sonido a, por ejemplo, el apartado gráfico o a la jugabilidad. La capacidad del sonido para impregnar la vivencia de juego de sensaciones y emociones es similar al mejor de los gráficos. Es cierto, sin embargo, que el "sonido" en la programación para dispositivos móviles siempre se ha considerado prescindible. En un primer momento por las capacidades de los dispositivos y, posteriormente, por el modo de juego al que van dirigidos estos programas.

Esto ha cambiado, radicalmente, con la llegada de nuevos dispositivos con capacidades multimedia sonoras de gran calidad y, con el uso masivo de smartphones, el usuario ha cambiado su modo de juego de un modo casual a usar su teléfono como consola de videojuegos, por lo que cada vez el usuario es más exigente con un cuidado apartado sonoro.

Con AndEngine podrás dotar a tu juego de toda la potencia de Android a la hora de reproducir sonidos.AndEngine te ofrece una capa por encima de la programación sonora que el API de Android pone a nuestra disposición. Te podrás estar preguntando que si sólo es una capa, ¿por qué no usar directamente la API deAndroid? La respuesta no es sencilla, sin embargo debes considerar que utilizar la capa de abstracción deAndEngine permitirá hacer la programación mucho más uniforme y te ahorrará, muchas veces, el tener que programar tus propias clases de utilidades.

Así pues, si podemos hacer lo mismo que hace Android, podremos reproducir un amplio abanico de formatos multimedia sonoros: AAC, MP3, Ogg Vorbis (OGG),  MIDI y el eterno WAV.

Además de estos formatos, AndEngine incluye una expansión (hace uso de la librería externa XMP) para poder reproducir ficheros MOD (MOD, S3M, IT...). El uso de esto lo veremos en la sección dedicada a las expansiones de AndEngine.

AndEngine divide la capacidad sonora en música (Music) y efectos de sonido (Sound). Normalmente la música no se cargará por completo en memoria y se irá reproducciendo en flujo, mientras que los efectos de sonido si estarán cargados en memoria (normalmente tienen un tamaño pequeño) para poder hacer un uso rápido de ellos, puesto que requieren un acceso rápido. Sin embargo, como veremos a continuación, esa distinción es prácticamente transparente para nosotros, puesto que, salvo por el uso de clases de distinto nombre, el uso de las mismas es casi idéntico.

Una vez finalizada la pequeña introducción, veamos qué necesitamos hacer para poder usar sonido conAndEngine. Haremos una pequeña introducción literal y luego veremos en un ejemplo todo lo que hemos dicho.

Cada vez que queramos usar en nuestra aplicación sonidos seguiremos este guión:

  1. Activar el sonido en AndEngine.
  2. Cargar los ficheros que contienen la música o los efectos de sonido.
  3. Configurar el sonido: continuidad, repeticiones, volumen...
  4. Operaciones sobre el sonido: reproducir, detener, pausar, continuar la reproducción...

Para avisar a AndEngine que vamos a usar sonido debemos activar dos opciones en la claseEngineOptions que, como vimos, es la clase que usamos para construir un objeto de tipo Engine. A estas dos opciones se acceden mediante dos métodos setters:
EngineOptions engineOptions = new EngineOptions([...]); 
   engineOptions.setNeedsMusic(true);
   engineOptions.setNeedsSound(true); 
AndEngine utiliza estos dos atributos "activadores" para saber si tiene que construir dos clases de utilidades: MusicManager SoundManager. Estas dos clases son las encargadas de hablar con la API deAndroid.

Aprovechando la referencia que hemos hecho a esas dos clases, vamos a tocar un tema que puede pasar desapercibido, porque realmente no sea necesario modificar, o no. Con AndEngine se puede ejecutar efectos sonidos de forma concurrente. Por defecto, y de forma interna, AndEngine define un número máximo de concurrencia en cinco. Por lo tanto, podemos tener en reproducción música y cinco efectos de sonido sonando a la vez. En principio, esto es más que suficiente para la programación de cualquier juego, sin embargo si por necesidad del proyecto tienes que ampliar ese parámetro, te comentamos cómo poder ampliar ese parámetro. Exceptuando la opción más evidente que es modificar la constante en la claseSoundManager, una opción más elegante es no usar los atributos activadores de la clase Engine, como hemos hecho en nuestro código ejemplo, y construir nuestro propio SoundManager. Esta clase ofrece un constructor SoundManager(final int pMaxSimultaneousStreams) que recibe como parámetro el número de canales concurrentes que queremos tener a nuestra disposición. En la carga de los archivos, especificaremos el MusicManager SoundManager del propio Engine, o una referencia al que hayamos creado.

Para cargar los archivos sonoros, AndEngine nos ofrece dos factorías para hacerlo, una para música y otra para efectos sonoros. Cada una de estas factorías disponen de varios métodos para obtener el archivo de varias formas, como vimos al cargar ficheros gráficos. En nuestros ejemplos usaremos la cargar de archivos desde el directorio /assets/, pero también puedes hacerlo desde un fichero de recursos Android.

Las clases factorías para cargar los archivos son: MusicFactory SoundFactory. Para cargar un archivo de música usaremos el método estático createMusicFromAsset de la clase MusicFactory y para cargar un archivo de efecto de sonido usaremos el método estático createSounFromAsset de la claseSoundFactory. Veamos un ejemplo con un corte del código que veremos más adelante.

Music musica = MusicFactory.createMusicFromAsset(mEngine.getMusicManager(), this, "mfx/50.mid");

Sound helices = helices = SoundFactory.createSoundFromAsset(mEngine.getSoundManager(), this, "sfx/copter.ogg"); 

Como veis, el código es auto explicativo. Para construir un fichero sonoro usamos el método de la factoría que necesitemos, en este caso los ficheros estarán en el directorio /assets/ de nuestro paquete. Estos métodos, reciben como parámetros el gestor de música y sonido de nuestra clase Engine (si hubiéramos necesitado usar un gestor de sonido propio, como vimos al tratar el tema de la concurrencia de sonidos, este sería el que debemos usar aquí), la propia clase y la ruta relativa a /assets/ donde estén nuestros ficheros. En este ejemplo cargaremos dos ficheros sonoros. Un fichero de música tipo MIDI que está situado en /assets/mfx/ y con el nombre 50.mid y un fichero de sonido de tipo Ogg Vorbis situado en /assets/sfx/ y de nombrecopter.ogg.

Nota: no es necesario indicar qué tipo de archivo queremos cargar. Android reconocerá el tipo de archivo que es por la extensión en el nombre del archivo. Si no es un tipo de archivo que reconoce, te dará una excepción en la carga o devolverá null en la construcción.

La configuración de los sonidos se puede hacer en la carga de estos o durante el bucle de nuestro juego. Entre las operaciones principales tenemos:

  • Control de volumen: getVolumesetVolume y sus especializaciones en volumen izquierdo y derecho.
  • Número de repeticiones: setLoopCount (número de repeticiones) y setLooping (repetición continua).

Algunos métodos están sólo disponible para sólo algún tipo de objeto de sonido (Music Sound). Os invitamos a que miréis la API de las distintas clases para ver las pequeñas diferencias.

Operar sobre los sonidos cargados es igual de sencillo. AndEngine ofrece una serie de métodos para realizar todas las acciones que podemos necesitar sobre un sonido, ya sea música o efecto de sonido. Estos métodos son: play (reproducir), stop (detener la reproducción completamente), pause (detener la reproducción y tener la posibilidad de continuarla), resume (continuar la reproducción en la última pausa) y release(descargar el fichero de sonido).

Como ocurría con los métodos de configuración, existen ciertos métodos únicos para una u otra clase de sonido (Music Sound). Por ejemplo, la clase Music ofrece un método para situarte en una posición concreta de un fichero de música (método: seekTo) que en un fichero de sonido no tendría mucha utilidad. Otro método interesante de Music es isPlaying que nos dice si está en ejecución.

Nota: al igual que vimos en el apartado de reproducción de sprites animados, podemos asociar un listener al objeto de clase Music para que ejecute código cuando finalice la reproducción. En este caso tendremos que implementar un listener de la API de AndroidOnCompletionListener.

Para finalizar este apartado, vamos a ver todo lo que hemos dicho en un único ejemplo. El ejemplo continuará el código que vimos en el apartado de sprites animados. En él, activaremos el apartado sonoro deAndEngine, cargaremos una música de ambiente (usaremos MID por el menor tamaño de estos ficheros en comparación con OGG) y un par de efectos de sonidos en formato Ogg Vorbis: hélices rotando y un efecto de golpe y rotura de cristales que sonará cuando nuestro helicóptero caiga al suelo.

Los archivos de sonido son: 50.midcopter.ogg y crash.ogg.

Como siempre, resaltamos el código nuevo en negrita y obviamos partes de código (importaciones, métodos vacíos...) para facilitar la lectura.
public class AndEnginePruebas extends BaseGameActivity {

    private static final int CAMERA_WIDTH = 320;
    private static final int CAMERA_HEIGHT = 200;

    private Camera mCamera;
    private Texture mTexture;
    
    private TiledTextureRegion helicoptero;

    private Music musica;
    private Sound helices;
    private Sound crash;
    private boolean destruido = false;
    
    @Override
    public Engine onLoadEngine() {

      this.mCamera = new Camera(0, 0, CAMERA_WIDTH, CAMERA_HEIGHT);
      EngineOptions engineOptions = new EngineOptions(true,
             ScreenOrientation.LANDSCAPE,
             new RatioResolutionPolicy(CAMERA_WIDTH, CAMERA_HEIGHT),
             this.mCamera); 

      engineOptions.setNeedsMusic(true);
      engineOptions.setNeedsSound(true);
      
      return new Engine(engineOptions); 
    }


   @Override
   public void onLoadResources() {

      this.mTexture = new Texture(128, 128,
                  TextureOptions.BILINEAR_PREMULTIPLYALPHA);

      helicoptero = TextureRegionFactory.createTiledFromAsset(this.mTexture,
                  this, "gfx/helicopter_tiled.png", 0, 0, 2, 2);

      this.mEngine.getTextureManager().loadTexture(this.mTexture);

     //Carga de música y sonido.
     try {
       musica = MusicFactory.createMusicFromAsset(mEngine.getMusicManager(),
                  this, "mfx/50.mid");
       musica.setLooping(true);        
       helices = SoundFactory.createSoundFromAsset(mEngine.getSoundManager(),
                  this, "sfx/copter.ogg");
       helices.setLooping(true);
       

       crash = SoundFactory.createSoundFromAsset(mEngine.getSoundManager(),
                  this, "sfx/crash.ogg");       crash.setLooping(false);

    } catch (IOException ioe) {
       //No se ha podido cargar el fichero. Realizar las operaciones
       //oportunas.
    }
   }

   @Override
   public Scene onLoadScene() {

       this.mEngine.registerUpdateHandler(new FPSLogger());
       final Scene scene = new Scene(1);
       scene.setBackground(new ColorBackground(0.09804f, 0.6274f, 0.8784f));

       final AnimatedSprite heliVolador = new AnimatedSprite(0f, 0f, this.helicoptero);

       float centerX = (CAMERA_WIDTH - (this.helicoptero.getWidth() >> 1)) >> 1;

       float centerY = (CAMERA_HEIGHT - (this.helicoptero.getHeight() >> 1)) >> 1;

       heliVolador.setPosition(centerX, centerY);
       heliVolador.animate(new long[]{100L, 100L}, 1, 2, 10, 
             new AnimatedSprite.IAnimationListener(){
               @Override
               public void onAnimationEnd(final AnimatedSprite sprite) {
                    helices.stop();
                    helices.release();
                    sprite.setVelocityY(40.0f);                  
               }
             }
       );

       scene.registerUpdateHandler(new IUpdateHandler() {
              @Override
              public void onUpdate(float arg0) {
                   //Si hemos llegado al suelo...
                   if (heliVolador.getY() + heliVolador.getHeight() >= CAMERA_HEIGHT && !destruido) {
                        destruido = true;
                        crash.play();
                        heliVolador.setVelocityY(0.0f);
                        heliVolador.animate(new long[]{100L, 100L}, 3, 4, false);

              }

       }

             @Override
             public void reset() {}                 
       });

   scene.getTopLayer().addEntity(heliVolador);
   return scene;
}

@Override
public void onLoadComplete() {

        try {
            Thread.sleep(1000);
        } catch(InterruptedException ie){}

        this.musica.play();
        this.helices.play();
    }
}
En el ejemplo vemos cómo activamos en uso de sonido al construir el objeto Engine y cómo cargamos los ficheros en el método onLoadResources. Al cargar los archivos debemos capturar la excepciónIOException por si no están disponibles los ficheros a cargar.

La reproducción de la música de fondo y las hélices la hacemos en el método onLoadComplete. Que será justo antes de mostrarse la escena del juego.

Nota: algunas veces la carga del archivo se solapa con la reproducción del mismo. En este caso, ocurre con el sonido de las hélices. Para solucionarlo, hacemos una pequeña pausa de un segundo antes de la reproducción. Esto es sólo necesario la primera vez y sólo si queremos reproducir sonidos inmediatamente tras la carga de la escena. Para ver lo que queremos decir, puedes eliminar la pausa de un segundo que hacemos mediante Thread.sleep(...) y lanzar el ejemplo. Observarás que el sonido de las hélices no se reproducirá.



Mejorar el rendimiento del juego

En esta sección veremos una serie de mejoras que puedes realizar en tu proyecto para obtener un mejor rendimiento.


Evitar dibujar el fondo de la ventana

Cuando programamos videojuegos en Android, por lo general, no necesitaremos que se dibuje el fondo (a nivel de sistema) de la ventana de la actividad. Como los gráficos de nuestro juego se dibujarán encima ocultándolo, no merece la pena gastar tiempo de proceso en esa tarea. Por consiguiente, conseguiremos una mayor tasa de refrescos de pantalla por segundo.

Para indicarselo a Android, deberemos crear el siguiente archivo "theme.xml" y alojarlo dentro del directorio "res/values" de nuestro proyecto.
<?xml version="1.0" encoding="UTF-8"?>
<resources>
    <style name="Theme.NoBackground" parent="android:Theme">
        <item name="android:windowBackground">@null</item>
    </style>
</resources>
Sólo falta añadir en el Manifest.xml el atributo

android:theme="@style/Theme.NoBackground"

al tag <application /> para aplicar este tema a todas las actividades de la misma, o al tag <activity /> en caso de que quieras aplicarlo sólo a determinadas actividades.

No hay comentarios:

Publicar un comentario

Gracias por comentar en mi blog. Saludos.