Razón Artificial

La ciencia y el arte de crear videojuegos

Engine VII: La clase Mapa I

Escrito por adrigm el 3 de junio de 2010 en Desarrollo Videojuegos, Noticias, Programación | 9 Comentarios.

Bien ya tenemos generado nuestro mapa de pruebas, ahora necesitamos cargarlo en Python, los próximos artículos van dedicados a interpretar los datos en Python y la manera de almacenar el mapa.

Para empezar vamos a hacer una copia de nuestra plantilla con el nombre de maps.py que es el archivo que va a contener todo lo relacionado con la carga y creación de los mapas de nuestro engine.

A modo de repaso en mi carpeta engine ahora mismo tengo dos carpetas una llamada graphics que contiene una carpeta tilesets que contiene nuestro tileset de ejemplo, y otra maps que contiene un mapa de prueba. Luego tengo el ejecutable del programa tiled y luego 3 scripts de Python plantilla.py, engine.py y maps.py. Eso es lo que deberían de tener todos si hemos seguido los artículos.

Cargando un mapa

Antes de ponernos a escribir código nos preguntamos, ¿Como representamos un mapa? La técnica ya la explique en el capítulo 3, lo que vamos es crear una clase Mapa que contenga toda la información de este en simple variables, el ancho, el alto, el tamaño de los tiles y lo más importante, una array bidimensional que contenga que tile del tileset va en cada lugar.

Te preguntarás, que como hacemos esto si el tileset que tenemos es una simple imagen png, bien tranquilo, lo que haremos será desde Python coger esa imagen y partirla en “cachitos” (los tiles) y almacenarla en una array, cada tile del array tendrá un índice que, oh sorpresa, se corresponderá con los valores de nuestro array bidimensional del mapa. Todo esto puede parecer lioso, pero ya veremos que no lo es.

Bueno pues aclaradas un par de cosas vamos a empezar a escribir nuestra clase mapas, en un principio solo tendrá un par de métodos de inicialización y carga, pero esta clase crecerá a medida que avanza el engine.

Una última cosa, vamos a trabajar con ficheros XML en Python, es un temga largo y tendido que no cabe dentro de estos artículo, pero os dejo este tutorial donde explica muy bien como tratar estos archivos con Python y extraer la información, de todas maneras yo pondré aquí como extraer la información de nuestro mapa para nuestro engine.

La clase Mapa

Lo primero que necesitamos es inicializar la clase y crear un método que lea nuestro fichero XML y extraiga los datos. Todo esto es muy difícil verlo fuera de conjunto así que voy a poner como va el fichero maps.py entero por ahora y lo comentamos paso a paso.

#!/usr/bin/env python
# -*- coding: utf-8 -*-

# Módulos
from xml.dom import minidom, Node
import base64
import gzip
import StringIO


# Clases
# ---------------------------------------------------------------------

class Mapa:
	def __init__(self, nombre):
		self.nombre = nombre
		self.capas = []
		
		self.cargar_mapa() # Inicializa los valores desde el xml.
		

	# Extrae valores mapa desde XML.	
	def cargar_mapa(self):
		xmlMap = minidom.parse("maps/"+self.nombre)
		nPrincipal = xmlMap.childNodes[0]
		
		# Tamaño mapa
		self.width = int(nPrincipal.attributes.get("width").value)
		self.height = int(nPrincipal.attributes.get("height").value)
		
		for i in range(len(nPrincipal.childNodes)):
			if nPrincipal.childNodes[i].nodeType == 1:
				if nPrincipal.childNodes[i].nodeName == "tileset":
					if nPrincipal.childNodes[i].attributes.get("name").value != "config":
						width = nPrincipal.childNodes[i].attributes.get("tilewidth").value
						height = nPrincipal.childNodes[i].attributes.get("tileheight").value
						nombre = nPrincipal.childNodes[i].childNodes[1].attributes.get("source").value
						nombre = extraer_nombre(nombre)
						self.tileset = nombre
					self.tam_tiles = (int(width), int(height))
				if nPrincipal.childNodes[i].nodeName == "layer":
					if  nPrincipal.childNodes[i].attributes.get("name").value != "colisiones":
						layer = nPrincipal.childNodes[i].childNodes[1].childNodes[0].data.replace("\n", "").replace(" ", "")
						layer = decodificar(layer) # Decodifica la lista
						layer = convertir(layer, self.width) # Convierta en array bidimensional
						self.capas.append(layer)
				if nPrincipal.childNodes[i].nodeName == "objectgroup":
					x = nPrincipal.childNodes[i].childNodes[1].attributes.get("x").value
					y = nPrincipal.childNodes[i].childNodes[1].attributes.get("y").value
					self.start = (int(x), int(y))

