Como hacer un Tetris en XNA


El tetris es de los mejores juegos clásicos que cualquiera haya jugado, existen versiones para todas las consolas y para todos los equipos, este juego debería ser el Hola Mundo de la programación de Juegos, y por eso en este Post voy a mostrarles como crear un tetris fácilmente.

Algo de Historia:

La verdad no voy a escribir lo que pueden encontrar en otro lado y mucho mejor, en resumen el Tetris lo escribió un ruso, cada figura tiene 4 bloques y por eso el nombre inicia con el prefijo tetra, hubo una pelea de las grandes compañías Atari y Nintendo para hacerse con el juego, la pelea la gano Nintendo, pero luego el ruso funda Tetris company y vuelve a obtener los derechos del juego, es uno de los juegos más conocidos de la historia.

Para profundizar más sobre la historia, dirigirse a Wikipedia jejejeje y si quieren ver cosas curiosas dirigirse a mi post anterior.

Arquitectura del Juego (Por fin!!)

El juego consta de 7 figuras, cada figura tiene un parecido con una letra, por lo que vamos a llamarlas por esa letra:

Cada figura como lo comenté tiene 4 bloques, para poder simularlo podríamos crear matrices de 4X4 para guardar los bloques, son 4 debido a que la figura I, que es la más larga ocupa 4 bloques en sus rotaciones.

En las matrices podemos guardar 1s y 0s, el 1 representaría un bloque y el 0 un espacio vacío, y para poder dibujarlo recorremos cada bloque y si es 1, dibujamos ese bloque.

Para resumir voy a mostrar un ejemplo de la figura S, con cada rotación:

Para dibujar cada bloque, primero se debe saber si el cuadro es un 1 o un 0, si es un 1, entonces obtenemos sus coordenadas y calculamos el tamaño y la posición del bloque, todas figuras tienen el origen en la esquina superior izquierda.

Un ejemplo visual:

Si recorremos la figura con dos for (uno para la X y otro para la Y) y cuando encontremos un 1, entonces podemos aplicar las siguientes formulas, para encontrar la posición donde vamos a dibujar el bloque:

(POSXOrigen + (CoordenadasX * TamanoBloque)

(POSYOrigen + (CoordenadasY * TamanoBloque)

Donde PosXorigen es la posición actual de la figura, y CoordenadasX y Y, son los índices donde se encuentra el 1, en una tabla podemos ver los resultados:

CoordenadasX CoordenadasY POSX Origen POSY Origen rotacion bloque PosicionX Bloque PosicionY Bloque
1 0 490 10 0 0 518 10
0 1 490 10 0 1 490 38
1 1 490 10 0 2 518 38
2 1 490 10 0 3 546 38

 

Lo que nos importa son los campos PosicionX Bloque y PosicionY Bloque, si miramos la imagen de la figura, el primer bloque se encuentra ubicado en la posición [1,0], al hacer los cálculos de arriba nos da como resultado que el bloque se dibujará en la posición[518,10], y así sucesivamente con los demás bloques y figuras.

Lo anterior descrito lo podemos resumir o factorizar, ya que no es necesario tener una matriz de 4X4 para cada figura y con sus 4 rotaciones, si no que podemos tener una matriz de 4X2 donde no vamos a guardar 1s y 0s si no la posición X y Y de los bloques que vamos a dibujar :).

Por ejemplo, la figura L la podemos resumir así:

Ahora para no crear 28 tablas, creamos una matriz de 3 dimensiones, una de 4X4X2, si no recuerdan matrices, lo que se va a dibujar son 4 Tablas, cada tabla tendrá otra tabla de 4X2, esto es solo una figura con sus 4 rotaciones.

La fórmula para obtener la posición de los bloques no cambia mucho con la anterior, lo único que va a cambiar es que vamos a tener una variable que guardará la posición, está posición será el índice de la tabla que tiene los índices de los bloques ocupados.

Para los que aún están confundidos, los voy a mostrar la matriz de la figura L, si ven son solo 4 tablas, y cada tabla es de 4X2:

El T1, son los bloques de la rotación 1, y así sucesivamente.

Las formulas quedan ahora así:

Int32 XPosicion = (Int32)(PosicionOrigenX + (Figura[Rotacion, Bloque, 0] * TamanoBloque));

Int32 YPosicion = (Int32)(PosicionOrigenY + (Figura[Rotacion, Bloque, 1] * TamanoBloque));

Donde Figura es la matriz [4,4,2], Rotación es el índice de la tabla, y 0 y 1 es el índice de x o de la y respectivamente.

Para  el juego he creado una clase Figura, que tendrá como propiedades la posición, el Color, la rotación actual y el Tipo de la figura.

El tipo de la figura será una enumeración, y cuando se asigne un valor, vamos a asignarle a la matriz de la figura, la matriz que corresponde al tipo de figura seleccionado.

public enum TipoFigura
{
  Ninguna = -1,
  FiguraT = 0,
  FiguraI = 1,
  FiguraJ = 2,
  FiguraL = 3,
  FiguraS = 4,
  FiguraZ = 5,
  FiguraO = 6,
}

 

Cómo Funciona el Tablero:

El tablero, es otra matriz, pero de dos dimensiones, cuando una figura a caído al final del tablero, adicionamos la figura al tablero, pero como?, muy sencillo, cada figura tiene una posición X y Y, el tablero también, cuando una figura es adicionada al tablero, debemos dibujar en el tablero la misma figura de la figura adicionada, para ello usamos una fórmula parecida a la descrita anteriormente:

for (int aBloque = 0; aBloque < mNumeroDeBloques; aBloque++)
{
 mTablero[
 (Int32)_figura.Posicion.X + _figura.FiguraActual[_figura.Rotacion, aBloque, 0],
 (Int32)_figura.Posicion.Y + _figura.FiguraActual[_figura.Rotacion, aBloque, 1] - 1] = _figura.Color;
}

Tomemos de un ejemplo un tablero de 6X10, si la figura J cae en la esquina inferior izquierda, entonces adicionamos los bloques al tablero:

El resultado de la operación es el siguiente:

CoordenadasX CoordenadasY POSX Origen POSY Origen rotacion abloque PosicionX Bloque PosicionY Bloque-1
0 0 0 9 0 0 0 8
0 1 0 9 0 1 0 9
1 1 0 9 0 2 1 9
2 1 0 9 0 3 2 9

 

Dando como resultado que los bloques que se van a pintar son [0,8],[0,9],[1,9],[2,9].

El valor de los campos que adicionamos va a ser un número mayor a 0, en este caso va a ser el color de cada figura.

Para dibujar el tablero, recorremos cada bloque y si el valor es mayor a 0, entonces pintamos dicho bloque de un color, y si no lo podemos dejar así transparente o pintarlo de otro color para representar bloques que no están llenos:

Int32 aXInicial = 100;
Int32 aYInicial = 0;
for (Int32 aXPosicion = 0; aXPosicion < ANCHO; aXPosicion++)
{
 for (Int32 aYPosicion = 0; aYPosicion < ALTO; aYPosicion++)  
{  
   Int32 aPositionX = (Int32)(aXInicial + (aXPosicion * mTamanoBloque));  
   Int32 aPositionY = (Int32)(aYInicial + (aYPosicion * mTamanoBloque));  
   if ((mTablero[aXPosicion, aYPosicion] >= 0))
  {
   spriteBatch.Draw(bloque, new Rectangle(aPositionX, aPositionY, mTamanoBloque, mTamanoBloque),null, Color.Gray);
  }
  else
  {
   spriteBatch.Draw(bloque, new Rectangle(aPositionX, aPositionY, mTamanoBloque, mTamanoBloque),null, Color.White);
  }
 }
}

Cada bloque del tablero inicialmente se llena con un valor menor a 0:

public void limpiarTablero()
{
 for (int aYPosition = 0; aYPosition < ALTO; aYPosition++)
  {
   for (int aXPosition = 0; aXPosition < ANCHO; aXPosition++)
    {
      mTablero[aXPosition, aYPosition] = -1;
    }
  }
}

La clase Tablero, tendrá de constantes el ancho y alto del tablero.

Verificar Colisiones:

 

Para el juego van a existir 3 colisiones, la primera verificará que la figura no se salga de los límites del tablero, la segunda  es para verificar que la posición Y de la figura no sea menor de 0, si es menor significa que la figura a alcanzado el tope y el juego ha terminado y la última para verificar que la figura a caído o chocado con una pieza ya existente:

for (int aBloque = 0; aBloque < mNumeroDeBloques; aBloque++)
{
 // verificar que la figura no se salga de los límites del área del juego ( a la izquierda)
 if (_figura.Posicion.X < 0)  
{  
return true;  
}  
//Verificar que la figura no se salga de los límites del área de juego ( a la derecha y abajo)  
if (((_figura.Posicion.X + _figura.FiguraActual[_figura.Rotacion, aBloque, 0] > ANCHO - 1)| (_figura.Posicion.Y + _figura.FiguraActual[_figura.Rotacion, aBloque, 1] > ALTO - 1)))
 {
 return true;
 }
 //si la posicón Y de la figura es mayor de 0, significa que el juego ha terminado
 if (_figura.Posicion.Y < 0)  
{   
return true;  
}
// el bloque de la fila siguiente se encuentra ocupado
if ((mTablero[    (Int32)_figura.Posicion.X + _figura.FiguraActual[_figura.Rotacion, aBloque, 0],    (Int32)_figura.Posicion.Y + _figura.FiguraActual[_figura.Rotacion, aBloque, 1]] >= 0))
 {
   return true;
 }
}
return false;
}

