Cómo hacer un motor de Tiles – XNA – Parte 3 – Editor


En internet podemos encontrar varios ejemplos de cómo crear una aplicación “hibrida” y poder combinar la potencia de XNA con un formulario de Windows Forms:

La que más me gusto fue la que encontré en el libro XNA 4.0 Development by Example por el señor Kurt Jaegers, se los recomiendo mucho ya que tiene 4 juegos explicados paso a paso.

En el ejemplo, se muestra como cambiar el handle sobre la que se pinta el dispositivo de XNA y dejarlo en un control PictureBox del formulario.

Así, que voy a continuar con la serie de Cómo hacer un motor de Tiles, esta vez les mostraré como crear el Editor para crear Mapas, guardarlos en XML y luego cargarlos nuevamente.
Para empezar se agrega un nuevo proyecto de Windows Game al proyecto con el que se venía trabajando, se renombra como TileEditor, y luego se adiciona la referencia del proyecto TileEngine, se selecciona como proyecto inicial.

Se selecciona el proyecto TileEditor, y el menú adicionar, luego se busca un elemento Windows Form y se llama MapEditor, luego se dibuja un Picturebox sobre el formulario y se llama pctSurface.

En el código del formulario (click derecho sobre cualquier lugar del formulario y clic en View Code), se adiciona la referencia using Microsoft.Xna.Framework; y se crea una instancia publica de la clase Game1 llamada game:

public Game1 game;

Ahora se debe declarar el pictureBox pctSurface como público para que pueda ser visto desde otras clases, se dirige al archivo MapEditor.Designer.cs, este archivo se muestra al expandir el archivo MapEditor, luego se declara como publico el picturebox:

public System.Windows.Forms.PictureBox pctSurface;

En el mismo proyecto, buscar el archivo Program.cs y abrirlo, reemplazar el método main() por lo siguiente:

static void Main(string[] args)
{
MapEditor editor = new MapEditor();
editor.Show();
editor.game = new Game1(editor.pctSurface.Handle, editor, editor.pctSurface);
editor.game.Run();
}

Luego se abre la clase Game1.cs y se adicionan las siguientes declaraciones:

IntPtr drawSurface;
System.Windows.Forms.Form parentForm;
System.Windows.Forms.PictureBox pictureBox;

Se modificar el constructor de la clase con el siguiente:

public Game1(IntPtr drawSurface, System.Windows.Forms.Form parentForm, System.Windows.Forms.PictureBox pictureBox)
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
this.drawSurface = drawSurface;
this.parentForm = parentForm;
this.pictureBox = pictureBox;
graphics.PreparingDeviceSettings += new EventHandler(graphics_PreparingDeviceSettings);
Mouse.WindowHandle = drawSurface;
}

Adicionar el Evento graphics_PreparingDeviceSettings:

void graphics_PreparingDeviceSettings(object sender, PreparingDeviceSettingsEventArgs e)
{
e.GraphicsDeviceInformation.PresentationParameters.DeviceWindowHandle = drawSurface;
}

Al ejecutar el proyecto se verá algo como esto:

Lo que ha sucedido, es que al modificar la clase Program.cs que es el punto inicial del juego, se le ha indicado que se muestre el formulario al iniciar el proyecto, y al modificar el constructor de la clase Game1, se le esta enviando el Windows Handle del PictureBox para que sea dibujado allí el juego, también se ha creado un evento que preparará el dispositivo grafico al drawSurface, al Mouse también se le ha indicado que las coordenadas que reporte solo serán cuando se encuentre sobre el PictureBox.

Como ven, existe un problema y es que se está quedando una ventana vacía abierta, está ventana era la que contenía el juego XNA, pero está vacía porque hemos movido la salida de la aplicación XNA al PictureBox.

Para corregirlo, se debe dirigir a la clase Game1, y adicionar la siguiente declaración:

System.Windows.Forms.Control gameForm;

Ahora se agrega lo siguiente al constructor, justo debajo de lo anterior:

gameForm = System.Windows.Forms.Control.FromHandle(this.Window.Handle);
gameForm.VisibleChanged += new EventHandler(gameForm_VisibleChanged);
gameForm.SizeChanged += new EventHandler(gameForm_SizeChanged);

Se Adiciona el método gameForm_VisibleChanged y el gameForm_SizeChanged:

void gameForm_VisibleChanged(object sender, EventArgs e)
{
 if (gameForm.Visible == true)
 {
 gameForm.Visible = false;
 }
}
void gameForm_SizeChanged(object sender, EventArgs e)
{
graphics.PreferredBackBufferWidth = pictureBox.Height;
graphics.PreferredBackBufferHeight = pictureBox.Width;
graphics.ApplyChanges();
}