# ---------------------------------------------------------------------

# Funciones
# ---------------------------------------------------------------------

# Convierta una array unidimensional en una bidimensional.
def convertir(lista, col):
	nueva = []
	for i in range(0, len(lista), col):
		nueva.append(lista[i:i+col])
	return nueva

# Decodifica y descomprime un mapa.
def decodificar(cadena):
	# Decodificar.
	cadena = base64.decodestring(cadena)
	
	# Descomprimir.
	copmressed_stream = StringIO.StringIO(cadena)
	gzipper = gzip.GzipFile(fileobj=copmressed_stream)
	cadena = gzipper.read()
	
	# Convertir.
	salida = []
	for idx in xrange(0, len(cadena), 4):
		val = ord(str(cadena[idx])) | (ord(str(cadena[idx + 1])) << 8) | \
		(ord(str(cadena[idx + 2])) << 16) | (ord(str(cadena[idx + 3])) << 24)
		salida.append(val)
		
	return salida

# Extra el nombre de un archivo de una ruta.	
def extraer_nombre(ruta):
	a = -1
	for i in range(len(ruta)):
		if ruta[i] == "/" or ruta[i] == "\\":
			a = i
	if a == -1:
		return ruta
	return ruta[a+1:]

# ---------------------------------------------------------------------

def main():
	mapa = Mapa("bosque.tmx")
	print mapa.tileset
	return 0

if __name__ == '__main__':
	main()

En primer lugar importamos todos los módulos que vamos a necesitar: trabajar con XML, descodificar y descomprimir con base64 y gzip y StringIO que nos hace falta también para la descompresión.

Luego creamos la clase Mapa que como vemos de momento tiene dos métodos, el método __init__ que la inicializa, toma como parámetros self (como todas las clases) y nombre, que es el nombre del mapa que queremos cargar. Ojo, que solo le debemos pasar una cadena con el nombre y la extensión tipo “mi_mapa.tmx” ya que más abajo he supuesto que lo busque siempre dentro de la carpeta maps que es donde estarán nuestros mapas.

Luego crea la variables nombre y capas. Esta segunda sera un array que contendrá a su vez varios arrays bidimensionales que representan a cada capa. Más abajo explico como mostrarla y se entenderá mejor.

Por último hacemos una llamada al método cargar_mapa() que hemos definido debajo y que es el que se encarga de extraer los datos del mapa. Vamos con ella.

Recuerda leer el artículo que puse más arriba acerca de como trabajar con XML y mirando el código del método más o menos lo entenderás, cargamos el mapa y extraemos el nodo principal, luego vamos recorriendo los nodos hijos y comprobando que tipo de nodos es (repito, leer el tutorial de XML) y extrayendo la información del árbol XML de nuestro archivo.

Especial atención a la línea 36 donde hace uso de una función que hemos definido más abajo para extraer el nombre del tileset de la ruta completa que se guarda en el archivo. ¿Por qué esto? Pues porque el archivo guarda la ruta absoluta y a nosotros solo nos interesa el nombre del tileset que está usando porque ya nuestro engine sabe en que carpeta buscar los tileset.

Bien llega la línea 41 que es la que obtiene la larga secuencia codificada que teníamos en nuestro archivo, como vemos simplemente cogemos la cadena, le quitamos los saltos de línea y los espacios y la almacenamos en una variable de la clase, luego esa variable la decodificamos y descomprimimos con la función que está más abajo. Explicar esa función sería extendernos demasiado porque es algo complicada, de todas maneras si alguien tiene dudas o interés dejad un comentario y escribiré un artículo sobre ella.

Después de decodificarla y descomprimirla la tenemos almacenada en una array unidimensional, para nuestro mapa mejor la pasamos a una bidimensional con el tamaño del mapa, más abajo está la función que hace esto, nada complicado.

por último, una parte que se tiene que desarrollar, la que obtiene los datos de la capa evento solo esta puesto ahí para sacar el evento que definimos que es el de la posición inicias, esto (y parte de lo de arriba) se ampliará cuando avance el engine.

Por último en la función main() (Para esto es para que la definimos en un módulo) podemos probar creando un objeto mapa con nuestro mapa de ejemplo y podemos probar a imprimir en pantalla los distintos valores que hemos sacado.

Probad a ir paso a paso por el método cargar_mapa y viendo todos los valores que hemos sacado (todos los que son self.algo, son valores del mapa). En el código de arriba he puesto como ejemplo que imprima el nombre del tileset.

Bien con esto ya tenemos algunos datos de nuestro mapa cargados en Python, ahora hay que empezar a trabajar con el tileset y pasar estos datos a Pygame.