Para explicar la colisión con las figuras, vamos a suponer que en el tablero ya hay bloques ocupados (con valores mayores  o igual a 0), ahora una figura I va a chocar con una figura del tablero (como en la imagen),  la colisión verifica que el bloque de la siguiente columna no sea mayor o igual a 0, si lo es entonces hay colisión, en la imagen se ve que la posición [1,17] se encuentra ocupada, al calcular la colisión nos da verdadero la siguiente fórmula:

if ((mTablero[
   (Int32)_figura.Posicion.X + _figura.FiguraActual[_figura.Rotacion, aBloque, 0],
   (Int32)_figura.Posicion.Y + _figura.FiguraActual[_figura.Rotacion, aBloque, 1]] >= 0))
 {
   return true;
 }

Verificar Filas Llenas

 

Ya vimos como dibujar y verificar las colisiones en el tablero, ahora vamos a ver cuando hay filas llenas y cómo hacer para eliminarlas, aumentando la puntuación del juego.

El algoritmo para verificar las filas llenas es sencillo:

–          Empezamos a recorrer las filas, de abajo hacia arriba

–          Por cada fila, verificamos que uno de los bloques se encuentre vacio

–          Si se encuentra vacio terminamos de recorrer dicha fila y continuamos con otra

–          Si se alcanza el total de columnas recorridas y no hay ningún bloque vacio, entonces significa que la fila se encuentra llena

–          Después de obtener la fila llena, subimos todas las filas anteriores a ella, aunque visualmente las filas se bajan

–          Cuando todas las filas se hayan subido, se limpia toda la fila 0.

La función es llamada una vez cada que se llena una fila:

public int verificarFilas()
{
  Int32 aNumeroDeLineasLimpiadas = 0;
  for (Int32 aPosicionY = ALTO - 1; aPosicionY > 0; aPosicionY--)
  {
  // Verificar si la fila esta llena en la Posición Y actual
  Boolean lineacompleta = true;
  for (Int32 aXPosition = 0; aXPosition < ANCHO; aXPosition++)  
   {    
    // Si una Posicion.X no se encuentra vacia (menor a 0) entonces la fila se encuentra llena     
    if (mTablero[aXPosition, aPosicionY] == -1)     
     {       
         lineacompleta = false;       
         break;     
      }  
   }   
    // Si la fila esta llena, entonces se elimina la fila   
   if (lineacompleta == true)   
    {    
     for (Int32 aY = aPosicionY; aY > 1; aY--)
     {
      for (Int32 aX = 0; aX < ANCHO; aX++)
       {
        // Mueven todas las filas hacia arriba
        mTablero[aX, aY] = mTablero[aX, aY - 1];
       }
     }
    // Se resetea la fila de tope
    for (Int32 aX = 0; aX < ANCHO; aX++)
    {
       mTablero[aX, 0] = -1;
     }
  aNumeroDeLineasLimpiadas += 1;
    }
  }
 return aNumeroDeLineasLimpiadas;
}

Caída Libre

 

Ahora que tenemos toda la lógica, vamos a crear una función que sea la encargada de hacer caer automáticamente las fichas, algo así como la gravedad, y se verificará la colisión con alguno de los bloques, se adiciona la figura al tablero y se crea una nueva figura:

public void Caer(GameTime gameTime)
{
   relojcaida += (float)gameTime.ElapsedGameTime.TotalSeconds;
   if (relojcaida < VelocidadCaida)
   {
     return;
   }
   relojcaida = 0f;
   mFiguraActual.Posicion.Y += 1;
   if (VerificarColision(mFiguraActual) == true)
{
 if (mFiguraActual.Posicion.Y <= 0)
 {
  return;
 }
  adicionarFigura(mFiguraActual);
  CrearNuevaFigura();
 }
}

Para el control del tiempo, utilizo la misma técnica explicada en el juego de la culebrita.

