Cómo hacer un Puzzle Bobble o Bust a Move en XNA – Parte 2


Esta es la segunda parte de Cómo hacer un clon de Puzzle Bobble, la primera parte la pueden encontrar aquí.

En la parte anterior se había explicado como rotar el disparador, disparar burbujas de diferentes colores, colisiones, buscar burbujas iguales y como ubicar las burbujas en el área de juego.

En esta parte se va a mostrar cómo hacer para que cuando haya un grupo de burbujas del mismo color a la burbuja lanzada, estas sean explotadas y verificar que no queden burbujas flotando, también para agregar la burbuja siguiente, mejorar la selección de colores, agregarle estados al juego (Ganar, perder, jugar), disparar la burbuja cuando el jugador no la dispara en cierto tiempo, hacer caer el techo o las burbujas, un administrador de niveles para cargar varios niveles,  una ayuda para saber donde apuntar y una que otra cosa para mejorar la jugabilidad.

Verificar Burbujas que van a explotar

Cuando se encuentren burbujas del mismo color, lo primero que se debe hacer es quitar el grupo de burbujas del grupo de burbujas pegadas, después se debe verificar en cada burbuja pegada para saber si la burbuja se encuentra flotando. Para saber si una burbuja se encuentra flotando, no solo basta con saber si tiene burbujas vecinas, se tiene que verificar que la burbuja conectada a ella, también tenga otra burbuja conectada y que una de esas burbujas conectadas, este pegada al techo.

En la imagen anterior, cuando la burbuja negra choque con las otras burbujas negras, se quitaran de las burbujas pegadas las dos burbujas negras, y las burbujas moradas van a quedar flotando, ya que no están conectadas a ninguna otra burbuja que esté conectada al techo.

Si en vez de tener una burbuja negra, se tiene una burbuja roja, y se envía al grupo de burbujas rojas que están arriba de las dos burbujas verdes que tienen el cuadro amarillo, al ser eliminadas del grupo de burbujas pegadas, verificamos que no hayan burbujas flotando, si tomamos las burbujas verdes, verificamos que ellas están pegadas a una morada, y esta a una azul y así sucesivamente hasta que hay una conectada al techo.

Para verificar esto, se crea un método en la clase burbuja, que es similar al método de encontrar burbujas iguales, pero el método retornará todas las burbujas conectadas a cierta burbuja, luego se recorre cada burbuja retornada por el método, y si una de ellas está pegada al techo, o su posición en la casilla es 0, significa que la burbuja está pegada al techo, sino hay ninguna, esta burbuja es guardada en el grupo que van a explotar.

Los métodos en la clase burbuja son:

public List<Burbuja> encontrarBurbujasConectadas()
{
List<Burbuja> lista = new List<Burbuja>();
lista.Add(this);
// encontrar burbujas conectadas recursivamente
encontrarBurbujasConectadas(lista, this);

return lista;
}

public void encontrarBurbujasConectadas(List<Burbuja> lista, Burbuja burbuja)
{
   burbuja.encontrarVecinos();
   foreach (Burbuja bubble in burbuja.Vecinos)
   {
    // si no se ha agregado anteriormente
    if (!bubble.destruir && !lista.Contains(bubble))
    {
      lista.Add(bubble);
      //si es raiz nos salimos para evitar buscar en todas las burbujas
      if (bubble.posicionCasilla.Y == 0)
       break;
     encontrarBurbujasConectadas(lista, bubble);
    }
  }
}

Y la verificación de las burbujas se hace en el método pegarBurbuja, antes de eso se debe crear una lista para guardar las burbujas que van a explotar:

public List<Burbuja> burbujasCayendo { get; private set; }
//Initialize
burbujasCayendo = new List<Burbuja>();
…
public void pegarBurbuja(Burbuja burbuja)
{
  List<Burbuja> grupoIguales = burbuja.encontrarIguales();
  if (grupoIguales.Count < 3)
  {
    burbuja.Posicion = posicionDeCasilla((Int32)burbuja.posicionCasilla.X,
     (Int32)burbuja.posicionCasilla.Y);
    burbujasPegadas.Add(burbuja);

  }
  else
  {
  // agregamos las burbujas del mismo color al grupo de las burbujas que van a explotar
  burbujasCayendo = grupoIguales;
  // eliminamos de la lista de burbujas pegadas a las burbujas que formaron el grupo
  // usando el método Except:
  burbujasPegadas = new List<Burbuja>(burbujasPegadas.Except(grupoIguales));
  //  se crean dos listas temporales para guardar las burbujas conectadas
  List<Burbuja> flotando = new List<Burbuja>();
  List<Burbuja> conectadas = new List<Burbuja>();

  // reiniciamos el flag que indica que la burbuja se va a caer de las burbujas pegadas
  foreach (Burbuja buble in burbujasPegadas)
  {
    buble.destruir = false;
  }

  // sirve para saber si la burbuja se encuentra conectada al techo
  Boolean raiz = false;
  foreach (Burbuja buble in burbujasPegadas)
  {
    raiz = false;
    // si la burbuja no es de las que estan conectadas ya al techo
    if (buble.posicionCasilla.Y > 0)
    {
      // buscar las burbujas conectadas
      conectadas = buble.encontrarBurbujasConectadas();
      foreach (Burbuja burb in conectadas)
      {
        //verificar que una de ellas esta pegada al techo
        if (burb.posicionCasilla.Y == 0)
        {
          raiz = true;
          break;
        }
      }

      // si no hay ninguna conectada al techo, se cae la burbuja
      if (!raiz)
      {
        buble.destruir = true;
        flotando.Add(buble);
      }
    }
  }

  // eliminamos de las burbujas pegadas, las que se van a caer por no
  // andar pegadas a ninguna otra
  burbujasPegadas = new List<Burbuja>(burbujasPegadas.Except(flotando));

  // agregamos las burbujas a la lista, toca una por una para no
  // borrar la lista de las que formaron el grupo del mismo color
  foreach (Burbuja burb in flotando)
  {
    burbujasCayendo.Add(burb);
  }
 }
}

Al ejecutar, lo único que se puede ver es que las burbujas van a desaparecer, lo próximo que se va a hacer es iniciar una explosión en el sitio donde estaban las burbujas.

Hacer explotar las Burbujas

Para hacer explotar las burbujas, se va a usar un sistema de partículas de un antiguo post, la textura que se va a usar es una burbuja de 32×32, y cuando haya un grupo de 3 o más burbujas iguales, cada una creara una explosión aleatoria, el código del sistema de partículas modificado es:

namespace BooblePuzzle
{
/// <summary>
/// Clase que respresenta las particulas
/// </summary>

public class Particula
{
public Texture2D Textura { get; set; }
public Vector2 Posicion { get; set; }
public Vector2 Velocidad { get; set; }
public float Angulo { get; set; }
public float VelocidadAngular { get; set; }
public Color Color { get; set; }
public float Tamano { get; set; }
public Int16 TiempodeVida { get; set; }

/// <summary>
/// Constructor de la clase
/// </summary>
/// <param name="textura"></param>
/// <param name="posicion"></param>
/// <param name="velocidad"></param>
/// <param name="angulo"></param>
/// <param name="velocidadAngular"></param>
/// <param name="color"></param>
/// <param name="tamano"></param>
/// <param name="tiempodeVida"></param>
public Particula(Texture2D textura, Vector2 posicion, Vector2 velocidad, float angulo, float velocidadAngular,
Color color, float tamano, Int16 tiempodeVida)
{
Textura = textura;
Posicion = posicion;
Velocidad = velocidad;
VelocidadAngular = velocidadAngular;
Angulo = angulo;
Color = color;
Tamano = tamano;
TiempodeVida = tiempodeVida;
}

/// <summary>
/// Disminuye el tiempo de Vida, y aumenta la Posición y la Velocidad Angular para la rotación
/// </summary>
public void Update()
{
TiempodeVida--;
Posicion += Velocidad;
Angulo += VelocidadAngular;
}

public void Draw(SpriteBatch spriteBatch)
{
Rectangle rectanguloFuente = new Rectangle(0, 0, Textura.Width, Textura.Height);
Vector2 origen = new Vector2(Textura.Width / 2, Textura.Height / 2);
spriteBatch.Draw(Textura, Posicion, rectanguloFuente, Color, Angulo, origen, Tamano, SpriteEffects.None, 0f);
}
}
}

El Motor de Partículas:

namespace BooblePuzzle
{
public class MotorParticulas
{
private Random random;
public Vector2 PosicionEmisor { get; set; }
public List<Particula> particulas;
private List<Texture2D> texturas;
public Boolean aleatorias = false;

/// <summary>
/// Constructor de la clase
/// </summary>
/// <param name="texturas"></param>
/// <param name="posicion"></param>
public MotorParticulas(List<Texture2D> texturas, Vector2 posicion)
{
PosicionEmisor = posicion;
this.texturas = texturas;
this.particulas = new List<Particula>();
random = new Random();
}

public MotorParticulas()
{
this.particulas = new List<Particula>();
this.texturas = new List<Texture2D>();
random = new Random();
}

/// <summary>
/// Genera partículas con propiedades aleatorias
/// </summary>
/// <returns></returns>
private Particula GenerarParticulasAleatorias(float maxTamano, Int32 maxTiempoVida, Color color)
{
Texture2D textura = texturas[random.Next(texturas.Count)];
Vector2 posicion = PosicionEmisor;
Vector2 velocidad = new Vector2(1f * (float)(random.NextDouble() * 2 - 1),
1f * (float)(random.NextDouble() * 2 - 1));
float angulo = 0;
float velocidadangular = 0.1f * (float)(random.NextDouble() * 2 - 1);
float tamano = (float)random.NextDouble() * maxTamano;
Int16 tiempodeVida = Convert.ToInt16(random.Next(maxTiempoVida));
return new Particula(textura, posicion, velocidad, angulo, velocidadangular, color, tamano, tiempodeVida);
}

public void adicionarParticula(Texture2D textura, Vector2 posicion, Vector2 velocidad, float angulo, float velocidadAngular,
Color color, float tamano, Int16 tiempodeVida)
{
particulas.Add(new Particula(textura, posicion, velocidad, angulo, velocidadAngular, color, tamano, tiempodeVida));
}

public void iniciarParticulas(Int32 cantidadTexturas, float maxTamano, Int32 maxTiempoVida, Color color)
{
for (int i = 0; i < cantidadTexturas; i++)
{
particulas.Add(GenerarParticulasAleatorias(maxTamano, maxTiempoVida, color));
}
}
public void adicionarTextura(Texture2D textura)
{
texturas.Add(textura);
}

/// <summary>
/// Añade y elimina partículas dependiendo del tiempo de Vida
/// </summary>
public void Update()
{
for (Int32 particula = 0; particula < particulas.Count; particula++)
{
particulas[particula].Update();
if (particulas[particula].TiempodeVida <= 0)
{
particulas.RemoveAt(particula);
particula--;
}
}
}

public void Draw(SpriteBatch spriteBatch)
{
for (Int32 indice = 0; indice < particulas.Count; indice++)
{
particulas[indice].Draw(spriteBatch);
}
}
}
}

Ahora para agregarlo al juego:

MotorParticulas particulas;
Texture2D textParticula;
// LoadContent
textParticula = Content.Load<Texture2D>("ParticulaBurbuja");
particulas = new MotorParticulas();
particulas.adicionarTextura(textParticula);
particulas.aleatorias = true;
// Update
…
// Se crea una lista temporal para poder eliminar las burbujas que se van a caer
List<Burbuja> burbujasdestruidas = new List<Burbuja>();
foreach (Burbuja burb in burbujasCayendo)
{
  // se inicia una explosión
  particulas.PosicionEmisor = burb.Posicion;
  // se reciben como parámetros la cantidad de partículas, el tamaño máximo, el tiempo de
  //vida y el color, que en este caso va a ser el de la burbuja
  particulas.iniciarParticulas(10, 1.5f, 40,burb.Color);
  burbujasdestruidas.Add(burb);
}

// se eliminan las burbujas
foreach (Burbuja burb in burbujasdestruidas)
{
  burbujasCayendo.Remove(burb);
}

if (burbujaLanzada == null)
{
  GenerarNuevaBurbuja();
}

particulas.Update();
base.Update(gameTime);

// Draw
particulas.Draw(spriteBatch);
spriteBatch.End();

Ahora al ejecutar y explotar las burbujas se ve así:

Mostrar la Siguiente Burbuja y mejorar la selección de colores

En el juego, para ayudar al jugador a ganar, se muestra una segunda burbuja que es la burbuja siguiente a la burbuja lanzada, para hacerlo se crea una segunda burbuja y cuando la burbuja que esta lista para lanzar, sea lanzada, se deja la próxima burbuja como la burbuja lista para lanzar, y se busca aleatoriamente el color para la próxima burbuja, el color que se va a escoger, se debe seleccionar igual a las burbujas que se encuentran pegadas.

