Juego de la Culebra (Snake) – XNA


Inicio:

En estos días he creado un clon del Juego Snake o la culebrita en XNA , para empezar voy a mostrar el algoritmo que se usa para elaborar el juego:
- La culebra está compuesta por una cabeza y un cuerpo
- Cada vez que la culebra se come un ratoncito, se crece el cuerpo de la culebra
- Cada cuerpo de la culebra va a tener una lista de rutas, las rutas van a guardar el recorrido de la cabeza, para luego repetir el recorrido de la cabeza
- Sí la cabeza de la culebra choca con alguna parte de su cuerpo se acaba el juego
- Sí la cabeza de la culebra choca o alcanza los límites de la pared se acaba el juego

La primera clase que se va a implementar es la clase Pieza, la cual va a representar cada parte de la culebra, tanto la cabeza como el cuerpo. La clase tendrá una dirección, posición, un área de colisión, que en este caso será un Rectángulo y una lista de las rutas, además de poder cargar una imagen y dibujarlo:

public class Pieza
{
   protected SpriteBatch sBatch;
   protected GraphicsDeviceManager misgraficos;
   protected ContentManager micontenido;
   protected Texture2D imagen;
   public Vector2 Posicion;public Rectangle AreaColision
   {
     get
      {
         return new Rectangle((int)Posicion.X, (int)Posicion.Y, imagen.Width, imagen.Height);
       }
   }
   public Direccion DireccionActual { get; set; }
   public List rutas = new List();

   public Pieza(GraphicsDeviceManager graficos, Vector2 _posicion)
   {
     misgraficos = graficos;
     Posicion = _posicion;
     this.sBatch = new SpriteBatch(misgraficos.GraphicsDevice);
   }

   public void cargarImagen(ContentManager contenido, String nombreImagen)
   {
    micontenido = contenido;
    this.imagen = micontenido.Load(nombreImagen);
   }

   public void Dibujar()
  {
    sBatch.Begin();
    sBatch.Draw(imagen, Posicion, Color.White);
    sBatch.End();
  }
}

La segunda clase será la que guardará la ruta recorrida por la cabeza en cada parte del cuerpo, la clase guardará una posición y una dirección:

public class Ruta
{
  public Ruta(Vector2 _Posicion, Direccion _Direccion)
  {
   Posicion = _Posicion;
   DireccionActual = _Direccion;
 }
 public Vector2 Posicion { get; set; }
 public Direccion DireccionActual { get; set; }
}

Ahora creamos la enumeración Dirección:

public enum Direccion
{
 Arriba,
 Abajo,
 Izquierda,
 Derecha
}

Si compilamos en este momento no va a pasar nada, por lo tanto empezamos creando la cabeza de la culebra, en la clase Game1 hacemos lo siguiente:

 Pieza mCabeza;

En el Initialize:


 mCabeza = new Pieza(graphics, new Vector2(0, 0));

En el Load:

 mCabeza.cargarImagen(Content, "cabezaculebrita");

Adicionamos una imagen de 20X20 que será la cabeza de la culebra.
Y para finalizar en el Draw:

 mCabeza.Dibujar();

Sí compilamos en este momento, solo veremos un punto azul (o la imagen de la cabeza) en la parte superior izquierda:

Adicionamos un poco de Movimiento a la cabeza, declarando una variable que guarde la nueva dirección, la velocidad en la que se moverá la cabeza.
La culebra cada vez que se actualice el juego se va a mover dependiendo de la Dirección que tiene la pieza, para ello hacemos un Switch con la dirección, y sumamos o restamos a la posición:

 Pieza mCabeza;
 Int32 velocidad = 2;

Initialize:

 mCabeza.DireccionActual = Direccion.Abajo;

En el Update:

protected override void Update(GameTime gameTime)
{
 // Allows the game to exit
 if (Keyboard.GetState().IsKeyDown(Keys.Escape) == true)
   this.Exit();
KeyboardState aKeyboard = Keyboard.GetState();
Direccion aNuevaDireccion = mCabeza.DireccionActual;

if (aKeyboard.IsKeyDown(Keys.Right) == true)
 {
  aNuevaDireccion = Direccion.Derecha;
 }
else if (aKeyboard.IsKeyDown(Keys.Left) == true)
 {
  aNuevaDireccion = Direccion.Izquierda;
 }
else if (aKeyboard.IsKeyDown(Keys.Up) == true)
 {
  aNuevaDireccion = Direccion.Arriba;
 }
else if (aKeyboard.IsKeyDown(Keys.Down) == true)
 {
  aNuevaDireccion = Direccion.Abajo;
 }
mCabeza.DireccionActual = aNuevaDireccion;
switch (mCabeza.DireccionActual)
 {
  case Direccion.Abajo:
   {
    mCabeza.Posicion.Y += velocidad;
    break;
   }
  case Direccion.Arriba:
   {
    mCabeza.Posicion.Y -= velocidad;
    break;
   }
  case Direccion.Izquierda:
   {
    mCabeza.Posicion.X -= velocidad;
    break;
   }
  case Direccion.Derecha:
   {
    mCabeza.Posicion.X += velocidad;
    break;
   }
}

Al compilar y mover el teclado, la culebra se moverá dependiendo de la última dirección guardada.

Ahora vamos a declarar otra pieza para la comida de la culebra, está pieza tendrá una posición aleatoria:

Pieza mComida;
Random mRandom = new Random();

Initialize:

[sourcecode language='csharp']
mComida = new Pieza(graphics, new Vector2(mRandom.Next(100, 700), mRandom.Next(80, 510)));
[/sourcecode]
Load:

mComida.cargarImagen(Content, "comida");

Draw:

mComida.Dibujar();

No olvidar adicionar la imagen de la comida, que puede ser una imagen de 20X20 y que se llamará comida.
Cuando compilamos veremos la imagen de la comida en una posición aleatoria:


Comiendo:
Crearemos una función que verificará sí la cabeza se encuentra sobre la comida, para ello usaremos la propiedad AreaColision, y XNA nos facilita las cosas porque la clase Rectangle tiene una función llamada Intersects que verifica si ha chocado con otro Rectángulo:

// comerse una pieza
private void ComerPieza()
 {
  //verificar si la cabeza ha alcanzado alguna pieza para comersela
  if (mCabeza.AreaColision.Intersects(mComida.AreaColision))
   {
    mComida.Posicion = new Vector2(mRandom.Next(100, 700), mRandom.Next(80, 510));
   }
  }

Cada vez que hay una colisión, volvemos a ubicar la comida en una posición aleatoria, la función se debe llamar en el Update del Juego.

CRECIENDO …

El manejo de los cuerpos de la culebra, lo manejamos con un diccionario, que guardará el índice y la parte del cuerpo.
Cada vez que la culebra coma algo, se adiciona una pieza detrás de la cabeza, para hacer esto hacemos una función llamada AgregarPiezaCola, está función declarará una nueva variable Pieza y la adicionará al Diccionario, la posición del primer cuerpo será la posición de la cabeza y también guardará su dirección, y dependiendo de la dirección se corre la pieza:
Si la dirección de la cabeza va hacia arriba, se baja la pieza
Si la dirección de la cabeza va hacia abajo, se sube la pieza
Si la dirección de la cabeza va hacia la derecha, se mueve la pieza hacia la izquierda
Si la dirección de la cabeza va hacia la izquierda, se mueva la pieza hacia la derecha.
También podemos crear una distancia entre pieza y pieza.


private void AgregarPiezaAlaCola()
{
 Pieza aPieza = new Pieza(graphics, new Vector2(0, 0));
 aPieza.cargarImagen(Content, "cuerpoculebrita"); 
 mColas.Add(mColas.Count + 1,aPieza);

 Vector2 aPosicion;
 if (mColas.Count <= 1)
  {
   aPosicion = mCabeza.Posicion;
   mColas[mColas.Count].DireccionActual = mCabeza.DireccionActual;
  }
 else
  {
   aPosicion = mColas[mColas.Count - 1].Posicion;
   mColas[mColas.Count].DireccionActual = mColas[mColas.Count - 1].DireccionActual;
  }

 int aDistancia = aPieza.AreaColision.Width;

 switch (mColas[mColas.Count].DireccionActual)
  {
   case Direccion.Abajo:
    {
     mColas[mColas.Count].Posicion = new Vector2(aPosicion.X, aPosicion.Y - aDistancia);
     break;
    }
   case Direccion.Arriba:
    {
     mColas[mColas.Count].Posicion = new Vector2(aPosicion.X, aPosicion.Y + aDistancia);
     break;
    }
   case Direccion.Izquierda:
    {
     mColas[mColas.Count].Posicion = new Vector2(aPosicion.X + aDistancia, aPosicion.Y);
     break;
    }
   case Direccion.Derecha:
    {
     mColas[mColas.Count].Posicion = new Vector2(aPosicion.X - aDistancia, aPosicion.Y);
     break;
    }
  }
}

Ahora debemos mover cada pieza del cuerpo, nuevamente dependiendo de la dirección de cada pieza:

private void MoverCola()
{
  // recorrer cada pieza y moverlas en la dirección apropiada
  foreach (Pieza aPieza in mColas.Values)
   {
    aPieza.DireccionActual = mCabeza.DireccionActual;

    switch (aPieza.DireccionActual)
     {
      case Direccion.Abajo:
       {
        aPieza.Posicion += new Vector2(0, velocidad);
        break;
       }
     case Direccion.Arriba:
      {
       aPieza.Posicion -= new Vector2(0, velocidad);
       break;
      }
     case Direccion.Izquierda:
     {
      aPieza.Posicion -= new Vector2(velocidad, 0);
      break;
     }
    case Direccion.Derecha:
    {
     aPieza.Posicion += new Vector2(velocidad, 0);
     break;
    }
   }
 }
}

La función de adicionar una cola, se debe llamar cada vez que comamos una pieza:


//verificar si la cabeza ha alcanzado alguna pieza para comersela
if (mCabeza.AreaColision.Intersects(mComida.AreaColision))
 {
  AgregarPiezaAlaCola();
  mComida.Posicion = new Vector2(mRandom.Next(100, 700), mRandom.Next(80, 510));
 }

Y la función de Mover la cola se debe llamar en el Update del Juego, adicionalmente para dibujar se debe adicionar una imagen de 20X20 que se llame cuerpoculebrita, y en el Draw escribimos lo siguiente:

//Dibujar el cuerpo
foreach (Pieza aPieza in mColas.Values)
 {
  aPieza.Dibujar();
 }

Si ejecutamos y movemos la culebra hacia la comida, podemos obtener un efecto como el siguiente:

El efecto no es muy bueno, para eso se va a usar la clase ruta, y cada vez que la cabeza tome una nueva dirección, se va a adicionar la ruta a las piezas del cuerpo para que ellas hagan el mismo recorrido, voy a modificar la clase AdicionarPiezaCola y MoverCola y crear una nueva clase que va adicionar la ruta a la lista de cada pieza y también voy a separar el movimiento de la Cabeza en una nueva función, todo modificado queda así:

protected override void Update(GameTime gameTime)
{
 // Allows the game to exit
 if (Keyboard.GetState().IsKeyDown(Keys.Escape) == true)
  this.Exit();

KeyboardState aKeyboard = Keyboard.GetState();
Direccion aNuevaDireccion = mCabeza.DireccionActual;

if (aKeyboard.IsKeyDown(Keys.Right) == true)
 {
  aNuevaDireccion = Direccion.Derecha;
 }
else if (aKeyboard.IsKeyDown(Keys.Left) == true)
 {
  aNuevaDireccion = Direccion.Izquierda;
 }
  else if (aKeyboard.IsKeyDown(Keys.Up) == true)
 {
  aNuevaDireccion = Direccion.Arriba;
 }
else if (aKeyboard.IsKeyDown(Keys.Down) == true)
 {
  aNuevaDireccion = Direccion.Abajo;
 }
agregarRuta(aNuevaDireccion);

MoverCabeza();
ComerPieza();
MoverCola();

 // TODO: Add your update logic here

 base.Update(gameTime);
}

private void MoverCabeza()
{
 switch (mCabeza.DireccionActual)
  {
   case Direccion.Abajo:
    {
     mCabeza.Posicion.Y += velocidad;
     break;
    }
   case Direccion.Arriba:
    {
     mCabeza.Posicion.Y -= velocidad;
     break;
    }
   case Direccion.Izquierda:
    {
     mCabeza.Posicion.X -= velocidad;
     break;
    }
   case Direccion.Derecha:
    {
     mCabeza.Posicion.X += velocidad;
     break;
    }
  }
}
// comerse una pieza
private void ComerPieza()
 {
  //verificar si la cabeza ha alcanzado alguna pieza para comersela
  if (mCabeza.AreaColision.Intersects(mComida.AreaColision))
   {
    AgregarPiezaAlaCola();
    mComida.Posicion = new Vector2(mRandom.Next(100, 700), mRandom.Next(80, 510));
  }
}

private void AgregarPiezaAlaCola()
 {
  Pieza aPieza = new Pieza(graphics, new Vector2(0, 0));
  aPieza.cargarImagen(Content, "cuerpoculebrita");
  mColas.Add(mColas.Count + 1, aPieza);

  Vector2 aPosicion;
  if (mColas.Count <= 1) 
   { 
    aPosicion = mCabeza.Posicion; 
    mColas[mColas.Count].DireccionActual = mCabeza.DireccionActual; 
   } 
  else 
   { 
    aPosicion = mColas[mColas.Count - 1].Posicion; 
    mColas[mColas.Count].DireccionActual = mColas[mColas.Count - 1].DireccionActual; 
   } 
  int aDistancia = aPieza.AreaColision.Width; 
  switch (mColas[mColas.Count].DireccionActual) 
  { 
   case Direccion.Abajo: 
     { 
       mColas[mColas.Count].Posicion = new Vector2(aPosicion.X, aPosicion.Y - aDistancia); 
       break; 
      } 
   case Direccion.Arriba: 
     { 
       mColas[mColas.Count].Posicion = new Vector2(aPosicion.X, aPosicion.Y + aDistancia); 
       break; 
     } 
   case Direccion.Izquierda: 
     { 
        mColas[mColas.Count].Posicion = new Vector2(aPosicion.X + aDistancia, aPosicion.Y); 
        break; 
     } 
  case Direccion.Derecha: 
    { 
       mColas[mColas.Count].Posicion = new Vector2(aPosicion.X - aDistancia, aPosicion.Y); 
       break; 
     } 
  } 
if (mColas.Count > 1)
 {
  foreach (Ruta aruta in mColas[mColas.Count - 1].rutas)
   {
    mColas[mColas.Count].rutas.Add(new Ruta(aruta.Posicion, aruta.DireccionActual));
   }
 }
else
 {
  mColas[mColas.Count].rutas.Add(new Ruta(mCabeza.Posicion, mCabeza.DireccionActual));
 }
}

private void MoverCola()
 {
  // recorrer cada pieza y moverlas en la dirección apropiada
  foreach (Pieza aPieza in mColas.Values)
  {
   if (aPieza.rutas.Count == 0)
    {
     aPieza.DireccionActual = mCabeza.DireccionActual;
    }

   switch (aPieza.DireccionActual)
   {
    case Direccion.Abajo:
    {
     aPieza.Posicion += new Vector2(0, velocidad);

     if (aPieza.rutas.Count > 0)
     {
      if (aPieza.Posicion.Y >= aPieza.rutas[0].Posicion.Y)
      {
        aPieza.Posicion = new Vector2(aPieza.Posicion.X, aPieza.rutas[0].Posicion.Y);
        aPieza.DireccionActual = aPieza.rutas[0].DireccionActual;
       aPieza.rutas.RemoveAt(0);
      }
    }
   break;
  }
case Direccion.Arriba:
 {
  aPieza.Posicion -= new Vector2(0, velocidad);
   if (aPieza.rutas.Count > 0)
    {
     if (aPieza.Posicion.Y <= aPieza.rutas[0].Posicion.Y) 
      { 
       aPieza.Posicion = new Vector2(aPieza.Posicion.X, aPieza.rutas[0].Posicion.Y); 
       aPieza.DireccionActual = aPieza.rutas[0].DireccionActual; aPieza.rutas.RemoveAt(0); 
      } 
     } 
    break; 
  } 
 case Direccion.Izquierda: 
 { 
  aPieza.Posicion -= new Vector2(velocidad, 0); 
   if (aPieza.rutas.Count > 0)
    {
     if (aPieza.Posicion.X <= aPieza.rutas[0].Posicion.X) 
      { 
       aPieza.Posicion = new Vector2(aPieza.rutas[0].Posicion.X, aPieza.Posicion.Y); 
       aPieza.DireccionActual = aPieza.rutas[0].DireccionActual; 
       aPieza.rutas.RemoveAt(0); 
     } 
    } 
   break; 
  } 
case Direccion.Derecha: 
 { 
  aPieza.Posicion += new Vector2(velocidad, 0); 
   if (aPieza.rutas.Count > 0)
    {
     if (aPieza.Posicion.X >= aPieza.rutas[0].Posicion.X)
      {
      aPieza.Posicion = new Vector2(aPieza.rutas[0].Posicion.X, aPieza.Posicion.Y);
      aPieza.DireccionActual = aPieza.rutas[0].DireccionActual;
      aPieza.rutas.RemoveAt(0);
      }
     }
    break;
   }
  }
 }
}

private void agregarRuta(Direccion nuevaDireccion)
{
 // Verificar si la culebra se esta moviendo en una nueva dirección, si es así ponemos la dirección
 // y agrega un nueva ruta a cada pieza de la cola

 if (nuevaDireccion != mCabeza.DireccionActual)
  {
   mCabeza.DireccionActual = nuevaDireccion;

   // Agrega una nueva ruta a todas las piezas
   if (mColas.Count > 0)
    {
     for (int aIndex = 1; aIndex <= mColas.Count; aIndex++)
      {
       mColas[aIndex].rutas.Add(new Ruta(mCabeza.Posicion, mCabeza.DireccionActual));
      }
    }
   }
}

Al ejecutar se ve mucho mejor, ya que las piezas del cuerpo hacen el mismo recorrido, cada vez que se hace el recorrido se elimina de la lista la ruta recorrida.

Aunque el movimiento aún no convence, lo mejor es que la culebra se mueva a una velocidad igual a su tamaño, para que el recorrido de las piezas del cuerpo sea más vistoso.
Pero si aumentamos la velocidad a 20, la cabeza se moverá rápidamente, en este caso debemos controlar el tiempo en que se actualiza el juego, y por eso usamos una variable que guardará el tiempo transcurrido, si el tiempo es igual al que nosotros deseamos entonces se actualiza el juego, si no es igual se seguirá intentando actualizar el juego.
Para usar la técnica descrita arriba, modificamos la función MoverCabeza para obtener como parámetro la variable GameTime, para que el cuerpo también se actulice, entonces adicionamos a la función MoverCabeza la función MoverCola y aumentamos la velocidad a 20 que es el tamaño de la pieza:

Int32 velocidad = 20;
Pieza mComida;
Random mRandom = new Random();

Dictionary mColas = new Dictionary();

public const float VelocidadMovimiento = .1f;
private float reloj;

Update:

MoverCabeza(gameTime);
ComerPieza();
//MoverCola();

Función MoverCabeza

private void MoverCabeza(GameTime gameTime)
 {
  reloj += (float)gameTime.ElapsedGameTime.TotalSeconds;

  if (reloj < VelocidadMovimiento) 
   {
    return; 
   } 
 reloj = 0f; 
 switch (mCabeza.DireccionActual) 
  { 
   case Direccion.Abajo: 
    { 
     mCabeza.Posicion.Y += velocidad; 
     break;
    } 
   case Direccion.Arriba:
    {
     mCabeza.Posicion.Y -= velocidad; break;
    }
   case Direccion.Izquierda:
   {
    mCabeza.Posicion.X -= velocidad;
    break;
    }
   case Direccion.Derecha:
   {
    mCabeza.Posicion.X += velocidad;
    break;
   }
 } 
//Mover el cuerpo 
MoverCola(); 
} 

La velocidad limite será de 0.1. Ahora verificamos las colisiones, una para la colisión con las partes del cuerpo y otra para la colisión con las paredes:

private bool choquePared() 
{ 
 if (mCabeza.Posicion.X > Window.ClientBounds.Width)
  {
   return true;
  }
if (mCabeza.Posicion.X < 0) 
 { 
   return true;
  } 
if (mCabeza.Posicion.Y > Window.ClientBounds.Height)
 {
  return true;
 }
if (mCabeza.Posicion.Y < 0)
 {
  return true;
 }
return false;
}

// verificar que choque con una pieza de la misma culebrita
private bool choquePiezasCola()
{
 for (int aIndex = 2; aIndex <= mColas.Count; aIndex++)
  {
   if (mCabeza.AreaColision.Intersects(mColas[aIndex].AreaColision) == true)
    {
     return true;
    }
  }
 return false;
}

En el Update:

/bool aIsColision = false;

//Verificar si la culebra ha chocado con alguna parte de ella
if (choquePiezasCola() == true)
{
 aIsColision = true;
}

if (choquePared() == true)
{
 aIsColision = true;
}
if (aIsColision == true)
{
 Exit();
}

Cuando la cabeza de la culebra choca con alguna pieza del cuerpo se termina el juego, lo mismo sucede cuando la culebra sobrepasa los límites de la pantalla.


Para terminar con este mini juego, vamos a adicionar la puntuación, para ello adicionamos un SpriteFont y una variable que será la puntuación, cada vez que la culebra se coma algo, entonces se aumenta ciertos puntos a la puntuación:

SpriteFont puntuacion;
//Puntuación
int mPuntuacion = 0;

Load:

puntuacion = Content.Load("SpriteFont1");

Comer Pieza:

private void ComerPieza()
{
 //verificar si la cabeza ha alcanzado alguna pieza para comersela
 if (mCabeza.AreaColision.Intersects(mComida.AreaColision))
  {
   AgregarPiezaAlaCola();
   mComida.Posicion = new Vector2(mRandom.Next(0, 700), mRandom.Next(80, 510));
   mPuntuacion += 100 + ((mColas.Count - 1) * 50);
  }
}

Dibujar:

spriteBatch.Begin();
spriteBatch.DrawString(puntuacion, "Puntuacion: " + mPuntuacion.ToString(), new Vector2(38, 8), Color.White);
spriteBatch.End();

Me ayudo bastante el siguiente enlace.

Código Fuente aquí 

About these ads

2 pensamientos en “Juego de la Culebra (Snake) – XNA

  1. Pingback: Articulo Indexado en la Blogosfera de Sysmaya

  2. Pingback: Jugar Snake en YouTube mientras esperamos que cargue el video « Escarbando Código

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