La función crearNuevaFigura, invoca la función nueva Figura de la clase Figura, la función aleatoriamente asigna una figura a la figura actual de juego.

private void CrearNuevaFigura()
{
  mFiguraActual.Tipo = mFiguraActual.SeleccionarFigura();
  mFiguraActual.Posicion.X = 5;
  mFiguraActual.Posicion.Y = 0;
  mFiguraActual.Rotacion = 0;
}

Mover Fichas y Rotarlas

 

Para el movimiento de las fichas, vamos a usar el teclado,  las teclas izquierda y derecha servirán para mover la figura hacia la derecha o izquierda, la tecla de arriba arriba rotará la figura.

La rotación solo cambia la propiedad Rotación para que cuando se dibuje la figura, se dibuje dependiendo de la rotación, seleccionando la tabla correspondiente, cuando se termina el total de las rotaciones, se vuelve a empezar el contador para hacer circular las rotaciones.

Para los movimientos, se verifica que la ficha no haya colisionado o con el tablero o con un  bloque, si ha colisionado se reversa el movimiento:

public void Mover(KeyboardState teclado, GameTime gameTime)
{
  reloj += (float)gameTime.ElapsedGameTime.TotalSeconds;
  if (reloj < VelocidadMovimiento)  
  {   
   return;  
   }  
   reloj = 0f;  
   if (teclado.IsKeyDown(Keys.Left) == true)  
   {    
     mFiguraActual.Posicion.X -= 1;    
     if (VerificarColision(mFiguraActual) == true)    
     {      
        mFiguraActual.Posicion.X += 1;    
     }  
   }
    if (teclado.IsKeyDown(Keys.Right) == true)
   {  
      mFiguraActual.Posicion.X += 1;  
      if (VerificarColision(mFiguraActual) == true)  
      {    
        mFiguraActual.Posicion.X -= 1;  
      }
    }
    if (teclado.IsKeyDown(Keys.Up) == true)
    {  
       mFiguraActual.Rotacion += 1;  
       if ((mFiguraActual.Rotacion > 3))
       {
        mFiguraActual.Rotacion = 0;
      }
    if (VerificarColision(mFiguraActual) == true)
    {
    mFiguraActual.Rotacion -= 1;
    if ((mFiguraActual.Rotacion < 0))
    {
      mFiguraActual.Rotacion = 3;
    }
  }
}
if (teclado.IsKeyDown(Keys.Down) == true)
{
  mFiguraActual.Posicion.Y += 1;
  if (VerificarColision(mFiguraActual) == true)
  {
    mFiguraActual.Posicion.Y -= 1;
  }
}
}

Ahora, lo que queda por hacer es manejar la puntuación, el nivel, aumento de la velocidad, mostrar la figura siguiente, todo esto viene en el código fuente que pueden descargar.

El video de como queda es así:

Código Fuente del Tetris.

Ejecutable para probar la aplicación.

14 pensamientos en “Como hacer un Tetris en XNA

  1. Muchas gracias por tal excelente explicación acerca de la lógica de este jueguito!!!

    Ahora me será más fácil desarrollarlo en Java!!!

    Gracias…

  2. Hola amigo!!!

    Pues aquí molestando. Sabes, no me queda muy claro de porque definir 4 matrices de 4×2, tampoco de que representan los números 2, 1, 0 etc. No les hayo forma.

    ¿Me podrías ayudar con eso?

    Gracias

    • Hola,
      La matriz de 4X4x2 es hecha para resumir, y no hacer matrices de 4X4, si hicieramos matrices de 4X4, tendríamos que llenar los espacios donde va el cuadro de la figura con 1 y con 0 los espacios con blanco, pero al hacerlas de 4X2, vamos a llenar en los cuadros las posiciones donde va quedar el cuadro dibujado, por ejemplo la figura L, va a tener los cuadros en las Posiciones: {0,0}, {0,1},{0,2} y {0,3}, el primer valor represta el eje de las X y el segundo el de las Y.
      Lo de arriba quiere decir que la figura va a tener el siguiente modelo:
      0 1 2 3
      0 1 0 0 0
      1 1 0 0 0
      2 1 0 0 0
      3 1 0 0 0
      Si te fijas, los 1 van en las posiciones que guardamos en la matriz 4X2. Espero sirva la explicación

  3. Ok muchas gracias, ahora si me quedo mucho mas claro. Voy a seguir practicando en Java. Gracias

    PD: Espero que no se moleste si me surge alguna nueva duda. 🙂

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