sábado, 7 de julio de 2012

Utilizando Tile Maps en AndEngine



Primeros pasos en la creación de Vídeos Juego



Hola a todos, si nuevamente por acá, esta vez les voy a hablar de un tema quizás no tan divertido, y como no soy diseñador gráfico, de ninguna manera piensen que puedo crear uno lindo jeje. Pero es muy importante conocerlo y saber como usarlo ya que es una parte fundamental de cualquier juego, así que aquí vamos.

Muchos de los juegos originales de arcade, están basados en Tiled para crear la escena del juego. En este post vamos a ver como manejar estos tiled con AndEngine.

Ahora me imagino que se estarán preguntando, porque usar Tile Maps? (y lo mas importante, también que es un Tile Maps)

Hay dos ventajas principales para utilizar Tile Maps, que fueron muy valiosas para los juegos en las primeras computadoras personales, y lo siguen siendo ahora, sobre todo en dispositivos móviles:

  • Para mucho de los juegos que intentemos armar, cuando diseñemos el escenario del juego, lo mas probable es que deseemos que este sea grande, y además extensible de una manera sencilla. En lugar de crear un bitmaps gigante, que demoremos una eternidad en cargar, y que nos consuma una enorme cantidad de recursos. Podemos utilizar unas pequeñas imágenes (los queridos tiled ) que se se repitan una y otra vez, formando el campo de juego que nosotros queremos y los grande que deseemos (Tile Maps).
  • Una ventaja es que con los Tiled se puede facilitar la detección de colisiones. En estos momentos existen muchas maneras de detectar colisiones, pero con los tiled esto se puede realizar consumiendo muy pocos recursos.

Tipos de Tile Maps:

Un tile maps es una imagen formada por un array polígonos repetidos. Para un tile maps sencillo, los polígonos normalmente son cuadrados. De esta manera podríamos pensar que es similar a una rejilla, donde al colocar los tiled de forma ordenada, colectivamente en conjunto terminaran formando una imagen, que es la que utilizaremos en nuestro escenario de juego.


Tile Maps Ortogonal:

La vista es directamente desde arriba, y los tiled normalmente son cuadrados, aunque a veces también se usan rectangulares. Este tipo de tile maps fue el primer tipo creado y tal vez el mas popular utilizado en los juegos. Este tipo es el mas sencillo, porque en ningún momento hay una superposición de los tiled .




Tile Maps Isométrico:

Este es otro tipo popular de tiled maps, la idea de este es crear una especie de efecto 3D (falso :D ) donde en vez de ver el territorio directamente desde arriba, se ve aproximadamente desde 45º grados.
Con este cambio de vista suceden 2 cosas.
  • El tiled cuadrado se ve transformado en forma de un rombo o diamante.
  • Algunas partes de los tiled mas cercanos, se terminan superponiendo sobre las que se encuentra en la parte posterior a la misma.

Tengamos en cuenta, que a pesar de este lindo efecto, de esta manera no puede lograrse lo mismo que se puede hacer con OpenGL en un juego verdaderamente en 3D.





Estructura de un Tile Maps.

El tiled maps esta formado por 3 partes, el mapa, el conjunto de tiled , y la imagen que provee la textura para los tiled . El mapa en si mismo es una estructura jerárquica.
  • Cada tiled tiene estas características, incluye su ID, que es la posición en el mapa y un ID global que identifica la textura que debería mostrar el tiled desde el Tiled set.
  • Los tiled se asignan a capas, cada una de ellas son una mapa completo de la zona 2D cubiertos por los tiled .
  • Las capas estas se combinan para formar el mapa.

Como dije antes, estos tile maps se han utilizado durante mucho tiempo en juego, y se ha desarrollado un formato estándar para guardar los tiled sets y el resultado de los tile maps. Estos nos sirven en resumen para poder crear y editar mapas sobre la base de estos estándares.

Archivos TMX y TSX:

Los archivos TMX es usado para los tiled maps, y los TSX para los tiled set, ambos tipos de archivos están basados en el formato XML. Los 2 trabajan juntos, donde el la mayoría de los archivos TMX hacen referencia a un archivo TSX para obtener información del lite set. Los archivos TMX se pueden integrar a AndEngine de una manera muy sencilla, se pueden cargar, manipular y procesar mediante un API bastante sencillo.


TMXLoader:

AndEngine incluye la clase TMXLoader que sabe como parsear y cargar archivos TMX y TSX dentro de un objeto TMXTiledMap. Deberíamos usar la clase TMXLoader primero para crear un nuevo objeto, posee varios constructores, este es el mas completo:

TMXLoader(final Context pContext, final TextureManager pTextureManager,
final TextureOptions pTextureOptions,
final ITMXTilePropertiesListener pTMXTilePropertyListener)

Los únicos dos parámetro obligatorios, son Context y TextureManager. Una vez creado el objeto, nosotros podemos usarlo para cargar el tiled map con uno de estos dos métodos:
TMXTiledMap loadFromAsset(final Context pContext, final String pAssetPath)
TMXTiledMap load(final InputStream pInputStream)