Lo que se hizo fue ocultar la ventana en la que anteriormente se dibujaba el juego de XNA, y cuando se cambia el tamaño de la ventana, se cambian las propiedades para no perder las proporciones.

Cuando finalicen la aplicación, verán que la aplicación no deja de estar en modo Debug, es porqué la ventana donde antes se dejaba el contenido del juego XNA, aún se encuentra abierta, esto se corrige adicionando al evento FormClosed de la ventana:

private void MapEditor_FormClosed(object sender, FormClosedEventArgs e)
{
game.Exit();
Application.Exit();
}

Ahora se modifica el formulario para que quede como la siguiente imagen:

El Editor consta de dos PictureBox, el del lado derecho va a mostrar la aplicación de XNA, y el de la izquierda que se encuentra dentro de un panel, va a tener la imagen del TileSheet y va a ser dividido por una grilla para poder seleccionar el Tile y dibujarlo en el mapa, también se tiene una lista de las capas, para adicionar más de una capa, son checkbox para que cuando se seleccione se vuelva activa y cuando se quite el check se vuelva inactiva y no se dibuje, se tienen dos botones, uno para cargar el TileSheet y otro para agregar la Capa.

El menú Archivo es útil cuando vayamos a guardar y cargar el mapa en archivo xml, la opción Nuevo mapa va a abrir una nueva ventana donde seleccionaremos el alto y ancho del mapa, además del tamaño de los Tiles, los Scrolls van a servir para mover el Mapa.

Se elimina de la clase TileEngine, en el método LoadContent lo siguiente:

centroPantalla = new Vector2(
(float)graficos.GraphicsDevice.Viewport.Width / 2f,
(float)graficos.GraphicsDevice.Viewport.Height / 2f);

En la clase TileMap se dejan algunas variables como propiedades publicas:

public Int32 NumXTiles { get; set; }
public Int32 NumYTiles { get; set; }
public Int32 AnchoCelda { get; set; }
public Int32 AltoCelda { get; set; }

Se Modifica el constructor reemplazando la inicialización de las variables por las nuevas propiedades:

this.NumXTiles = numXTiles;
this.NumYTiles = numYTiles;
AnchoCelda = tileAncho;
AltoCelda = tileAlto;

Se Modifican algunas propiedades:

public Int32 obtenerCeldaporPixelX(Int32 pixelX)
{
return pixelX / AnchoCelda;
}
public Int32 obtenerCeldaporPixelY(Int32 pixelY)
{
 return pixelY / AltoCelda;
}
private void obtenerVisibilidad()
{
// obtengo los puntos de la pantalla
min.X = obtenerCeldaporPixelX((int)posicionCamara.X);
max.X = obtenerCeldaporPixelX((int)posicionCamara.X + (int)(tamanoPantalla.X / zoom)) + 1;
min.Y = obtenerCeldaporPixelY((int)posicionCamara.Y);
max.Y = obtenerCeldaporPixelY((int)posicionCamara.Y + (int)(tamanoPantalla.Y / zoom)) + 1;
// limitamos los valores
min.X = (int)MathHelper.Clamp((float)min.X, 0, NumXTiles);
min.Y = (int)MathHelper.Clamp((float)min.Y, 0, NumYTiles);
max.X = (int)MathHelper.Clamp((float)max.X, 0, NumXTiles);
max.Y = (int)MathHelper.Clamp((float)max.Y, 0, NumYTiles);
}

Se Crean dos nuevos métodos en la clase TileMapLayer que será utiles para limpiar la capa y para rellenarla con un solo Tile seleccionado:

public void limpiar(int ancho, int alto)
{
 for (int i = 0; i < ancho; i++)
 {
  for (int j = 0; j < alto; j++)
  {
   mapa[i][j] = new Tile(0);
  }
 }
}
public void rellenarConTile(int ancho, int alto, Tile tile)
{
 for (int i = 0; i < ancho; i++)
 {
  for (int j = 0; j < alto; j++)
  {
   mapa[i][j] = tile;
  }
 }
}

Ahora para ver un ejemplo de lo que se lleva hasta ahora, se va a la clase Game1 del proyecto TileEditor y se adiciona en las declaraciones lo siguiente:

public TileEngine motor;
private Boolean mapaCreado = false;

Luego en el método LoadContent:

protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
Services.AddService(typeof(SpriteBatch), spriteBatch);
Services.AddService(typeof(ContentManager), Content);
Services.AddService(typeof(GraphicsDeviceManager), graphics);
}

Se crea un método que creara un nuevo mapa, por el momento se llenarlo con un solo Tile, el método recibe como parámetros el ancho, alto del mapa y el tamaño de los Tiles, al finalizar deja en true la variable que indica que ya hay un mapa creado, también deben ver que el tamaño de la cámara se debe inicializar con el tamaño del PictureBox donde se muestra la aplicación:

public void crearMapa(Int32 ancho, Int32 alto, Int32 tamanoTile)
{
motor = new TileEngine(this, ancho, alto, tamanoTile);
motor.mapa.tamanoPantalla = new Vector2(pictureBox.Width, pictureBox.Height);
Camara2D.TamanoPantalla = motor.mapa.tamanoPantalla;
Camara2D.altoTile = tamanoTile;
Camara2D.anchoTile = tamanoTile;
Camara2D.numXTiles = ancho;
Camara2D.numYTiles = alto;
Texture2D spriteSheet = Content.Load("tiles2");
motor.tileSheets.Add(new TileSheet(spriteSheet));
//agregar capas
motor.mapa.agregarCapa(new TileMapLayer(ancho, alto, motor.tileSheets[0], true));
motor.mapa.agregarCapa(new TileMapLayer(ancho, alto, motor.tileSheets[0], true));
//agregar rectangulos que representan los Tiles en los TileSheet
motor.tileSheets[0].adicionarRectangulo(1, new Rectangle(0, 0, motor.TamTiles, motor.TamTiles));
motor.tileSheets[0].adicionarRectangulo(2, new Rectangle(motor.TamTiles, 0, motor.TamTiles, motor.TamTiles));
motor.tileSheets[0].adicionarRectangulo(3, new Rectangle(motor.TamTiles * 2, 0, motor.TamTiles, motor.TamTiles));
motor.tileSheets[0].adicionarRectangulo(4, new Rectangle(motor.TamTiles * 3, 0, motor.TamTiles, motor.TamTiles));
motor.tileSheets[0].adicionarRectangulo(5, new Rectangle(motor.TamTiles * 4, 0, motor.TamTiles, motor.TamTiles));
motor.tileSheets[0].adicionarRectangulo(6, new Rectangle(motor.TamTiles * 5, 0, motor.TamTiles, motor.TamTiles));
motor.tileSheets[0].adicionarRectangulo(7, new Rectangle(motor.TamTiles * 6, 0, motor.TamTiles, motor.TamTiles));
motor.tileSheets[0].adicionarRectangulo(8, new Rectangle(motor.TamTiles * 7, 0, motor.TamTiles, motor.TamTiles));
motor.tileSheets[0].adicionarRectangulo(9, new Rectangle(motor.TamTiles * 8, 0, motor.TamTiles, motor.TamTiles));
for (int i = 0; i < ancho; i++)
{
for (int j = 0; j < alto; j++)
{
//rellenar las capas con los Tiles
motor.mapa.tileMapLayers[0].adicionarTile(i, j, new Tile(1));
}
}
mapaCreado = true;
}

Y en el método Draw:

protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
if (mapaCreado)
{
spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend);
motor.Draw(gameTime);
spriteBatch.End();
}
base.Draw(gameTime);
}

Ahora, en la clase MapEditor, hacemos doble clic sobre la opción Nuevo Mapa del menú Archivo, en ese evento llamamos la creación del nuevo mapa:

private void nuevoMapaToolStripMenuItem_Click(object sender, EventArgs e)
{
game.crearMapa(100, 100, 48);
}

Al ejecutar la aplicación se ve:

El problema es que los ScrollBars no son útiles aún, y no se puede mover a través del mapa.

Para que los ScrollBars tomen cómo límites el tamaño del mapa, se crea una función que va a fijar los scrollbars, en el código del form MapEditor:

private void FijarValoresScrollBar()
{
Camara2D.TamanoPantalla = new Vector2(pctSurface.Width, pctSurface.Height);
Vector2 zero = Vector2.Zero;
Camara2D.Mover(ref zero);
vScrollBar1.Minimum = 0;
vScrollBar1.Maximum = Camara2D.rectMundo.Height - (int)Camara2D.TamanoPantalla.Y;
hScrollBar1.Minimum = 0;
hScrollBar1.Maximum = Camara2D.rectMundo.Width - (int)Camara2D.TamanoPantalla.X;
}

Ahora, justo después de llamar el método crearMapa:

private void nuevoMapaToolStripMenuItem_Click(object sender, EventArgs e)
{
game.crearMapa(100, 100, 48);
FijarValoresScrollBar();
}

Lo que se hizo fue ajustar la cámara al tamaño del PictureBox donde se dibuja la aplicación XNA y se ajustan los límites del ScrollBar, para que solo se mueva igual al tamaño del mapa.