Se agrega una nueva burbuja y una lista que va a tener todos los colores que se usan en el juego:

Burbuja proximaBurbuja;
public static  List<Color> Colores;

En el método Initialize, se inicializa la burbujaLanzada  y se adicionan los colores a la lista, el color de la burbujaLanzada va a ser entre los colores de la lista:

protected override void Initialize()
{
  rotacion = 0f;
  elapsed = 0f;

  posicionBurbuja = new Vector2(350 - 21, 400 - 21);
  //burbujaLanzada = new Burbuja(posicionBurbuja, Color.White);\
  burbujasPegadas = new List<Burbuja>();
  burbujasCayendo = new List<Burbuja>();

  Colores = new List<Color>();
  Colores.Add(Color.White);
  Colores.Add(Color.Red);
  Colores.Add(Color.Black);
  Colores.Add(Color.DarkBlue);
  Colores.Add(Color.Green);
  Colores.Add(Color.Yellow);
  Colores.Add(Color.Purple);
  Colores.Add(Color.Orange);

  burbujaLanzada = new Burbuja(posicionBurbuja, Colores[new Random().Next(7)]);
  GenerarNuevaBurbuja();

  base.Initialize();
}

Ahora se modifica el método generarNuevaBurbuja, para que inicialice la proximaBurbuja y cuando se llame el método, se asigne la burbujaLanzada igual a la proximaBurbuja, el color de la proximaBurbuja va a estar entre los colores de las burbujas pegadas:

public void GenerarNuevaBurbuja()
{
  if (proximaBurbuja != null)
  {
    burbujaLanzada = proximaBurbuja;
    burbujaLanzada.Posicion = posicionBurbuja;
  }
  Color colorBurbuja = new Color();

  if (burbujasPegadas.Count > 0)
  {
   // seleccionar solo los colores de las burbujas faltantes
   List<Color> coloresFaltan = new List<Color>((from c in burbujasPegadas select c.Color).Distinct());
   Int32 color = new Random().Next(coloresFaltan.Count);

   if (coloresFaltan.Count <= 0)
   {
     colorBurbuja = Colores[new Random().Next(7)];
   }
   else
   {
     colorBurbuja = coloresFaltan[color];
   }
 }
 else
 {
  colorBurbuja = Colores[new Random().Next(7)];
 }

 proximaBurbuja = new Burbuja(posicionBurbuja - new Vector2(tamanoBurbuja * 3, 0), colorBurbuja);
}

Ahora lo que falta es dibujar la burbuja:

//Draw
if (proximaBurbuja != null)
{
 spriteBatch.Draw(bubble, proximaBurbuja.Posicion, proximaBurbuja.Color);
}

Estados del Juego

El juego debe tener estados para identificar si se está jugando, o si se ha ganado el juego, o peor, si se ha perdido.

Para manejar los estados, se crea una enumeración que tendrá los estados: Jugar, Ganar y Perder, el estado Jugar será el estado inicial, y habrá un método que verificará dos cosas, la primera es que no hayan burbujas pegadas, en este caso se cambia al estado Ganado, y la segunda es verificar que no haya una burbuja cerca del límite de abajo, si es así, se pasa a estado Perder.

Los estados del juego:

public enum EstadoJuego
{
 Jugar,
 Perder,
 Ganar
}

Crear una variable que mantenga el estado:

EstadoJuego Estado = EstadoJuego.Jugar;

Crear método para verificar si se ha ganado o perdido, si no hay burbujas pegadas, entonces se cambia a estado Ganar y si una burbuja está en la posición de la casilla 7, entonces el estado es perder, 7 es el máximo de las casillas que he puesto, pero se puede cambiar si el tablero tiene más casillas:

private void verificarFinJuego()
{
 if (burbujasPegadas.Count == 0)
 {
  Estado = EstadoJuego.Ganar;
 }
 else
 {
  foreach (Burbuja b in burbujasPegadas)
  {
   if (b.posicionCasilla.Y > 7)
   {
    Estado = EstadoJuego.Perder;
    return;
   }
  }
 }
}

Ahora se modifica el método Update para separar las cosas que se hacen en cada estado, también para llamar el método de verificación.