9 Comentarios en "Engine VII: La clase Mapa I"

  1. Bline dice:

    Genial tutorial adrigm, ya espero ansioso el siguiente paso. Aunque hay que decir que a pesar de seguir los pasos me da error al ejecutar el map.py diciéndome que en la línea 32 hay un error pues nPrincipal no tiene un método “attributes.get”

    Un abrazo desde Tenerife ;-)

  2. admin dice:

    Que raro, de todas maneras en la próxima entrega subiré lo que yo llevo hasta ahora para que comprueben que todo esta igual.

    Otro abrazo desde Gran Canaria ;)

  3. Bline dice:

    El error, no se por qué lo da desde el childNode[0], si cambio el índice por 1 me imprime sin problemas lo que yo quiera, en este caso siguiendo el EngineIX el nombre del tileset.

  4. inedit00 dice:

    adrigm, me pregunto porque usas XML para leer los datos y no utilizas estructuras JSON. Para no tener que hacer el doloroso ejercicio de tener que recorrerte las clases hijas y extraer la información que necesitas de un XML lo puedes guardar en un diccionario de python que sería algo así:

    maps = {
    ____”width”:22,
    ____”heigth”:44,
    ____”data”: {
    ________”tileset”: {
    ____________”width”:3,
    ____________”height”:3,
    ____________”nombre”:”nombre1″,
    ________},
    ________”layer”:”datos de layer”
    ________”objectgroup”: {
    ____________”x”: 44,
    ____________”y”: 22,
    ________}
    ____}
    }

    Par guardar:
    import simplejson as json
    f = open(‘mapa1.tmx’, ‘w’)
    f.save(json.dumps(maps))
    f.close()

    Para leer:
    f = open(‘mapa1.tmx’, ‘r’)
    data = eval(f.save(json.loads(f.read())))
    f.close()

    Y así tienes una estructura facilmente accesible, de modo que aciendo un
    “data[‘width’]” tienes el with, o haciendo un:
    “data[‘data’][‘objectgroup’][‘x’]” tienes “44”

    Todo tipos de datos nativos en python, facilmente manejables. Es solo un humilde comentario, sin ánimo de cambiar nada, solo dar otro enfoque al mismo problema. Saludos!

  5. admin dice:

    inedit00, pues no conocía JSON, parece una buena idea, aunque de la forma que yo lo he hecho tengo mas control sobre que quiero y no quiero extraer y modificarlo a mi gusto. Pero claro teniéndolo todo en un diccionario es también fácilmente manejable y modificable.

    Miraré esto y veré si es mejor para el engine. Sí es así, lo implementamos.

    Muchas gracias por colaborar, que bonito sería que este engine fuera un poquito de todos.

    Un saludo.

  6. inedit00 dice:

    Gracias, admin. La verdad que es algo muy fácil de implementar, y són estructuras completamente nativas en Python. Todos los tipos de datos que puedas ser convertidos a string, se pueden pasar a un JSON ( Pronunciado Jeison ). Tienen la ventaja de que puedes persistir en el disco el estado de determinadas variables: Por ejemplo, una estructura así sería facilmente convertida a un JSON:

    data {
    ____”nombre”:”admin”
    ____”apellido”:”istrador”
    ____”telefonos”:[“75566″, “+34-887744″]
    }

    Si queremos ampliar los datos, se puede hacer de este modo:
    data[“edad”] = 20

    Así nos quedaría algo así:
    data {
    ____”nombre”:”admin”
    ____”apellido”:”istrador”
    ____”telefonos”:[“75566″, “+34-887744″]
    ____”edad”:20
    }

    La ventaja es que con JSON, se convierten estos datos a string, y así se persisten el el disco, de modo que podemos prescindir de utilizar estructuras “complejas” como el XML.

    Sobre el “coste” que tu dices, es algo irrelevante. Me explico: Si tu tienes un XML, cuando se lee de disco y se envía a “minidom.parse()”, se tiene que parsear todo el fichero. Osea, que lo tienes que cargar todo en memória igual. Parsear XML no es ( computacionalmente hablando ) algo barato. Mucho mas rapido el “eval” de Python.

    Claro está que todo tiene inconvenientes. Entre ellos, que no te da la facilidad de enviar los datos a otro programa hecho con Java, o PHP, por ejemplo, ya que XML ( desgraciadamente ) está mas “estandarizado”. Aún así existen librerías de JSON en la mayoría de los lenguajes.

    Pero bueno, si solo se va a trabajar en Python, es una muy buena elección. Saludos!

  7. Mike dice:

    Hola, tengo una pregunta.

    Sabras como solucionar este problema:

    raise IOError, ‘Not a gzipped file’
    IOError: Not a gzipped file

    La verdad te agradeceria muchisimo si me ayudas :)

    Saludos!

Deja un comentario