Razón Artificial

La ciencia y el arte de crear videojuegos

Engine IX: Cargando el tileset

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

Antes de empezar con este tema he ordenado un poco el código para que esté todo bien estructurado desde el principio que sino luego es liante saber donde está cada cosa, hay rutinas y funciones que usaremos a menudo, como las que usaremos para cargar imágenes sonidos, etc. Por eso he pensado que sería mejor tener todas estas funciones de uso genérico en un fichero llamado funciones.py y ordenarlas por temas y según en que módulo de nuestro engine nos haga falta una u otra cargarla desde ahí.

Por tanto nuestro maps.py nos queda así:

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

# Módulos
import pygame
from pygame.locals import *
from xml.dom import minidom, Node

from funciones import *
from engine import WIDTH, HEIGHT


# 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

# 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():
	return 0

if __name__ == '__main__':
	main()

y tenemos un archivo funciones.py de la siguiente manera:

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

# Módulos
import base64
import gzip
import StringIO
import pygame
from pygame.locals import *


# Mapas
# ---------------------------------------------------------------------

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

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

# Pygame
# ---------------------------------------------------------------------

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

Cortando el tileset

Bien ahora necesitamos coger nuestra imagen de tileset, cortar los tiles que la componen y almacenarlos en un array unidimensional y ese índice coincidirá con el que almacena nuestro archivo mapa.

Primero que nada necesitamos una función para la carga de imágenes en Pygame, ya escribimos una bastante buena en el tutorial de Pygame y esa es la que utilizaremos.

# Carga una imagen transparencia y color tranasparente opcionales.
def load_image(filename, transparent=False, pixel=(0,0)):
        try: image = pygame.image.load(filename)
        except pygame.error, message:
                raise SystemExit, message
        image = image.convert()
        if transparent:
                color = image.get_at(pixel)
                image.set_colorkey(color, RLEACCEL)
        return image

Esta funciión va en nuestro archivo funciones.py pues desde todos lados de nuestro engine vamos a necesitar cargar imágenes, atención al archivo funciones.py que dejé arriba que importa pygame, necesario para esta función y otras.

Bien ya que tenemos la forma de cargar imágenes con python y pygame estamos listos para cortar nuestro tileset. Añadimos una nueva función al funciones.py

# Corta un tilest y lo almacena en un array unidimensional.  
def cortar_tileset(ruta, (w, h)):
	image = load_image(ruta, True)
	rect = image.get_rect()
	col = rect.w / w
	fil = rect.h / h
	sprite = [None]
		
	for f in range(fil):
		for c in range(col):
			sprite.append(image.subsurface((rect.left, rect.top, w, h)))
			rect.left += w
		rect.top += h
		rect.left = 0
		
	return sprite

Bien analicemos. Recibe 2 parámetros, la ruta del tileset y una tupla (w, h) que es el ancho y el alto de nuestros tiles, recuerda que el tileset que estoy usando de ejemplo usamos tiles de 40×40, pero que lo común es encontrar en internet tilesets con tiles de 32×32. De todas maneras nuestro engine se adapta a cualquier tamaño.

A continuación Carga la imagen con nuestra función anterior estableciendo que tiene color transparente y como no definimos de que pixel lo toma pues lo tomará del pixel (0, 0) Por eso es muy importante que nuestros tilesets para este engine el primer tile sea del color que vamos a usar como transparente (un buen color es el (255, 0, 255) que es un rosa intenso muy poco común en las paletas).

La siguiente línea obtiene el rect de la imagen para poder manipularla. A continuación creamos dos variables fil y col que las obtenemos de dividir el ancho y alto del tileset por el ancho y el alto de los tiles, por ejemplo, nuestro tileset tiene de ancho 320px (lo obtenemos con rect.w) y lo dividimos entre w (recuerda que lo recibe como parámetro y en nuestro caso vale 40) 320/40 nos da 8 que es el numero de columnas que hay en nuestro tileset. Lo mismo para el alto y sacar las filas.

A continuación creamos la array sptrite que contendrá nuestro tileset, una lista unidimensional. Observa que tiene un valor nuestra lista none, ¿Por qué esto? Pues veras nuestro mapa guarda las posiciones de los tiles a partir del 1 y el 0 lo toma como que no hay ningún tile en ese cuadro del mapa, es por esto que para que coincidan los índices el valor 0 de nuestro tileset contendrá none, es decir, nada.

luego recorremos las filas y columnas de nuestro tileset y vamos añadiendo al final de nuestra array sprite una subsurfase, ¿Qué es esto? Pues una imagen que se obtiene a partir de otra, es decir, recortamos de una mas grande un trozo de ella y la guardamos. Mirad el bucle y lo entenderéis, es bueno que vayas sustituyendo las variables por los valores de ejmplo para ver cuanto vale cada cosa en cada momento y ver como funciona.

Con esto ya tenemos un array unidimensional que tiene las imágenes de nuestros tiles. Hacemos que la función retorne esta lista.

Ahora hay que usarla en nuestro archivo maps.py Añadimis la siguiente línea en el método init de la clase mapa, justo después de cargar el mapa

self.tileset = cortar_tileset("graphics/tilesets/"+self.tileset, self.tam_tiles)

Como ves le pasamos valores que extraímos de nuestro mapa, el nombre del tileset (Ahora vemos lo que explique de que solo necesitábamos el nombre del mapa porque la ruta ya se la dábamos nosotros) y le pasamos la tupla self.tam_tiles que en nuestro caso vale (40, 40).

Ya no podemos probar la clase mapa dentro del módulo mapa, sino que debemos de hacerlo en el archivo principal, engine.py esto es porque al estar ya tratando con pygame y conversiones de imagen, Pygame necesita saber cosas como la resolución o la profundidad de color del ordenador para convertir las imágenes.

Basta con importar el archivo maps.py en engine.py

from maps import *

y añadir debajo de la carga del reloj, antes del empezar el bucle del juego:

clock = pygame.time.Clock()
	
mapa = Mapa("bosque.tmx")
print mapa.tileset

Con esto cargamos el mapa, en este caso el mío se llama bosque.tmx y con print mapa.tileset podemos ver una lista de sprites que guarda nuestro tileset, atendemos como el primer valor es None y el resto ya es los tiles de nuestro tileset.

Puede que este tutorial haya sido algo lioso por numerosos cambios que hemos hecho, así que dejo para descargar el proyecto como lo llevo hasta ahora, incluyo todo menos el tiled que cada uno debe usar su versión y añadirla a la carpeta.

3 Comentarios en "Engine IX: Cargando el tileset"

  1. Bline dice:

    Esto avanza :) una recomendación es que para guiar mejor en lo que uno hace pongas screens del proceso o de la ejecución de cómo debería quedar al final porque igual más de uno se espera ya ver cargado el mapa en vez de la pantalla en negro y en la consola la ristra de surfaces.

  2. admin dice:

    De momento en pantalla no se muestra nada de nada, todo en negro aún estamos procesando datos, paciencia que pronto veremos nuestro mapa en pantalla.

    Podría poner las salidas de consola, pero ocuparían muchas las largas listas y creo que el que esté siguiendo el tutorial en serio no tendrá problema en verlos.

    De todas maneras si tienes curiosidad, por como trabajo y lo que estoy viendo yo ahora mismo, aquí te dejo una captura: http://img41.imageshack.us/img41/3339/pantallazoyz.png

  3. […] Engine IX: Cargando el tileset […]

Deja un comentario