El primer método es el mas conveniente a menos que tenga algún problema para abrir el archivo .tmx cualquiera de estos pueden darnos una excepción del tipo TMXLoadException . El retorno es la raíz que vamos a usar para acceder a toda la información del tiled map.

TMXTiledMap es la clase AndEngine que corresponde a un tiled map entero, con todas sus capas, tiled , y las propiedades.
Normalmente no vamos a usar el constructor para crear todo el mapa, sino que vamos tomar el objeto para cargar un tiled como se ha mencionado antes. En cambio, si vamos a usar métodos getter TMXTiledMap para acceder a los contenidos del tiled maps como estos:

int getTileColumns()
int getTileRows()
int getTileWidth()
int getTileHeight()
ArrayList<TMXTileSet> getTMXTileSets()
ArrayList<TMXLayer> getTMXLayers()
ArrayList<TMXObjectGroup> getTMXObjectGroups()
TMXProperties<TMXTileProperty> getTMXTilePropertiesByGlobalTileID(
final int pGlobalTileID)
TMXProperties<TMXTiledMapProperty> getTMXTiledMapProperties()
TMXProperties<TMXTileProperty> getTMXTileProperties(
final int pGlobalTileID)

Y este:
TextureRegion getTextureRegionFromGlobalTileID(final int pGlobalTileID)

Este ultimo método nos permite recuperar el TextureRegion resultante de la creación del tiled maps una vez que se ha cargado.
Los Tile Maps se componen de capas, cada capa tiene su propio mapa de tiled . Ya hemos visto que podemos recuperar la lista de capas en un mapa con un objeto TMXTiledMap. Podemos seleccionar una capa en concreto y utilizar sus métodos para recuperar o establecer información sobre esa capa del mapa.
También podemos usar estos métodos:
TMXTile[][] getTMXTiles()
TMXTile getTMXTile(final int pTileColumn, final int pTileRow)
TMXTile getTMXTileAt(final float pX, final float pY)

Para que nos retorne una referencia a un objeto TMXTile y así podemos usar sus métodos getters y setters, dos métodos interesantes de estos son:

int getGlobalTileID()
void setGlobalTileID(final TMXTiledMap pTMXTiledMap, final int pGlobalTileID)

Si se acuerdan, yo ya había mencionado el ID Global, estos son asignados cuando construimos el tile map, para eso pueden usar por ejemplo el programa Tiled QT, o el que prefieran ustedes ;) Por lo tanto usted puede utilizar este método para manipular las imágenes que se muestran en tiempo de ejecución.

En este post no voy a hablar sobre como armar sus archivos .tmx, o como usar el editor Tiled QT, eso lo podemos llegar a ver mas adelante, o pueden Googlear un poco, ya que no es algo exclusivo de AndEngine. Además que no me gusta mucho el diseño jeje.

Acá les pongo de ejemplo, código fuente java, donde utilizamos el tiled map,


package com.aprendiendo.deandroid.ymas;

imports
/* Acá iría una lista grande de import que te los agrega de manera automática Eclipse, si usan ese IDE ;) */