Ahora lo que falta es que al mover los ScrollBars se mueva el mapa, esto se puede hacer muy fácil, ya que en el constructor de la clase game1, hemos enviado el formulario como parámetro, así que lo que se debe hacer es en la clase game1 crear dos variables de tipo VScroll y HScroll e inicializarlas con los controles del formulario:

En las declaraciones de la clase game1:

System.Windows.Forms.VScrollBar vscroll;
System.Windows.Forms.HScrollBar hscroll;

En el constructor de la clase game1:

vscroll = (System.Windows.Forms.VScrollBar)parentForm.Controls["vScrollBar1"];
hscroll = (System.Windows.Forms.HScrollBar)parentForm.Controls["hScrollBar1"];

En el método Update:

Camara2D.Posicion = new Vector2(hscroll.Value, vscroll.Value);
if (mapaCreado)
{
if (Camara2D.aCambiado)
{
motor.camaraCambiada();
}
}

La posición de la cámara va a ser el valor de las barras de desplazamiento, luego modificamos la cámara.

Al compilar ya verán que cuando se mueven las barras, el mapa también e mueve.

Ahora lo que falta, es cargar una imagen y que sea mostrada en el Editor, luego la imagen va a ser dividida en Tiles iguales y cuando se seleccione uno y luego se dé clic en la aplicación XNA, se dibuje dicho Tile:

En la vista diseño de la clase MapEditor, en el código del botón btnCargarTileSheet:

private void btnCargarTileSheet_Click(object sender, EventArgs e)
{
 String directorio = Application.StartupPath + @"\Content\";
 {
 openFileDialog1.Title = "Seleccionar Imagen TileSheet";
 openFileDialog1.Filter = "Image Files|*.jpg;*.gif;*.bmp;*.png;*.jpeg|All Files|*.*";
 openFileDialog1.InitialDirectory = directorio;
 openFileDialog1.CheckFileExists = true;
 openFileDialog1.CheckPathExists = true;
 if (openFileDialog1.ShowDialog() == DialogResult.OK)
  {
   Bitmap tileSheet = new Bitmap(openFileDialog1.FileName);
   pbImageSheet.Image = tileSheet;
   pbImageSheet.Size = tileSheet.Size;
  }
 }
}

Lo que se intenta hacer es abrir un archivo de imagen a través de un cuadro de dialogo, y luego con la clase Bitmap asignarla al pictureBox del TileSheet.

Cuando se ejecuta el botón, va a ocurrir una excepción “Current thread must be set to single thread apartment (STA) mode before OLE calls can be made. Ensure that your Main function has STAThreadAttribute marked on it. This exception is only raised if a debugger is attached to the process.” Está excepción se corrige adicionando a la clase Program, en el método main el atributo STAThread:

[STAThread]
static void Main(string[] args)
{
MapEditor form = new MapEditor();
form.Show();
form.game = new Game1(form.pctSurface.Handle, form, form.pctSurface);
form.game.Run();
}

Al ejecutar se puede buscar una imagen y seleccionarla:

Ahora se dibuja una grilla cada vez que se cargue una imagen, la grilla va a ser del tamaño de los Tiles, para ello se usa el evento Paint del PictureBox pbImageSheet:

private void pbImageSheet_Paint(object sender, PaintEventArgs e)
{
 {
 //Dibujar la Grilla
 Pen miLapiz = new Pen(System.Drawing.Color.Gray);
 for (int y = 0; y < this.pbImageSheet.Height; y += 32)
  {
  e.Graphics.DrawLine(miLapiz, 0, y, this.pbImageSheet.Width, y);
  }
 for (int x = 0; x < this.pbImageSheet.Width; x += 32)
  {
  e.Graphics.DrawLine(miLapiz, x, 0, x, this.pbImageSheet.Height);
  }
 }
}

Lo próximo que se va a hacer es dibujar un cuadro de selección en el cuadro que se haya escogido con el Mouse, se crea la variable que sirve para la selección:

private System.Drawing.Rectangle seleccion = new System.Drawing.Rectangle(0, 0, 0, 0);

Y se añade al evento Paint lo siguiente:

//Dibujar los rectangulos de Selección
miLapiz = new Pen(System.Drawing.Color.Red, 2);
e.Graphics.DrawRectangle(miLapiz, seleccion.X * 32 + 1,
seleccion.Y * 32 + 1, seleccion.Width * 32 - 1.5f,
seleccion.Height * 32 - 1.5f);

Para finalizar se crea el evento MouseUp del mismo PictureBox:

private void pbImageSheet_MouseUp(object sender, MouseEventArgs e)
{
int x1 = (int)Math.Floor((double)e.X / 32);
int y1 = (int)Math.Floor((double)e.Y / 32);
seleccion = new System.Drawing.Rectangle(x1, y1, 1, 1);
this.pbImageSheet.Invalidate();
}