if (Estado == EstadoJuego.Jugar)
{
 burbujaLanzada.Mover();
 if (burbujaLanzada.Posicion.X <= limiteIzquierdo || burbujaLanzada.Posicion.X >= limiteDerecho - 42) // choca
 {
  burbujaLanzada.Direccion.X *= -1;
 }
…
 if (Keyboard.GetState().IsKeyDown(Keys.Space))
 {
  if (burbujaLanzada != null && !burbujaLanzada.moviendo)
  {
   burbujaLanzada.moviendo = true;
   // disparar burbuja
   //dependiendo de la rotación
   Matrix m = Matrix.CreateRotationZ(rotacion);
   burbujaLanzada.Direccion.X += m.M12 * 7f;
   burbujaLanzada.Direccion.Y -= m.M11 * 7f;
  }
 }
 verificarFinJuego();
}

Si se el juego, no se podrá hacer nada, debido a que apenas se empieza el juego, se verifica que no hayan burbujas pegadas y como no hay, se cambia el estado a Ganar.

Mientras se hacen pruebas se puede comentar la línea del método verificarFinJuego, donde se cambia el estado a Ganar.

Cargar Niveles

Para cargar los niveles se va a usar un archivo de texto, donde por cada línea o renglón se van a escribir las burbujas que van a estar en cada nivel, el archivo de texto va a tener el número del color de la burbuja, y cada color va a ir separado por un espacio, el 0 representa que no hay burbujas, y para finalizar el nivel, se puede hacer escribir un símbolo como “-“, cuando se vaya a leer el archivo y se encuentre el “-“, significa que el nivel se ha acabado y se empieza a leer otro nivel.

El archivo, para evitar dejarlo en una ruta del disco, se va a incrustar en la Dll, y luego con Reflexión o Reflection se obtendrá el archivo y se lee.

Al leer el archivo se dejará un contador de cada fila y cada columna, para luego agregar a las burbujas, la posición de la casilla.

El nivel tendrá una lista de las burbujas,  y se crea una clase llamada AdministradorNiveles, esta clase es la encargada de leer el archivo y de tener una lista para guardar todos los niveles, y en el juego se crea un método llamado CargarNivel, el cual dependiendo de otra variable que indica el nivel actual del juego,  con esta variable se lee de la lista el nivel actual, luego se recorren todas las burbujas de la lista y se van pegando, sin verificar si hay del mismo color, solo pegarlas en la posición correcta.

Un ejemplo del archivo de texto es el siguiente:

Las filas pares, tienen siempre 8 colores, mientras que las filas impares tienen 7.

La clase Nivel es la siguiente:

public class Nivel
{
 public List<Burbuja> Burbujas { get; private set; }

 public Nivel(List<Burbuja> burbujas)
 {
  Burbujas = burbujas;
 }
}

La clase AdministradorNiveles:

public class AdministradorNiveles
{
 public List<Nivel> Niveles { get; private set; }
 public AdministradorNiveles(string nombreArchivo)
 {
 Niveles = new List<Nivel>();
 String datos = leerArchivo(nombreArchivo);
 String[] niveles = datos.Split(new char[] { '-' });
 Int32 fila = 0;
 List<Burbuja> datosNivel = new List<Burbuja>();
 // se recorre cada nivel
 foreach (string nivel in niveles)
 {
  // se recorre cada línea
  string[] lineas = nivel.Split(new char[] { '\n' });
  foreach (string linea in lineas)
  {
   if (!String.IsNullOrEmpty(linea))
   {
    int columna = 0;
    // se recorre la línea y se van a adicionando las burbujas
    foreach (string item in linea.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries))
    {
     // si es un 0 se ignora y se pasa a otra columna
     if (item != "0")
     {
     Burbuja b =  new Burbuja(Vector2.Zero, Game1.Colores[Int32.Parse(item)]);
     b.posicionCasilla = new Vector2(columna, fila);
     datosNivel.Add(b);
    }
    columna++;
   }
   fila++;
  }
 }

 // Se adiciona un nuevo nivel
 Niveles.Add(new Nivel(datosNivel));
 datosNivel = new List<Burbuja>();
 fila = 0;
 }
}

private string leerArchivo(string ruta)
{
 Assembly assembly = Assembly.GetExecutingAssembly();
 StringBuilder sb = new StringBuilder();
 StreamReader reader = new StreamReader(assembly.GetManifestResourceStream(ruta));
 string linea = reader.ReadLine();
 // Se deja todo el archivo como una sola línea
 while (linea != null)
 {
  sb.Append(linea);
  sb.Append("\n");
  linea = reader.ReadLine();
 }
 reader.Close();
 return sb.ToString();
 }
}