public class MuestraTIled extends BaseGameActivity {
/* Estas son las constantes */

private static final int CAMERA_WIDTH = 480;
private static final int CAMERA_HEIGHT = 320;
private String tag = "WAVActivity";

private Handler mHandler;
protected Camera mCamera;
protected Scene mMainScene;
private TMXTiledMap mWAVTMXMap;
private TMXLayer tmxLayer;
private TMXTile tmxTile;
private int[] imagenes = new int[50];
private int coffinPtr = 0;
private int mCoffinGID = -1;
private int mOpenCoffinGID = 1;
private Random gen;

/* estos son los metodos que debemos implementar*/

@Override
public Engine onLoadEngine() {
mHandler = new Handler();
gen = new Random();
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() {
this.mEngine.registerUpdateHandler(new FPSLogger());
final Scene scene = new Scene(1);
try {
final TMXLoader tmxLoader = new TMXLoader(
this, this.mEngine.getTextureManager(),
TextureOptions.BILINEAR_PREMULTIPLYALPHA,
new ITMXTilePropertiesListener() {
@Override
public void onTMXTileWithPropertiesCreated(
final TMXTiledMap pTMXTiledMap,
final TMXLayer pTMXLayer,
final TMXTile pTMXTile,
final TMXProperties<TMXTileProperty>
pTMXTileProperties) {
if(pTMXTileProper-
ties.containsTMXProperty("imagen", "true")) {
imagenes[coffinPtr++] =
pTMXTile.getTileRow() * 15 +
pTMXTile.getTileColumn();
if (mCoffinGID<0){
mCoffinGID =
pTMXTile.getGlobalTileID();
}
}
}
});
this.mWAVTMXMap = tmxLoader.loadFromAsset(this,
"gfx/WAV/WAVmap.tmx");
} catch (final TMXLoadException tmxle) {
Debug.e(tmxle);
}
tmxLayer = this.mWAVTMXMap.getTMXLayers().get(0);
scene.getFirstChild().attachChild(tmxLayer);
scene.setOnSceneTouchListener(new IOnSceneTouchListener() {
@Override
public boolean onSceneTouchEvent(
final Scene pScene,
final TouchEvent pSceneTouchEvent) {
switch(pSceneTouchEvent.getAction()) {
case TouchEvent.ACTION_DOWN:
/* Get the touched tile */
tmxTile = tmxLayer.getTMXTileAt(
pSceneTouchEvent.getX(),
pSceneTouchEvent.getY());
if((tmxTile != null) &&
(tmxTile.getGlobalTileID() == mOpenCoffinGID)) {
tmxTile.setGlobalTileID(mWAVTMXMap, mCoffinGID);
}
break;
}
return true;
}
});
mHandler.postDelayed(openCoffin,gen.nextInt(2000));
return scene;
}
@Override
public void onLoadComplete() {
}
private Runnable openCoffin = new Runnable() {
public void run() {
int openThis = gen.nextInt(coffinPtr);
int tileRow = imagenes[openThis]/15;
int tileCol = imagenes[openThis] % 15;
tmxTile = tmxLayer.getTMXTileAt(tileCol*32 + 16,
tileRow*32 + 16);
tmxTile.setGlobalTileID(mWAVTMXMap, mOpenCoffinGID);
mHandler.postDelayed(openCoffin,gen.nextInt(4000));
}
};
}



Después de las importaciones habituales, declaraciones de variables y la configuración del motor, las mayores diferencias que encontramos arrancan en el método onLoadScene (). Tratamos de cargar el Tile Map en AndEngine creando un objeto nuevo y pidiendo TMXLoader para crear el mapa desde el asset. Como vamos a crear el TMXLoader, vamos a sobreescribir el método onTMXTileWithPropertiesCreated () y lo utilizamos para realizar un seguimiento cada vez que se crea un tiled su propiedad se establece en true. Cuando cargamos un tiled, hacemos un seguimiento de su posición en una matriz, int imagenes[].

AndEngine espera encontrar el nombre de archivo .tmx en la carpeta assets, modificado por el llamado setAssetBasePath () en onLoadResources (). Si llegas a estar usando subcarpetas, vamos a tener que editar manualmente los archivos .tsx y .tmx. para agregar la ruta completa de la carpeta assets.

Ejemplo de archivo .tmx:

<?xml version="1.0" encoding="UTF-8"?>
<map version="1.0" orientation="orthogonal" width="15" height="10"
tilewidth="32" tileheight="32">
<tileset firstgid="1" source="gfx/WAV/archivotile.tsx"/>
<layer name="Tile Layer 1" width="15" height="10">
<data encoding="base64" compression="gzip">
H4sIAAAAAAAAC5VQQQ4AMAQTG/9/8rKDpBFlO/RAldYWkQ3wVF9o0QtYw6E2zylBd3cV+6ueD5
4nZG38pMvqoK2yoL6b+fX28mv0xjJMXhhvhDtug+XEWAIAAA==
</data>
</layer>
</map>


Y archivo .tsx:

<?xml version="1.0" encoding="UTF-8"?>
<tileset name="WAVTileset" tilewidth="32" tileheight="32" spacing="2"
margin="2">
<image source="gfx/img/imagenes.png" width="256" height="64"/>
<tile id="4">
<properties>
<property name="imagen" value="true"/>
</properties>
</tile>
</tileset>


Con el tiled map cargado, se recupera la capa 0 (la única que hemos creado). Y la colocamos como un hijo de la actual Scene. Ahora podemos crear un onSceneTouchListener () para capturar eventos del usuario. Cuando un ACTION_DOWN se produce, se recupera el tile tocado de la capa que hemos creado, utilizando el método getTileAt (). Asi que con este método podemos comprobar el ID global del tile, para por ejemplo poder cambiar la imagen que se encuentra en el mismo o generar un evento o una acción, que nosotros pretendamos mostrar.

Tiled Maps Isométrico:

Este tipo de tiled maps puede hacer un juego más interesante, proporcionando una sensación de "3D" sin tener las dificultades que puede tener la creación de los modelos 3D y sus animaciones. El primer desafío que este nos pone es el tema de la elaboración de los tiled para que se vean correctamente desde la perspectiva adecuada. También está el como acomodarlos para que se vea bien en el “eje z”, lo que puede hacer o deshacer la ilusión de “3D”. Algunos artistas gráficos prefieren crear tiled isométricos utilizando modelos 3D, y a continuación, para después colocar la cámara virtual en la correspondiente posición y hacer una renderizado 2D. Esto no tiene nada que ver con la diferencia técnica que comente anteriormente para crear las secuencias de animación de modelos 3D.

Fuentes de referencia:





Este recurso es uno de los mas importantes:


Y una muy buena lectura el libro Learnin Android Game Programing.


Espero que les allá gustado el post, espero no haberlos aburrido :D, mas adelante veremos como manejar colisiones de partículas y demás cosas. Seguramente sera mas divertido, también quería comentarles que no eh puesto el código completo del proyecto porque seria medio engorroso. Pero como pueden ver en las referencias que puse, se pueden bajar mucho código de ejemplo. Nos vemos en la próxima, espero poder hacerlo en poco tiempo.


Saludos a todos, Gabriel E. Pozo