Cuando se suelta el Mouse se inicializa el rectángulo de selección, y al llamar el método Invalidate se vuelva a llamar el método Paint haciendo que se dibuje un rectángulo rojo en el cuadro seleccionado:

Ahora se modifica la clase TileSheet, para que cuando vayamos a guardar el mapa, se pueda acceder a sus propiedades y poder guardar los máximos detalles posibles.

Lo primero es crear dos nuevas propiedades y una :

public Int32 ID { get; set; }
public String TexturaNombre { get; set; }
private ContentManager Content;

Se crea un nuevo constructor sin parámetros, esto va a ser útil para la serialización que se vaya a usar, también se modifica el constructor:

public TileSheet()
{
}
public TileSheet(ContentManager content, String nombreTextura, Int32 id)
{
listaRectangulos = new Dictionary();
ID = id;
Content = content;
TexturaNombre = nombreTextura;
if (nombreTextura.IndexOf(".") > 0) // tiene extensión (?)
{
 nombreTextura = nombreTextura.Substring(0, nombreTextura.Length - 4); // quitar extensión
}
CargarTextura(nombreTextura);
}

Al nuevo constructor se le envía el contentManager del juego, el nombre de la textura que se va a enviar con extensión, pero para el cargue se le quita la extensión y se carga la imagen, también se crea el método cargarTextura:

private void CargarTextura(String nombreTextura)
{
textura = Content.Load(nombreTextura);
}

También se modifica el método obtenerRectangulo para evitar errores cuando se seleccionen Tiles mayores o menores a los que existen:

public void obtenerRectangulo(ref Tile i, out Rectangle rect)
{
rect = new Rectangle();
if (i.Tipo >= 0 && i.Tipo <= listaRectangulos.Count)
 {
 rect = listaRectangulos[i.Tipo];
 }
}

Lo próximo que se hace es modificar el método crearMapa de la clase Game1:

public void crearMapa(Int32 ancho, Int32 alto, Int32 tamanoTile)
{
motor = new TileEngine(this, ancho, alto, tamanoTile);
motor.mapa.tamanoPantalla = new Vector2(pictureBox.Width, pictureBox.Height);
Camara2D.TamanoPantalla = motor.mapa.tamanoPantalla;
Camara2D.altoTile = tamanoTile;
Camara2D.anchoTile = tamanoTile;
Camara2D.numXTiles = ancho;
Camara2D.numYTiles = alto;
mapaCreado = true;
}

Y se crea el método crearTileSheet:

public void crearTileSheet(String image, Int32 altoImage, Int32 anchoImage)
{
 Int32 idSheet = motor.tileSheets.Count;
 motor.tileSheets.Add(new TileSheet(Content, image, idSheet));
 int tilecount = 0;
 for (int y = 0; y < altoImage / motor.TamTiles; y++)
  {
  for (int x = 0; x  0) && (ms.Y > 0) &&
  (ms.X < Camara2D.rectMundo.Width) && (ms.Y  0)
   {
   motor.mapa.tileMapLayers[capa].adicionarTile((int)mouseLoc.X / motor.TamTiles,
   (int)mouseLoc.Y / motor.TamTiles, new Tile(tipoTile));
   }
  }
 }
}

Si se ejecuta la aplicación, y se da clic en el menú Nuevo Mapa, luego se carga una imagen, y se selecciona un rectángulo, no se va a ver nada aún, debido a que no se ha creado ninguna Capa del mapa.

Para crear las capas, se usa la lista lstCapas de la clase MapEditor, en el botón btnAgregarCapa se adiciona lo siguiente para crear una nueva capa y adicionarla a la lista:

private void btnAgregarCapa_Click(object sender, EventArgs e)
{
 if (game.motor != null)
  {
  if (game.motor.tileSheets.Count > 0)
   {
   game.motor.mapa.agregarCapa(new TileMapLayer(100, 100,
   game.motor.tileSheets[0], true));
   lstCapas.Items.Add("Capa " + lstCapas.Items.Count, true);
   lstCapas.SelectedIndex = lstCapas.Items.Count - 1;
   }
  }
 }

Ahora si se puede ejecutar la aplicación, crear el nuevo mapa, cargar la imagen, crear una capa, seleccionar un Tile y empezar a dibujar:

Si se agrega una nueva capa y se dibuja, no se verá el efecto que se desea, ya que siempre se va a dibujar en la capa 0, para solucionarlo se hace lo mismo que con el tipo del Tile Seleccionado, se crea una variable publica en la clase game1:

public Int32 capaSeleccionada;

Y se modifica el Update:

if (tipoTile != -1)
{
int capa = capaSeleccionada;
 if (motor.Mapa.tileMapLayers.Count > 0)
 {
 motor.Mapa.tileMapLayers[capa].adicionarTile((int)mouseLoc.X / motor.TamTiles,
 (int)mouseLoc.Y / motor.TamTiles, new Tile(tipoTile));
 }
}

Ahora en la clase MapEditor, en el evento SelectedIndexChanged de la lista lstCapas inicializamos la variable de la capa seleccionada y además se modifica la propiedad Activo de la capa, entonces si el ítem de la capa se encuentra chequeda, la propiedad Activo queda en true, pero si no se encuentra chequeada, la propiedad Activo queda en false y no se va a dibujar la capa:

private void lstCapas_SelectedIndexChanged(object sender, EventArgs e)
{
game.motor.Mapa.tileMapLayers[lstCapas.SelectedIndex].Activo = lstCapas.GetItemChecked(lstCapas.SelectedIndex);
game.capaSeleccionada = lstCapas.SelectedIndex;
}

Ahora ya se pueden crear varias capas y dibujar diferentes Tiles en las capas.

Guardar Mapa y Cargar Mapa:

Para guardar el Mapa, se va a armar un xml con las propiedades necesarias del Motor de Tiles, para armar el xml algunas partes van a ser manuales y otra parte va a ser gracias a la serialización XML, el formato que va a tener el mapa guardado va a quedar así:

Antes de continuar con el guardado del Mapa, se modifican algunas clases:

A la clase Tile se le adiciona un constructor sin parámetros:

public Tile()
{
}

En la clase TileMapLayer, se le adicionan algunos parámetros a la variable mapa y se crea un constructor sin parametros:

[XmlArray(ElementName = "MAPA")]
[XmlArrayItem(ElementName = "TILES")]
public Tile[][] mapa;
public TileMapLayer()
{
}

Esto sera útil en la serialización para asignar nuestros propios Titulos.

En la clase TileSheet, a la variable listaRectangulos le añadimos una propiedad que hará que cuando se serialice la clase, se ignore la lista de rectángulos:

[XmlIgnore]
public Dictionary listaRectangulos;

Ahora en la clase TileEngine, se crea un método para guardar el mapa y armar el xml:

public String guardarMapa()
{
StringBuilder xml = new StringBuilder();
xml.Append("<MAPA>");
xml.Append("<ANCHO>");
xml.Append(mapa.NumXTiles.ToString());
xml.Append("</ANCHO>");
xml.Append("<ALTO>");
xml.Append(mapa.NumYTiles.ToString());
xml.Append("</ALTO>");
xml.Append("<TAMANOTILE>");
xml.Append(mapa.AnchoCelda.ToString());
xml.Append("</TAMANOTILE>");
 foreach TileSheet sheet in tileSheets)
{
xml.Append("<TILESHEET>");
xml.Append("<TEXTURANAME>");
xml.Append(sheet.TexturaNombre);
xml.Append("</TEXTURANAME>");
 foreach (Rectangle rect in sheet.listaRectangulos.Values)
 {
 xml.Append("<RECTANGLE>");
 xml.Append("<X>");
 xml.Append(rect.X.ToString());
xml.Append("</X>");
xml.Append("<Y>");
xml.Append(rect.Y.ToString());
xml.Append("</Y>");
xml.Append("<WIDTH>");
xml.Append(rect.Width.ToString());
xml.Append("</WIDTH>");
xml.Append("<HEIGHT>");
xml.Append(rect.Height.ToString());
xml.Append("</HEIGHT>");
xml.Append("</RECTANGLE>");
}
xml.Append("</TILESHEET>");
}
foreach (TileMapLayer layer in mapa.tileMapLayers)
{
String xmlserializado = null;
XmlSerializer serializer = new XmlSerializer(typeof(TileMapLayer));
MemoryStream stream = new MemoryStream();
XmlTextWriter xmltext = new XmlTextWriter(stream, Encoding.UTF8);
XmlSerializerNamespaces xsn = new XmlSerializerNamespaces();
xsn.Add("", "");
serializer.Serialize(xmltext, layer, xsn);
stream = (MemoryStream)xmltext.BaseStream;
xmlserializado = UTF8ByteArrayToString(stream.ToArray());
xmlserializado = xmlserializado.Remove(xmlserializado.IndexOf("<?xml"),xmlserializado.IndexOf("?>") + 1);
xml.Append(xmlserializado);
}
xml.Append("<CAMARA>");
xml.Append("<TAMANOPANTALLAX>");
xml.Append(Camara2D.TamanoPantalla.X.ToString());
xml.Append("</TAMANOPANTALLAX>");
xml.Append("<TAMANOPANTALLAY>");
xml.Append(Camara2D.TamanoPantalla.Y.ToString());
xml.Append("</TAMANOPANTALLAY>");
xml.Append("</CAMARA>");
xml.Append("</MAPA>");
return xml.ToString();
}