Lo primero que hace la clase anterior, es buscar el archivo dentro del asemmbly, luego lo recorre y va agregando todo en una sola línea, así es más fácil manejarlo, luego se divide la línea por cada “-“ que se encuentre, y se recorre cada nivel, y por cada enter  (\n) se vuelve a dividir la línea pero por cada espacio encontrado, cada vez se va guardando una nueva burbuja, y se aumentan las columnas o filas.

El archivo de texto, se adiciona al proyecto de XNA, en el mismo sitio donde están las clases, y luego se configura la propiedad Build Action para que sea un objeto incrustado o Embedded Resource:

Ahora se modifica la clase Game1 para crear el método de cargarNiveles(), este método limpia todas las listas y variables, luego va recorriendo cada burbuja que se encuentre en el nivel actual:

//Se crea la variable que guardará el nivel actual y el Adminitrador de niveles
AdministradorNiveles Niveles;
Int32 nivelActual;
//Método Initialize, se envía el nombre del archivo, primero el namespace y luego el nombre del archivo
Niveles = new AdministradorNiveles("BooblePuzzle.Niveles.txt");
nivelActual = 0;

//LoadContent
if (Niveles != null)
{
 cargarNivel();
}

//Método para cargar niveles
private void cargarNivel()
{
 //limpio todo
 burbujasPegadas.Clear();
 burbujasCayendo.Clear();
 burbujaLanzada = null;
 proximaBurbuja = null;
 rotacion = 0;

 Nivel nivel = Niveles.Niveles[nivelActual];
 //cargar Burbujas
 foreach (Burbuja b in nivel.Burbujas)
 {
  Vector2 posicionBurbuja = posicionDeCasilla((int)b.posicionCasilla.X, (int)b.posicionCasilla.Y);
  b.Posicion = posicionBurbuja;
  burbujasPegadas.Add(b);
 }
}

Ahora se debe volver a des-comentar la línea del método verificarJuegoFin, para que verifique si el juego se ha ganado, y en el método Update, cuando se verifica si el estado es Jugar, si no esta en el estado Jugar, se verifique que se encuentre en el estado Ganar y se cambia de nivel, solo si el nivel actual es menor al total de niveles, también hay que blindar el código para que no haga nada a menos que el objeto burbujaLanzada es diferente de null, esto es porque como se llama el método CargarNiveles y este deja en null las burbujas:

//Update
if (Estado == EstadoJuego.Jugar)
{
 if (burbujaLanzada != null)
 {
  burbujaLanzada.Mover();
  ///…
 }

 if (burbujaLanzada == null)
 {
  GenerarNuevaBurbuja();
 }
 verificarFinJuego();
}
else if (Estado == EstadoJuego.Ganar)
{
 if (nivelActual < (Niveles.Niveles.Count - 1))
 {
  nivelActual++;
  cargarNivel();
  Estado = EstadoJuego.Jugar;
 }
}

A iniciar, se puede ver como ya hay burbujas pegadas al techo y lo único que toca hacer es empezar a tumbarlas, cuando se han tumbado todas, se carga un nuevo nivel:

Bajar techo y disparar burbujas automáticamente

En el juego original, cuando el jugador no ha disparado la burbuja en cierto tiempo, la burbuja es disparada automáticamente, para hacerlo solo hay que tener una variable que va a aumentando con el transcurrir del juego, y cuando llegue a un límite, se llama un método que hace disparar la burbuja, luego el contador se reinicia de nuevo.

Se agrega un SpriteFont al proyecto, este servirá para mostrar un mensaje de alerta y avisarle al usuario de que tiene que disparar o se disparará automáticamente la burbuja.

float tiempoDisparar;
Boolean mostrarTextoApresurar = false;

tiempoDisparar = 0;

