Cómo hacer un motor de Tiles – Parte 4 – Colisión


Al motor de Tiles ya le añadí un editor que permite cargar y guardar las creaciones para usarlas en los juegos, pero aún no he definido las colisiones, esto es muy fácil y lo mostraré en el siguiente Post

 

Lo primero es modificar la clase Tile, añadiendo una nueva propiedad que indicará si el Tile es sólido o no y hay colisión con el tile:

public class Tile
{
 public Int32 Tipo { get; set; }
 public Boolean Colision { get; set; }

public Tile()
{
}

 public Tile(Int32 tipo)
 {
  Tipo = tipo;
  Colision = false;
 }
}

Otra cosa que se puede hacer, es modificar el Editor de Tiles para que muestre aquellos mapas como “solidos” de un color diferente.

Se modifica la clase TileMap, para adicionar una variable pública que sea Boolean:

public Boolean debug = false;

Y en el for donde se dibuja cada Tile, se modifica y añade lo siguiente:

layer.sheet.obtenerRectangulo(ref tile, out sourceRect);
Color color = Color.White;
if (debug)
{
 if (tile.Colision)
 {
 color = Color.Red;
 }
}
spriteBatch.Draw(layer.sheet.Textura, Vector2.Zero, sourceRect, color,0, position, scale, SpriteEffects.None, 0.0f);

Ahora se modifica el Editor para que cuando se cree el mapa, se deje la variable debug como verdadero, en la clase game1 del Editor, en el método crearMapa:

motor.Mapa.debug = true;

Y al editor se le adiciona un CheckBox (Name: ckbTileColisionable) que indica que el Tile que se va a adicionar tiene colisión:

También hay que adicionar en la clase game1 del proyecto TileEditor una variable Booleana que será la tendrá el valor del checkbox.

En las declaraciones:

public Boolean tileColisionable = false;

Y en el Update, al adicionar el Tile:

Tile tile = new Tile(tipoTile);
tile.Colision = tileColisionable;
motor.Mapa.tileMapLayers[capa].adicionarTile((int)mouseLoc.X / motor.TamTiles, (int)mouseLoc.Y / motor.TamTiles, tile);

En el MapEditor, en el evento CheckedChanged del checkbox se pasa el valor que indica la colisión:

private void checkBox1_CheckedChanged(object sender, EventArgs e)
{
  game.tileColisionable = ckbTileColisionable.Checked;
}

Ahora ya se puede cargar un set de imágenes, y al seleccionar la opción “Es Colisionable?”, el Tile tendrá una coloración roja, en la siguiente imagen verán que los ladrillos se ven más rojos, debido a que son sólidos y van a tener colisión:

No es necesario modificar el método de guardarMapa para que tome la propiedad de Colisión, ya que esto se hace en la serialización XML de la clase TileMapLayer, y como es una propiedad pública, se puede serializar.

Lo que si hay que modificar es el método cargarMapa de la clase TileEngine para indicar que los tiles tienen colisión:

XmlNodeList tiles = Nodetile.GetElementsByTagName("Tile");
foreach (XmlElement tile in tiles)
{
 Tile t = new Tile();
 Boolean colision = Convert.ToBoolean(tile.SelectSingleNode("Colision").InnerText);
 tipo = Convert.ToInt32(tile.SelectSingleNode("Tipo").InnerText);
 t.Tipo = tipo;
 t.Colision = colision;
 Mapa.tileMapLayers[contLayer].adicionarTile(contTiles, contTile, t);
 contTile++;
}

Con esto ya se asegura que cuando se cargue el mapa, si tiene Tiles con colisión, se pueda hacer algo con ellos.

Probando las colisiones con un personaje

En el primer post, se había creado la clase SpriteComponent y la clase Jugador, y permitían cargar un Sprite que sería el jugador principal.

Ahora se va a modificar para permitir cargar un set de imágenes que harán que el jugador tenga una animación, primero adicionamos nuevas propiedades que serán las encargadas de tener la información de los frames y el tiempo en que se dibujarán:

// propiedades para movimientos con frames
private List<Rectangle> frames = new List<Rectangle>();
private Int32 frameActual;
private float tiempoframe = 0.1f;
private float tiempoParaActualFrame = 0.0f;
private float rotacion = 0.0f;
public Boolean Activo = false;
public Boolean Animado = false;
public Boolean Colision = false;

public Int32 AnchoFrame
{
 get
 {
  return frames[0].Width;
 }
}

public Int32 AltoFrame
{
 get
  {
  return frames[0].Height;
  }
}

public float Rotacion
{
 get
  {
  return rotacion;
  }
 set
  {
  rotacion = value;
  }
}