Para la serialización se usa el siguiente método:

private String UTF8ByteArrayToString(Byte[] characters)
{
UTF8Encoding encoding = new UTF8Encoding();
String constructedString = encoding.GetString(characters);
return (constructedString);
}

Como ven, la clase StringBuilder es más eficaz que la clase String, cuando se usan cadenas muy largas y se están adicionando más campos, la única parte que se ha serializado, es la que define la matriz de Tiles, después de serializarlo, reemplazamos el namespace que se crea en el xml.

Para usarlo, en la clase MapEditor, en la opción GuardarMapa se llama el método guardarMapa y se hace uso del SaveFileDialog para guardar el xml retornado en un archivo:

private void guardarMapaToolStripMenuItem_Click(object sender, EventArgs e)
{
if (game.motor != null)
{
saveFileDialog1.Filter = "Xml|*.xml";
saveFileDialog1.Title = "Guardar un Mapa";
saveFileDialog1.ShowDialog();
// If the file name is not an empty string open it for saving.
if (saveFileDialog1.FileName != "")
{
String xml = game.motor.guardarMapa();
File.WriteAllText(saveFileDialog1.FileName, xml);
}
}
else
{
MessageBox.Show("Aún no se ha creado un mapa");
}
}

Para probarlo, se debe crear un nuevo Mapa, cargar una imagen, crear las capas necesarias y dibujarlo, luego escoger la opción Guardar Mapa, cuando hayamos seleccionado un nombre y un directorio, se puede ir al directorio y verificar el archivo que se ha creado.

Ahora para cargar el Mapa, se debe obtener el archivo XML, y empezar a leerlo parte por parte, e inicializar el motor.

Se crea el método cargarMapa, que recibe como parámetros una instancia de la clase Game y el xml:

public void cargarMapa(Game game, String xml)
{
XmlDocument mapDoc = new XmlDocument();
mapDoc.LoadXml(xml);
Int32 anchoTile = 0;
Int32 altoTile = 0;
Int32 tamanoTile = 0;
anchoTile = Convert.ToInt32(mapDoc.SelectSingleNode("MAPA/ANCHO").InnerText);
altoTile = Convert.ToInt32(mapDoc.SelectSingleNode("MAPA/ALTO").InnerText);
tamanoTile = Convert.ToInt32(mapDoc.SelectSingleNode("MAPA/TAMANOTILE").InnerText);
TamTiles = tamanoTile;
Mapa = new TileMap(game, tamanoTile, tamanoTile, anchoTile, altoTile);
Int32 tamanoPantallaX = 0;
Int32 tamanoPantallaY = 0;
tamanoPantallaX = Convert.ToInt32(mapDoc.SelectSingleNode("MAPA/CAMARA/TAMANOPANTALLAX").InnerText);
tamanoPantallaY = Convert.ToInt32(mapDoc.SelectSingleNode("MAPA/CAMARA/TAMANOPANTALLAY").InnerText);
Mapa.tamanoPantalla = new Vector2(tamanoPantallaX, tamanoPantallaY);
Camara2D.TamanoPantalla = Mapa.tamanoPantalla;
Camara2D.altoTile = tamanoTile;
Camara2D.anchoTile = tamanoTile;
Camara2D.numXTiles = anchoTile;
Camara2D.numYTiles = altoTile;
String nombreTextura = "";
Rectangle rect = new Rectangle();
XmlNodeList NodetileSheets = mapDoc.GetElementsByTagName("TILESHEET");
//if (NodetileSheets != null)+
{
foreach (XmlElement nodoTileSheet in NodetileSheets)
{
nombreTextura = nodoTileSheet.SelectSingleNode("TEXTURANAME").InnerText;
tileSheets.Add(new TileSheet(Content, nombreTextura, tileSheets.Count));
Int32 cont = 1;
XmlNodeList NodeRects = nodoTileSheet.GetElementsByTagName("RECTANGLE");
foreach (XmlElement nodoRect in NodeRects)
{
rect.X = Convert.ToInt32(nodoRect.SelectSingleNode("X").InnerText);
rect.Y = Convert.ToInt32(nodoRect.SelectSingleNode("Y").InnerText);
ect.Width = Convert.ToInt32(nodoRect.SelectSingleNode("WIDTH").InnerText);
rect.Height = Convert.ToInt32(nodoRect.SelectSingleNode("HEIGHT").InnerText);
tileSheets[tileSheets.Count - 1].adicionarRectangulo(cont, rect);
cont++;
}
}
}
XmlNodeList NodetileLayers = mapDoc.GetElementsByTagName("TileMapLayer");
Int32 tipo = 0;
Int32 contLayer = 0;
Int32 contTiles = 0;
Int32 contTile = 0;
Int32 tileSheet = 0;
foreach (XmlElement nodoTileMapLayer in NodetileLayers)
{
XmlNodeList NodeSheets = nodoTileMapLayer.GetElementsByTagName("sheet");
foreach (XmlElement NodeSheet in NodeSheets)
{
tileSheet = Convert.ToInt32(NodeSheet.SelectSingleNode("ID").InnerText);
}
Mapa.agregarCapa(new TileMapLayer(anchoTile, altoTile, tileSheets[tileSheet], true));
//MAPA
XmlNodeList NodetileMaps = nodoTileMapLayer.GetElementsByTagName("MAPA");
contTiles = 0;
contTile = 0;
foreach (XmlElement NodetileMap in NodetileMaps)
{
mlNodeList Nodetiles = NodetileMap.GetElementsByTagName("TILES");
foreach (XmlElement Nodetile in Nodetiles)
{
contTile = 0;
XmlNodeList tiles = Nodetile.GetElementsByTagName("Tile");
foreach (XmlElement tile in tiles)
{
tipo = Convert.ToInt32(tile.SelectSingleNode("Tipo").InnerText);
Mapa.tileMapLayers[contLayer].adicionarTile(contTiles, contTile, new Tile(tipo));
contTile++;
}
contTiles++;
}
}
contLayer++;
}
componentesHijos.Add(Mapa);
LoadContent();
}