// Update
if (Estado == EstadoJuego.Jugar)
{
 tiempoDisparar += (float)gameTime.ElapsedGameTime.TotalSeconds;
 if (tiempoDisparar > 5 && tiempoDisparar < 5.5f)
 {
  //Mostrar Texto o hacer algo para informar que se debe apresurar
  mostrarTextoApresurar = true;
 }
 else if (tiempoDisparar > 7 && tiempoDisparar < 7.5f)
 {
  //Mostrar Texto o hacer algo para informar que se debe apresurar
  mostrarTextoApresurar = true;
 }
 else if (tiempoDisparar > 9 && tiempoDisparar < 9.5f)
 {
  //Mostrar Texto o hacer algo para informar que se debe apresurar
  mostrarTextoApresurar = true;
 }
 else if (tiempoDisparar > 10)
 {
  // si no se ha disparado y ha pasado el tiempo, se dispara la burbuja
  DispararBurbuja();
 }
 else
 {
  mostrarTextoApresurar = false;
 }
…

if (Keyboard.GetState().IsKeyDown(Keys.Space))
{
 DispararBurbuja();
}

private void DispararBurbuja()
{
 if (burbujaLanzada != null && !burbujaLanzada.moviendo)
 {
  burbujaLanzada.moviendo = true;
  // disparar burbuja
  //dependiendo de la rotación
  Matrix m = Matrix.CreateRotationZ(rotacion);
  burbujaLanzada.Direccion.X += m.M12 * 8f;
  burbujaLanzada.Direccion.Y -= m.M11 * 8f;
  tiempoDisparar = 0;
 }
}

Cuando pasen 5, 7 y 9 segundos sin disparar, se muestra un texto para que el jugador dispare, si se cumplen 10 segundos, la burbuja es disparada:

Para adicionarle más dificultad al juego, en cierta cantidad de burbujas pegadas, el techo es bajado y las burbujas se acercaran cada vez más al límite de abajo, puede verse muy sencillo, ya que lo fácil sería aumentarle 1 a la posición Y de la casilla de cada burbuja pegada, y luego calcular la nueva posición de las burbujas.

Pero ocurre un problema, ya que al bajar las burbujas, las que estaban en líneas impares se convertirán en líneas pares, y las líneas que tienen 8 burbujas, se correrán a la derecha, también se descuadrará el método de obtener vecinos, debido a que la formula o patrón para verificar si una burbuja es vecino o no se dañará:

Para corregir esto o evitarlo, es necesario modificar los métodos donde se verifique o se use el límite del techo.

Para empezar, se crean dos variables, una que tendrá el contador para bajar el techo y la segunda tendrá el nivel o el tamaño del techo que va a bajar, cada vez que se baje aumentará esta variable, también he agregado una imagen para que se vea mejor:

Int32 bajarBurbujasTiempo = 0;
public static Int32 posTecho = 0;
Texture2D textTecho;

//LoadContent
textTecho = Content.Load<Texture2D>("techo");

//Modificar el método Update, para hacer referencia a la posición del techo
…
if (burbujaLanzada.Posicion.Y <= limiteArriba + (posTecho * tamanoBurbuja))

Modificar el método pegarBurbuja para aumentar el contador para bajar el techo y hacer las validaciones:

//pegarBurbuja
public void pegarBurbuja(Burbuja burbuja)
{
 List<Burbuja> grupoIguales = burbuja.encontrarIguales();
 bajarBurbujasTiempo++;
 …
 foreach (Burbuja buble in burbujasPegadas)
 {
  raiz = false;
  // si la burbuja no es de las que estan conectadas ya al techo
  if (buble.posicionCasilla.Y > posTecho)
  {
   // buscar las burbujas conectadas
   conectadas = buble.encontrarBurbujasConectadas();
   foreach (Burbuja burb in conectadas)
   {
   //verificar que una de ellas esta pegada al techo
   if (burb.posicionCasilla.Y == posTecho)
   {
    raiz = true;
    break;
   }
  }
 …

//bajar techo
if (bajarBurbujasTiempo == 5)
{
 posTecho++;
 //limiteArriba += tamanoBurbuja;
 bajarBurbujasTiempo = 0;
 foreach (Burbuja burb in burbujasPegadas)
 {
  burb.posicionCasilla = new Vector2(burb.posicionCasilla.X, burb.posicionCasilla.Y + 1);
  burb.Posicion = posicionDeCasilla((Int32)burb.posicionCasilla.X,
   (Int32)burb.posicionCasilla.Y);
  }
 }
}

Modificar el método de posicionDeCasilla para que tenga en cuenta la posición del techo:

private Vector2 posicionDeCasilla(int cx, int cy)
{
 Rectangle dRect = new Rectangle((cx * tamanoBurbuja) + limiteIzquierdo +
 (((cy + posTecho) % 2) * (tamanoBurbuja / 2)),
 (int)cy * tamanoBurbuja + limiteArriba, tamanoBurbuja, tamanoBurbuja);
 return new Vector2(dRect.X, dRect.Y);
}

Modificar el método de cargarNivel para inicializar los contadores y la posición del techo:

posTecho = 0;
bajarBurbujasTiempo = 0;

También se puede modificar el dibujado de las casillas y dibujar el techo:

//Método Draw
for (int y = 0; y < 9; y++)
{
 for (int x = 0; x < 9; x++)
 {
  Rectangle dRect = new Rectangle((x * tamanoBurbuja) + limiteIzquierdo + (((y + posTecho) % 2) * (tamanoBurbuja / 2)),
  (int)y * tamanoBurbuja + limiteArriba, tamanoBurbuja, tamanoBurbuja);
  if (x < 8)
   spriteBatch.Draw(pointText, new Rectangle(dRect.X, dRect.Y, tamanoBurbuja, 1), new Color(255, 0, 0, 100));
  if (y < 8)
   spriteBatch.Draw(pointText, new Rectangle(dRect.X, dRect.Y, 1, tamanoBurbuja), new Color(255, 0, 0, 100));
  }
}
…

//dibujar rectángulo del techo
spriteBatch.Draw(textTecho, new Vector2(limiteIzquierdo, (limiteArriba + (posTecho * tamanoBurbuja)) - 20), null, Color.White);

En la clase Burbuja también se hacen algunas modificaciones:

public void encontrarCasillaMasCercana()
{
 int fy = (int)(Posicion.Y - Game1.limiteArriba + (Game1.tamanoBurbuja / 2))
 / Game1.tamanoBurbuja;
 int fx = (int)((Posicion.X - Game1.limiteIzquierdo + (Game1.tamanoBurbuja / 2) - (((fy + Game1.posTecho) % 2) *
 (Game1.tamanoBurbuja / 2))) / Game1.tamanoBurbuja);

 posicionCasilla = new Vector2(fx, fy);
}
public bool proximoA(Burbuja bubble)
{
 if (bubble.posicionCasilla == new Vector2(posicionCasilla.X - ((posicionCasilla.Y + 1 + Game1.posTecho) % 2),  posicionCasilla.Y - 1))
 {
  // verificar arriba izq
  return true;
 }
  else if (bubble.posicionCasilla == new Vector2(posicionCasilla.X - ((posicionCasilla.Y + 1 + Game1.posTecho) % 2) + 1, posicionCasilla.Y - 1))
 {
  // verificar arriba der
  return true;
 }
 else if (bubble.posicionCasilla == new Vector2(posicionCasilla.X - 1, posicionCasilla.Y))
 {
  // verificar izq
  return true;
 }
 else if (bubble.posicionCasilla == new Vector2(posicionCasilla.X + 1, posicionCasilla.Y))
 {
  // verificar der
  return true;
 }
 else if (bubble.posicionCasilla == new Vector2(posicionCasilla.X - ((posicionCasilla.Y + 1 + Game1.posTecho) % 2), posicionCasilla.Y + 1))
 {
  // verificar abajo izq
  return true;
 }
 else if (bubble.posicionCasilla == new Vector2(posicionCasilla.X + 1 - ((posicionCasilla.Y + 1 + Game1.posTecho) % 2), posicionCasilla.Y + 1))
 {
  // verificar abajo der
  return true;
 }
return false;
}

public void encontrarBurbujasConectadas(List<Burbuja> lista, Burbuja burbuja)
{
 burbuja.encontrarVecinos();
 foreach (Burbuja bubble in burbuja.Vecinos)
 {
  // si no se ha agregado anteriormente
  if (!bubble.destruir && !lista.Contains(bubble))
  {
   lista.Add(bubble);
   //si es raiz nos salimos para evitar buscar en todas las burbujas
   if (bubble.posicionCasilla.Y == Game1.posTecho)
    break;
   encontrarBurbujasConectadas(lista, bubble);
  }
 }
}

 

Para no alargar tanto, queda por hacer las burbujas de ayuda, la puntuación, el sonido y mejorar lo visual.

Les dejo el video de como quedaría el juego:

 

 

Y el código fuente para que lo estudien y lo mejoren: Código Fuente.

Si llegan a hacer un juego mejor, por favor pongan los link, cualquier comentario o modificación es bienvenida.

Como referencia estudié el código del juego Frozen Bubble

Anuncios

4 pensamientos en “Cómo hacer un Puzzle Bobble o Bust a Move en XNA – Parte 2

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