public Int32 Frame
{
 get
  {
  return frameActual;
  }
 set
  {
  frameActual = (int)MathHelper.Clamp(value, 0, frames.Count - 1);
  }
}

public float TiempoFrame
{
 get
  {
  return tiempoframe;
  }
 set
 {
  tiempoframe = MathHelper.Max(0, value);
 }
}

public Rectangle Fuente
{
 get
 {
  return frames[frameActual];
 }
}

La propiedad Fuente, tiene las medidas de la imagen actual, la variable frameActual guarda el número de la imagen, AnchoFrame  y AltoFrame guardan el tamaño de la imagen.

La propiedad Frame va a ser útil cuando se quiera ir directamente a una imagen, mientras que TiempoFrame servirá para poder modificar el tiempo en que se va a animar el personaje. La lista frames, contiene los tamaños de todos los rectángulos que componen la animación.

También se modifica el método Update, que será el que verificará y cambiará el frame del jugador, cada vez que se cumple el tiempo y se está moviendo el jugador (Velocidad ¡= Vector2.Zero), se cambia el frameActual que sirve para saber que Rectángulo o imagen se dibujará, la animación es cíclica:

public override void Update(GameTime gameTime)
{
 if (Activo)
 {
  float transcurrido = (float)gameTime.ElapsedGameTime.TotalSeconds;
  tiempoParaActualFrame += transcurrido;
  if (Animado)
  {
   if (tiempoParaActualFrame >= TiempoFrame)
   {
    if (Velocidad != Vector2.Zero)
    {
     frameActual = (frameActual + 1) % (frames.Count);
     tiempoParaActualFrame = 0.0f;
    }
   }
  }
}

base.Update(gameTime);
}

El método Draw se modifica para que dibuje el rectángulo del frame actual y tenga en cuenta la propiedad Rotación:

public override void Draw(GameTime gameTime)
{
 if (Activo)
 {
  //spriteBatch.Draw(Textura, Posicion, new Rectangle(0, 0, (int)Tamano.X, (int)Tamano.Y), ColorImagen);
  spriteBatch.Draw(Textura, Posicion, Fuente, ColorImagen, Rotacion, Centro, 1.0f, SpriteEffects.None, 0.0f);
 }
base.Draw(gameTime);
}

El siguiente método es útil para adicionar un frame, pero lo que se adiciona es un rectángulo a la lista frames,:

public void adicionarFrame(Rectangle rectanguloFrame)
{
  frames.Add(rectanguloFrame);
}

Para calcular el ángulo en que se encuentra la imagen, se usa la función Atan2 y como parámetro se le envía un vector:

public void rotarA(Vector2 direccion)
{
  Rotacion = (float)Math.Atan2(direccion.Y, direccion.X);
}

Por último se modifica la función Mover, para que se asigne la propiedad velocidad y se sepa que se encuentra moviendo y se pueda hacer la animación:

public void Mover(Vector2 velocidad)
{
  this.velocidad = velocidad;
  Posicion += velocidad;
  bound.Max = new Vector3((Posicion.X + Tamano.X), (Posicion.Y + Tamano.Y), 0);
  bound.Min = new Vector3(Posicion.X, Posicion.Y, 0);
}

Ahora se modifica la clase Jugador, cambiando el constructor para recibir la cantidad de frames que va a tener el personaje, también se inicializan algunas propiedades y se cargan los frames dependiendo de la cantidad de frames que se hayan indicado:

public Jugador(Game game, Vector2 tamano, Vector2 posicion, String nombreImagen, Int16 cantFrames)
       : base(game, tamano, posicion)
{
  Activo = true;
  Animado = true;
  NombreImagen = nombreImagen;
  ColorImagen = Color.White;
  for (int x = 1; x < cantFrames; x++)
  {
   adicionarFrame(new Rectangle((int)tamano.X * x, (int)tamano.Y, 32, 32));
  }
  LoadContent();
}

Para probar todo, se crea un nuevo proyecto y se carga el mapa que se creó anteriormente, en la clase game1:

//Declaraciones
TileEngine mapa;

protected override void LoadContent()
{
 spriteBatch = new SpriteBatch(GraphicsDevice);
 Services.AddService(typeof(SpriteBatch), spriteBatch);
 Services.AddService(typeof(ContentManager), Content);
 Services.AddService(typeof(GraphicsDeviceManager), graphics);
 String xml;
 xml = File.ReadAllText("C:/Mapa/ColisionMapa20.xml");
 mapa = new TileEngine(this, xml);
}

Se actualiza y dibuja el mapa:

mapa.Update(gameTime);

spriteBatch.Begin();
mapa.Draw(gameTime);
spriteBatch.End();

Al ejecutar se ve lo siguiente:

Lo siguiente por hacer es cargar el jugador y verificar las colisiones, el set de imágenes lo obtuve de la siguiente página: http://xnaresources.com/default.asp?page=Tutorial:SpriteEngine:1

Se crea el Jugador y se carga el set de imágenes:

Jugador tanque;

//LoadContent:
tanque = new Jugador(this, new Vector2(32, 64), new Vector2(46, 46), "MulticolorTanks", 8);

//Update:
tanque.Update(gameTime);

//Draw:
tanque.Draw(gameTime);

Al ejecutar se ve el tanque en uno de los Tiles:

Mover el tanque:

 

Crear el método para leer el teclado:

//Declaraciones
public KeyboardState estadoTeclado = new KeyboardState();
Vector2 distancia; //temporal para guardar la distancia

public float leerTeclado(KeyboardState teclado, Keys teclaAbajo,Keys teclaArriba)
{
float valor = 0;
if (teclado.IsKeyDown(teclaAbajo))
valor -= 1.0f;
if (teclado.IsKeyDown(teclaArriba))
valor += 1.0f;
return valor;
}

Obtener los datos del teclado y mover el mapa:

//Update:
estadoTeclado = Keyboard.GetState();
float dX = leerTeclado(estadoTeclado, Keys.Left, Keys.Right);
float dY = leerTeclado(estadoTeclado, Keys.Up, Keys.Down);
distancia = new Vector2(dX, dY);

tanque.Mover(distancia * 3);

Al probar se verá al tanque teniendo una animación cada vez que se mueve, pero no rota ni colisiona con ningún tile.

Para hacerlo colisionar, se debe obtener el tile de donde se encuentra posicionado el tanque y luego verificar si el Tile tiene colisión, si lo es entonces el tanque no se moverá, y para hacerlo rotar se usa el vector distancia que se obtiene con la lectura del teclado, y se le suma a otro vector que será el ángulo de movimiento, se normaliza y se rota:

//declaraciones
Vector2 anguloMovimiento = Vector2.Zero;
Vector2 posTile;
Vector2 distancia;

protected override void Update(GameTime gameTime)
{
// Allows the game to exit
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();
mapa.Update(gameTime);
estadoTeclado = Keyboard.GetState();
float dX = leerTeclado(estadoTeclado, Keys.Left, Keys.Right);
float dY = leerTeclado(estadoTeclado, Keys.Up, Keys.Down);

distancia = new Vector2(dX, dY);
anguloMovimiento += distancia;

//normalizar el ángulo
if (anguloMovimiento != Vector2.Zero)
{
  anguloMovimiento.Normalize();
}

// validación para evitar errores por desbordes
if (float.IsNaN(anguloMovimiento.X) || float.IsNaN(anguloMovimiento.X))
{
  anguloMovimiento = Vector2.Zero;
}

//rotar el tanque
tanque.rotarA(anguloMovimiento);

// obtener el tile a donde se dirige el tanque, hay que tener en cuenta que como
// el origen del tanque es en el centro de la imagen, se debe obtener es la posición
// del frente de la imagen:

posTile = mapa.Mapa.GetSquareAtPixel(new Vector2(tanque.Posicion.X + (distancia.X * tanque.Centro.X),
tanque.Posicion.Y + (distancia.Y * tanque.Centro.Y)) + distancia);
Tile tile = mapa.Mapa.tileMapLayers[0].obtenerTile((int)posTile.X, (int)posTile.Y);
// si el tile a donde se dirige no es sólido, se mueve el tanque
if (!tile.Colision)
{
 tanque.Mover(distancia * 3);
}

tanque.Update(gameTime);
base.Update(gameTime);
}

Ahora el tanque no se moverá, a menos que el tile a donde se dirige no sea sólido, si es sólido simplemente no se cambia de posición, y hacia dónde se dirige el tanque, se rota la imagen para dar una mejor sensación.

Código Fuente Aquí: Código.

Anuncios

2 pensamientos en “Cómo hacer un motor de Tiles – Parte 4 – Colisión

  1. saudos, soy nuevo en xna y me parece muy interezante todos los tutoriales, ahora me baje tu codigo para poder analizarlo y entenderlo mejor pero al probarlo me sale error, porque necesita un archivo llamado mapa20 o algo asi que se encuentra en la ubicacion del disco C, logicamente no tengo ese archivo si podrias hacer algo con respecto a eso , muchas gracias y felicidades por los tutos

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