Como ven, en el método leemos las partes del XML, con la clase XmlNodeList se pueden obtener varias partes en una lista y luego recorrerla para obtener los elementos, después de asignar todos los campos, adicionamos el Mapa y cargamos los contenidos, aún le hacen falta las validaciones al método para evitar errores.

También se crea un nuevo constructor de la clase:

public TileEngine(Game game, String xml)
: base(game)
{
spriteBatch = (SpriteBatch)Game.Services.GetService(typeof(SpriteBatch));
graficos = (GraphicsDeviceManager)Game.Services.GetService(typeof(GraphicsDeviceManager));
Content = (ContentManager)Game.Services.GetService(typeof(ContentManager));
juego = game;
cargarMapa(juego, xml);
}

Desde el menú Cargar Mapa, se abre un cuadro de dialogo para obtener un archivo Xml y llamar el método cargarMapa

private void cargarMapaToolStripMenuItem_Click(object sender, EventArgs e)
{
openFileDialog1.Title = "Abrir Mapa XML";
openFileDialog1.Filter = "XML Files|*.xml";
openFileDialog1.InitialDirectory = @"C:\";
openFileDialog1.CheckFileExists = true;
openFileDialog1.CheckPathExists = true;
String xml = "";
if (openFileDialog1.ShowDialog() == DialogResult.OK)
{
xml = File.ReadAllText(openFileDialog1.FileName);
game.CargarMapa(xml);
for (int i = 0; i < game.motor.mapa.tileMapLayers.Count; i++)
{
lstCapas.Items.Add("Capa " + (i + 1), true);
lstCapas.SelectedIndex = lstCapas.Items.Count - 1;
String directorio = Application.StartupPath + @"\Content\" + game.motor.tileSheets[0].TexturaNombre;
Bitmap tileSheet = new Bitmap(directorio);
pbImageSheet.Image = tileSheet;
pbImageSheet.Size = tileSheet.Size;
}
}

Ahora, se puede hacer clic en la opción CargarMapa y buscar el xml guardado anteriormente, y se debe mostrar el TileSheet Seleccionado, se deben crear las capas en la lista y se debe dibujar el mapa, cabe anotar que las imágenes que se usen deben estar en el Content del Proyecto.

Para Finalizar:

Como el tutorial va muy largo, las mejoras que se le deben hacer son:

– Crear un formulario para inicializar el tamaño del Mapa, y el tamaño del Tile, para generalizarlos en el cargue de las imágenes y las capas

– Para limpiar una capa, llamar el método limpiar de la clase TileMapLayer

– Crear las validaciones posibles para evitar errores.

Con las mejoras puede que quede así el Editor:

El Código Fuente Aquí.

Anuncios

6 pensamientos en “Cómo hacer un motor de Tiles – XNA – Parte 3 – Editor

    • Hola, al final del post, justo debajo del video se encuentra en link sobre la palabra: “Código Fuente Aquí”, el problema es que esta en SkyDrive, y ahora el link redirecciona a toda la libreria, busca el archivo “TileEngine3parte” y a la derecha das en descargar

Deseas comentar o sugerir algo?

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s