Cap Extra OnWeb VideoJuegos Tetris

52
 Te tr is La mayoría de nosotros alguna vez  jugamo s al Tetris o, al menos, conocemos de qué se trata este juego. Por lo tanto, sin más preámbulo, desarrollaremos un juego del mismo estilo, en donde pondremos en práctica lo aprendido y abordaremos un nuevo concepto: el mapa. Video Juegos SERVICIO DE ATENCIÓN AL LECTOR: [email protected]

Transcript of Cap Extra OnWeb VideoJuegos Tetris

  • TetrisLa mayora de nosotros alguna vez

    jugamos al Tetris o, al menos,

    conocemos de qu se trata este juego.

    Por lo tanto, sin ms prembulo,

    desarrollaremos un juego del mismo

    estilo, en donde pondremos en prctica

    lo aprendido y abordaremos un nuevo

    concepto: el mapa.

    Video Juegos

    SERVICIO DE ATENCIN AL LECTOR: [email protected]

  • COMPONENTES Y ESTRUCTURAEl Tetris es un juego del tipo puzzle donde ir descendiendo una figura (pieza)dentro de un rectngulo (tablero o mapa). Una vez que sta colisiona con el bor-de inferior del rectngulo o contra otra figura, se estampar all y se lanzar una nue-va pieza. stas debern ser ubicadas de manera tal de llenar filas para que sean eli-minadas, evitando que lleguen hasta el extremo superior.

    Figura 1. El Tetris es un clsico juego del tipo puzzle.

    El origen del nombre del juego proviene de la palabra tetra (cuatro) dado que laspiezas posibles estn conformadas por las combinaciones de cuatro cuadrados, co-mo nos muestra la Figura 2.

    Figura 2. Las piezas estn conformadas por todas

    las combinaciones posibles de cuatro cuadrados.

    Como vimos en el captulo anterior, si imaginamos el juego terminado, podemosdeducir qu elementos necesitamos. Observando la descripcin, deducimos que ne-cesitaremos como mnimo una pieza y un tablero donde estamparla. Por lo tanto,el diagrama de clases se ver como nos muestra la Figura 3.

    TETRIS

    2

  • Figura 3. Observemos que necesitamos, al menos, una clase

    que manipule la pieza que ir descendiendo y otra para el mapa.

    La clase Piece se encargar de almacenar, trasladar, rotar y mostrar la pieza en esce-na. Luego, la clase BoardMap manipular el mapa chequeando colisiones de la pie-za con los bordes y con las piezas ya estampadas en l.

    Veamos, a continuacin, las clases en detalle con su cdigo fuente.

    LA CLASE PIECEComo ya se dijo, la clase Piece se encargar de almacenar, trasladar, rotar y mostrarla pieza en escena.

    Cada pieza estar representada por un arreglo cuyo primer elemento corresponde-r al tamao del lado de la pieza, y el resto, a la forma.

    Figura 4. La pieza cuadrada del juego.

    Menu TestGame

    zak::Game

    1

    1

    2 1

    1 11

    11 1

    1 1

    InGame

    zak::Sprite

    Intro

    BoardMapPlace

    La clase Piece

    3

  • En la Figura 4, apreciamos la pieza cuadrada. sta posee dos casilleros por lado, porlo tanto, el arreglo que la define tendr la siguiente informacin:

    {2,1,1,1,1}

    El elemento 2 indica que posee dos casilleros por lado; los unos siguientes repre-sentan la forma de la pieza y sern interpretados como una matriz de 2x2. Vea-mos otro ejemplo:

    Figura 5. Esta pieza posee tres casilleros por lado.

    La pieza que muestra la Figura 5 posee tres casilleros por lado y, por lo tanto, el arre-glo que la representa se ver del siguiente modo:

    {3,0,0,0,0,1,0,1,1,1}

    El 3 indicar que esta pieza posee tres casilleros por lado. Luego, del mismo modoque en el ejemplo anterior, los subsiguientes ceros y unos representarn la forma ens misma, como nos muestra la Figura 6.

    Figura 6. La figura estar representada por un arreglo cuadrado de NxN elementos dondeel primer valor indicar el tamao por lado de la figura, y el resto codificar su forma.

    Definiremos el resto de las figuras de manera anloga, como indica la Figura 7.

    0 0 0

    0 0 0

    0 0 0

    0 0 0 1 0 1 1 1

    TETRIS

    4

  • Figura 7. Aqu vemos como se definen los figuras restantes.

    Como vemos, siempre trabajamos con matrices cuadradas de NxN elementos. Es-to seguramente nos trae a la mente la pregunta por qu utilizar una matriz cuadra-da cuando hay toda una fila o columna en cero? Se debe a la manera en que desa-rrollaremos la rotacin que veremos ms adelante en este captulo.

    Observemos el archivo cabecera de la clase Piece:

    #pragma once

    #include zakengine/zak.h

    using namespace zak;

    #define MAX_PIECES 7#define MAX_PIECE_SIZE 4

    class Piece : public Sprite {public:

    void SetRandomType();void SetType(int type, DWORD color);int GetType() { return _type; }

    {4, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0}

    {3, 0, 1, 0, 0, 1, 0, 1, 1, 0}

    {3, 0, 1, 0, 0, 1, 0, 0, 1, 1}

    {3, 0, 1, 0, 1, 1, 0, 1, 0, 0,}

    {3, 1, 0, 0, 1, 1, 0, 0, 1, 0}

    La clase Piece

    5

  • void RotateCW();void RotateCCW();void MoveLeft() { _col; }void MoveRight() { _col++; }void MoveUp() { _row; }void MoveDown() { _row++; }

    DWORD GetColorByType(int type);

    void SetStartPos(float x, float y) { _startPosX = x; _startPosY = y;}

    void Update(float dt);void Draw();

    Piece();~Piece();

    private:int _size;DWORD _data[MAX_PIECE_SIZE*MAX_PIECE_SIZE];DWORD _color;int _row;int _col;float _startPosX;float _startPosY;int _type;

    friend class BoardMap;};

    Comencemos por las constantes de preprocesador:

    #define MAX_PIECES 7#define MAX_PIECE_SIZE 4

    La constante MAX_PIECES indica la cantidad mxima de tipos de piezas que tendre-mos. En el caso del Tetris, este valor est definido por la cantidad de combinacio-nes posibles entre cuatro cuadrados, que da como resultado 7.

    TETRIS

    6

  • Figura 8. Existen innumerables clones de este clsico, incluso para GameBoy.

    Luego tenemos la constante MAX_PIECE_SIZE, que identificar el tamao mxi-mo de un lado de la matriz cuadrada ms grande que podr tener una pieza; eneste caso, ser de 4x4.

    Pasemos ahora a los mtodos:

    void SetRandomType();

    Este mtodo seleccionar de manera aleatoria un tipo de pieza.

    void SetType(int type, DWORD color);

    El mtodo SetType permitir la seleccin de un tipo especfico de pieza y podremospasarle, adems, el color.

    int GetType() { return _type; }

    Devolver el tipo de pieza.

    void RotateCW();void RotateCCW();

    La clase Piece

    7

  • Los mtodos RotateCW (rotacin clockwise o segn las agujas del reloj) y RotateCCW(rotacin counter clockwise o contra el sentido de las agujas del reloj) sern invoca-dos para rotar la pieza segn corresponda.

    void MoveLeft() { _col; }void MoveRight() { _col++; }void MoveUp() { _row; }void MoveDown() { _row++; }

    Con estos mtodos, moveremos la pieza a derecha, izquierda, arriba y abajo segnnecesitemos.

    DWORD GetColorByType(int type);

    Esto devolver el color que corresponda, a partir de un tipo de pieza pasado porparmetro.

    void SetStartPos(float x, float y) { _startPosX = x; _startPosY = y;}

    Permitir seleccionar la posicin desde la cual se comienza a dibujar la pieza paraque coincida con el trazado del mapa.

    void Update(float dt);

    Actualizar la posicin y, si la tuviera, la animacin de la pieza.

    void Draw();

    El mtodo Draw, como siempre, dibuja la pieza.

    Veamos ahora las propiedades:

    int _size;

    Tamao del lado de la matriz correspondiente a la pieza actual.

    TETRIS

    8

  • DWORD _data[MAX_PIECE_SIZE*MAX_PIECE_SIZE];

    Arreglo que contendr la codificacin de la pieza actual.

    DWORD _color;

    Color de la pieza actual.

    int _row;int _col;

    Columna y fila en la que se encuentra la pieza actual dentro del mapa.

    float _startPosX;float _startPosY;

    Posicin en coordenadas de mundo a partir de la cual se comienza a dibujar la pieza.

    int _type;

    Tipo de pieza que se muestra.

    friend class BoardMap;

    Por ltimo, indicamos que la clase BoardMap tendr acceso a las propiedades priva-das de la clase.

    Veamos ahora el cdigo fuente de la clase:

    #include piece.hunsigned char pieceType[][1+(MAX_PIECE_SIZE*MAX_PIECE_SIZE)] = {

    {2,1, 1,1, 1},

    La clase Piece

    9

  • {3,0, 1, 0,1, 1, 1,0, 0, 0},

    {3,1, 0, 0,1, 1, 0,0, 1, 0},

    {3,0, 1, 0,1, 1, 0,1, 0, 0},

    {3,0, 1, 0,0, 1, 0,0, 1, 1},

    {3,0, 1, 0,0, 1, 0,1, 1, 0},

    {4,0, 1, 0, 0,0, 1, 0, 0,0, 1, 0, 0,0, 1, 0, 0}

    };

    Piece::Piece() {_size = 0;_color = 0xFF00FF00;_col = 0;_row = 0;_startPosX = 0;_startPosY = 0;

    }

    Piece::~Piece() {

    }

    TETRIS

    10

  • DWORD Piece::GetColorByType(int type) {DWORD color = 0xFF000000;

    switch(type) {case 0:

    color = 0xFFFF0000;break;

    case 1:color = 0xFF00FF00;break;

    case 2:color = 0xFF0000FF;break;

    case 3:color = 0xFFFFFF00;break;

    case 4:color = 0xFF00FFFF;break;

    case 5:color = 0xFFFF00FF;break;

    case 6:color = 0xFF601060;break;

    }return color;

    }

    void Piece::SetRandomType() {int rnd = rand()%MAX_PIECES;

    SetType(rnd, GetColorByType(rnd));}

    void Piece::SetType(int type, DWORD color) {

    // Almaceno el tamao de lado de la pieza_size = pieceType[type][0];

    La clase Piece

    11

  • _type = type;

    _color = color;

    for (int row=0; row

  • {ncol = row;nrow = _size-1-col;newPiece[ncol + nrow * _size] = _data[col + row * _size];

    }

    // Copio la pieza temporal a la definitivamemcpy((void *) _data, (const void *) newPiece, _size * _size *

    sizeof(DWORD));}

    void Piece::Update(float dt) {float x = GetPosX();float y = GetPosY();

    Sprite::Update(dt);

    SetPos(_startPosX+_col*GetWidth()*GetScaleX(), _startPosY-_row*GetHeight()*GetScaleY());

    }

    void Piece::Draw() {float x = GetPosX();float y = GetPosY();

    if (GetVisible()) {for (int row=0; row

  • SetPos(x,y);}

    Analicemos detalladamente cada parte del cdigo fuente comenzando por la defini-cin del arreglo constante de piezas:

    unsigned char pieceType[][1+(MAX_PIECE_SIZE*MAX_PIECE_SIZE)] = {{2,1, 1,1, 1},

    {3,0, 1, 0,1, 1, 1,0, 0, 0},

    {3,1, 0, 0,1, 1, 0,0, 1, 0},

    {3,0, 1, 0,1, 1, 0,1, 0, 0},

    {3,0, 1, 0,0, 1, 0,0, 1, 1},

    {3,0, 1, 0,0, 1, 0,1, 1, 0},

    {4,0, 1, 0, 0,0, 1, 0, 0,0, 1, 0, 0,0, 1, 0, 0}

    };

    Definimos una matriz constante que har las veces de repositorio de piezas. Elarreglo es de dos dimensiones, donde la primera componente indicar la pieza por

    TETRIS

    14

  • mostrar mientras que la segunda alojar la informa-cin de la pieza. El tamao de la segunda componen-te estar dado por el tamao del lado de la matriz de-finido por MAX_PIECE_SIZE elevado al cuadrado (poreso, lo multiplicamos por s mismo MAX_PIECE_SIZE*MAX_PIECE_SIZE) sumndole uno dado. Como yavimos, deberemos guardar como primer elemento eltamao del lado de la matriz que codifica la pieza.

    Figura 9. Modificando los valores del arreglo,

    podramos crear una versin del Tetris con piezas

    formadas por cinco cuadrados en vez de cuatro.

    Pasemos ahora a analizar los mtodos de la clase:

    DWORD Piece::GetColorByType(int type) {DWORD color = 0xFF000000;switch(type) {

    case 0:color = 0xFFFF0000;break;

    case 1:color = 0xFF00FF00;break;

    case 2:color = 0xFF0000FF;break;

    case 3:color = 0xFFFFFF00;break;

    case 4:

    La clase Piece

    15

  • color = 0xFF00FFFF;break;

    case 5:color = 0xFFFF00FF;break;

    case 6:color = 0xFF601060;break;

    }return color;

    }

    Como podemos ver, simplemente, decidimos el color segn el tipo de pieza.

    void Piece::SetType(int type, DWORD color) {

    // Almaceno el tamao de lado de la pieza_size = pieceType[type][0];

    Almacenamos el tamao de la pieza que ser el primer elemento del arreglo quecontiene su codificacin.

    _type = type;_color = color;

    Guardamos el tipo y el color de la pieza.

    for (int row=0; row

  • _data[col+row*_size] = pieceType[type][(col+row*_size)+1]*_color;

    Si analizamos mejor la forma en que recorremos los arreglos, veremos que utili-zamos la expresin col+row*_size. Esto nos permitir manipular un arreglo uni-dimensional como si fuera una matriz bidimensional por medio de columnas yfilas, como indica la Figura 10.

    Notemos que la expresin en el arreglo pieceType difiere de la de _data, dado que sele suma uno. Esto se debe a que pieceType contiene como primer componente el ta-mao del arreglo que no copiaremos dentro de _data y, por lo tanto, debemos saltear.

    Figura 10. Utilizando la expresin col+row*_size, podemos recorrer un arreglo unidimensional por medio

    de componentes de columna y fila, como si fuera una matriz bidimensional.

    _col = 0;_row = 0;

    0 0 00 1 01 1 1

    col

    _size = 30 0 00 1 01 1 1

    row

    0 1 2

    0

    1

    2

    col+row*_size = 0+0*3 = 0 0 0 0 0 1 0 1 1 1

    0

    0 0 00 1 01 1 1

    Col = 0Row = 0

    Col = 1Row = 0

    0 1 2

    0

    1

    2

    col+row*_size = 1+0*3 = 10 0 00 1 01 1 1

    0 1 2

    0

    1

    2

    Col = 1Row = 1

    col+row*_size = 1+1*3 = 4

    0 1 2

    0

    1

    2

    0 0 0 0 1 0 1 1 1

    1

    0 0 0 0 1 0 1 1 1

    4

    La clase Piece

    17

  • Inicializamos la posicin en el mapa a cero.

    SetVisible(true);}

    Hacemos visible la pieza.

    void Piece::SetRandomType() {int rnd = rand()%MAX_PIECES;

    SetType(rnd, GetColorByType(rnd));}

    Como vemos, el mtodo SetRandomType toma un nmero aleatorio entre 0 y MAX_PIECES-1 por medio de la expresin rand()%MAX_PIECES y luego la inicializa consu color haciendo uso del mtodo SetType visto anteriormente.

    void Piece::RotateCCW() {int ncol, nrow;DWORD newPiece[MAX_PIECE_SIZE*MAX_PIECE_SIZE];

    // Roto la piezafor (int row=0; row

  • Figura 11. La rotacin se desarrolla sobre el arreglo de la pieza.

    Ahora debemos determinar qu columna/fila pasar a qu columna/fila. Para esto,tendemos que analizar la tabla que vemos en la Figura 12.

    (COLUMNA, FILA) DESDE (COLUMNA, FILA) HACIA

    (0, 0) (0, 2)

    (0, 1) (1, 2)

    (0, 2) (2, 2)

    (1, 0) (0, 1)

    (1, 1) (1, 1)

    (2, 0) (0, 0)

    (2, 1) (1, 0)

    (2, 2) (2, 0)

    Figura 12. Analizando la tabla, podemos determinar

    qu columna/fila pasar a qu columna/fila.

    Como se puede apreciar en la tabla, lo que en la primera columna son filas, en lasegunda, terminan siendo columnas. Por lo tanto, obtenemos la primera relacin:

    ncol = row;

    Nos resta calcular cul es la fila en la cual termina cada elemento. Si analizamosnuevamente la tabla, inferimos que de 0 pasa a ser 2, de 1 a 1 y de 2 a 0. Por lotanto, deducimos que:

    nrow = 2 col;

    Luego determinamos un mtodo general que sirva para todos los casos. Por lo tan-to, analizamos de dnde sale el 2. Al tratarse de una pieza cuyo tamao es 3, dedu-cimos que el 2 es el tamao menos uno. Por lo tanto, nos quedar:

    0 1 2

    0 1 2

    3 4 5

    6

    0

    1

    2 7 8

    2 5 8

    0 1 2

    1 4 7

    0

    0

    1

    2 3 6

    La clase Piece

    19

  • nrow = _size 1 col;

    Por ltimo, debemos copiar la pieza obtenida en newPiece de nuevo a su arreglooriginal _data:

    // Copio la pieza temporal a la definitivamemcpy((void *) _data, (const void *) newPiece, _size *

    _size * sizeof(DWORD));

    Para ello, utilizamos la funcin memcpy que copiar la cantidad de bytes total delnuevo arreglo newPiece. Como indica la Figura 13, los bytes de memoria reservadapor un arreglo (no importa si es unidimensional o de ms de una dimensin) estnalineados. Por lo tanto, debemos pasarle por parmetro a la funcin el tamao delarreglo en bytes que, en este caso, estar dado por la expresin _size * _size * si-zeof(DWORD) desde la posicin de memoria a la que apunta el arreglo original _data.

    Figura 13. La memoria de un arreglo (unidimensional

    o bidimensional) se encuentra alojada de manera secuencial.

    ndice01234

    0

    En memoria, los bytes del arregloestn alineados consecutivamente.

    Un DWORD ocupa 4 bytesPor lo tanto, un arreglo deDWORDs se vera de la siguiente manera:

    1 2 3 4

    TETRIS

    20

    Modificando las constantes MAX_PIECES, MAX_PIECE_SIZE y pieceType convenientemente, pode-

    mos crear un nuevo juego con menos piezas grandes o ms piezas ms pequeas. Por ejemplo,

    podramos crear un Pentrix formando piezas con la combinacin de cinco cuadrados.

    CONSTANTES

  • Anlogamente, podemos rotar la pieza en sentido contrario por el mtodo RotateCW.

    Veamos ahora el mtodo Update:

    void Piece::Update(float dt) {Sprite::Update(dt);

    SetPos(_startPosX+_col*GetWidth()*GetScaleX(), _startPosY-_row*GetHeight()*GetScaleY());

    }

    Lo que haremos ser tomar las variables _startPosX y _startPosY que indicarn laposicin donde deber comenzar a dibujarse la pieza en coordenadas de mundo.Luego sumaremos la columna (_col) y la fila (_row) dadas en coordenadas localesde la matriz, multiplicadas por el ancho y el alto, respectivamente, y por la escaladel sprite. De esta manera, transformaremos la posicin dentro de la matriz encoordenadas de mundo para poder mostrar cada cuadrado de la pieza en la posi-cin adecuada respecto de su tamao y de su escala. Los valores de _startPosX y_startPosY debern coincidir con la posicin del mapa.

    Figura 14. Debemos indicar en qu posicin de mundo se comenzar a dibujar la pieza.

    _startPosX

    Coord. de mundo en pantalla

    _startPosY

    0

    0

    La clase Piece

    21

  • Por ltimo, tenemos el mtodo Draw:

    void Piece::Draw() {float x = GetPosX();float y = GetPosY();

    Tomamos la posicin actual de la pieza en coordenadas de mundo.

    if (GetVisible()) {for (int row=0; row

  • Dibujamos el cuadrado correspondiente y deshabilitamos la modulacin del color.

    SetPos(x,y);}

    Por ltimo, volvemos la pieza a su posicin original, dado que fue modificada an-teriormente para mostrar cada uno de los recuadros.

    Quedar como ejercicio probar la pieza para comprender mejor su funcionamiento.

    LA CLASE BOARDMAPLa clase BoardMap ser la encargada de manipular el tablero o mapa del juego y delas verificaciones de colisin. Cada vez que seleccionamos una pieza y la lanzamosdesde el borde superior, verificaremos en cada chequeo de reglas si colisiona o nocon alguna otra pieza o con algn extremo del mapa.

    Figura 15. Verificaremos si la pieza colisiona

    con alguna otra pieza o con algn extremo del tablero.

    A grandes rasgos, podramos establecer que, cuando la pieza no colisiona contra na-da dentro del mapa, la posicin hacia la cual deseamos moverla es vlida y podre-mos actualizar definitivamente su posicin.

    La clase BoardMap

    23

  • Figura 16. Si la pieza no colision, actualizamos su posicin definitivamente.

    En un determinado momento, la pieza colisionar contra otra o contra el borde in-ferior del mapa; llegada esta situacin, se proceder a estamparla, es decir, se copia-r la informacin que describe la pieza en el arreglo que representa el mapa (en laltima posicin vlida en que qued), como vemos en la Figura 17.

    Figura 17. Si la pieza colision, la estampamos

    en su posicin inmediatamente anterior al evento.

    Posicin vlida Posicin vlida Colisin!

    Estampamos en la posicin anterior

    Posicin vlida Posicin vlida Colisin!

    TETRIS

    24

  • Luego el objeto pieza se reutilizar, seleccionando otra cualquiera (al azar) y vol-vindola a soltar desde el borde superior del mapa.

    Veamos, entonces, en qu consiste la clase BoardMap, comenzando por su archi-vo de cabecera:

    #pragma once

    #include zakengine/zak.h

    #include piece.h

    using namespace zak;

    class BoardMap : public Sprite {

    public:static const int COLS = 10;static const int ROWS = 20;

    BoardMap(void);~BoardMap(void);

    // Dibuja el mapavoid Draw();

    // Actualiza void Update(float dt);

    // Verifica si la pieza colisiona contra el tablerobool CollideWith(Piece & piece);

    // Copia la pieza en el tablerovoid StampPiece(Piece & piece);

    // Limpia una lnea especfica del tablerovoid ClearLine(int row);

    // Busca lneas formadas en el tablero

    La clase BoardMap

    25

  • int CheckForLines();

    // Reseteo el tablerovoid Reset();

    private:DWORD _map[COLS][ROWS];

    };

    Como podemos ver, la clase BoardMap hereda de la clase Sprite puesto que deberdibujar en pantalla cada bloque estampado en l.

    static const int COLS = 10;static const int ROWS = 20;

    Tendr dos constantes estticas llamadas COLS y ROWS que incluirn la cantidadde columnas y filas que tendr el mapa. Cambiando estos valores, podremos mo-dificar su tamao.

    // Dibuja el mapavoid Draw();

    // Actualiza void Update(float dt);

    Adems sobrecargaremos los mtodos Draw y Update puesto que, como veremosms adelante, debemos dibujar el mapa recorriendo cada elemento del arreglo.

    // Verifica si la pieza colisiona contra el tablerobool CollideWith(Piece & piece);

    Dada una pieza, esta clase chequear si colisiona o no con el mapa. En caso afirma-tivo, devolver true, y false en caso contrario.

    // Copia la pieza en el tablerovoid StampPiece(Piece & piece);

    TETRIS

    26

  • En el caso de que la pieza hubiera colisionado con el borde inferior o, al bajar, conotra pieza, deber ser estampada por medio de este mtodo.

    // Limpia una lnea especfica del tablerovoid ClearLine(int row);

    Como veremos ms adelante, una vez que una lnea horizontal fue completada, s-ta deber ser eliminada para evitar llegar al extremo superior del mapa.

    // Busca lneas formadas en el tableroint CheckForLines();

    Este mtodo chequear si hubo lneas horizontales completadas. En ese caso, se-rn eliminadas por medio del mtodo ClearLine, y devolver la cantidad encon-trada. De lo contrario, devolver 0.

    // Reseteo el tablerovoid Reset();

    Llenar la matriz que define el mapa de ceros.

    Pasemos ahora a analizar el cdigo fuente de la clase:

    #include boardmap.h

    BoardMap::BoardMap(void) {Reset();

    }

    La clase BoardMap

    27

    Por medio de la utilizacin de las constantes COLS y ROWS, tenemos la posibilidad de definir la

    cantidad de columnas y de filas que contiene el mapa del juego.

    CONSTANTES

  • BoardMap::~BoardMap(void) {

    }

    // Dibuja el mapavoid BoardMap::Draw() {

    float x = GetPosX();float y = GetPosY();

    g_renderer.EnableModulate();

    for (int i=0; i

  • {// Me estoy yendo del mapa por la derecha o

    por la izquierdaif (piece._data[col + row * piece._size] != 0)

    return true;

    }else if (piece._row+row >= ROWS || piece._row+row < 0){

    // Estoy tocando el piso del mapa o // estoy por encima del mapa -> ste es un

    caso posible?if (piece._data[col + row * piece._size] != 0)

    return true;

    }else if (_map[piece._col+col][piece._row+row] != 0 &&

    piece._data[col + row * piece._size] != 0)// Una parte de la pieza toca otra pieza del mapareturn true;

    }

    return false;}

    // Copia la pieza en el tablerovoid BoardMap::StampPiece(Piece & piece) {

    for (int row=0; row= 0 && piece._row+row < ROWS &&piece._data[col + row * piece._size] != 0)_map[piece._col+col][piece._row+row] = piece.

    _data[col + row * piece._size];}

    piece.SetVisible(false);}

    // Limpia una lnea especfica del tablerovoid BoardMap::ClearLine(int row) {

    La clase BoardMap

    29

  • // Copio las lineas hacia abajofor (int i=row; i>0; i)

    for (int col=0; col

  • for (int j=0; j ste es un

    caso posible?if (piece._data[col + row * piece._size] != 0)

    return true;

    }else if (_map[piece._col+col][piece._row+row] !=

    0 && piece._data[col + row * piece._size] != 0)// Una parte de la pieza toca otra pieza del mapareturn true;

    }return false;

    }

    Como se puede apreciar en el listado, realizamos una iteracin en todas las posicio-nes de la pieza por medio de dos bucles anidados. Notemos que no recorremos elmapa completo, puesto que sera ms lento.

    La clase BoardMap

    31

  • Por cada posicin de la pieza verificaremos: No quedar fuera del mapa por izquierda ni por derecha. No quedar fuera del mapa por abajo ni por arriba. No quedar superpuesto a otra pieza ya estampada en el mapa.

    Sin embargo, vale una aclaracin muy importante: nuestra pieza, trabajada como sifuese un arreglo de dos dimensiones cuadrado, posee una forma irregular. Por lotanto, no todos los elementos del arreglo son necesariamente parte de la pieza. Co-mo vemos en la Figura 17, la pieza parece quedar fuera del mapa, pero no es as da-do que la parte que qued afuera no contiene ninguna porcin slida.

    Figura 18. No todos los elementos del arreglo son necesariamente parte de la pieza.

    // Copia la pieza en el tablerovoid BoardMap::StampPiece(Piece & piece) {

    for (int row=0; row= 0 && piece._row+row < ROWS &&piece._data[col + row * piece._size] != 0)_map[piece._col+col][piece._row+row] =

    piece._data[col + row * piece._size];}

    piece.SetVisible(false);}

    Estampar una pieza en el mapa es bastante simple. Bastar con recorrer el arreglocon dos bucles anidados (de manera anloga a la que desarrollamos anteriormente)en busca de algn elemento diferente de cero (es decir, slido).

    TETRIS

    32

  • _map[piece._col+col][piece._row+row] = piece._data[col + row * piece._size];

    En el caso de tratarse de un elemento slido, lo copiamos en la posicin correspon-diente del mapa que estar dada por la columna y la fila en las que se encuentra lapieza (piece._col y piece._row) dentro del mapa, sumndole la columna y la fila co-rrespondiente que estemos leyendo dentro del arreglo (variables locales col y row de-claradas en los bucles for).

    // Busca lneas formadas en el tableroint BoardMap::CheckForLines() {

    bool isALine;int lines=0;

    Buscar si una lnea fue completada resulta trivial. Recorremos la matriz del mapa.Mientras no haya un cero en alguno de los elementos, seguimos recorriendo.

    for (int row=ROWS; row>=0; row){

    isALine = true;

    Inicializamos en true la bandera (o flag) isALine, suponiendo que hay lnea paracambiarla a false en caso contrario.

    for (int col=0; col

  • TETRIS

    34

    El Tetris es un juego muy popular desarrollado originalmente por Alexey Pajitnov en junio de

    1985, mientras trabajaba en Mosc en el Centro de Computacin Dorodnicyn de la Academia de

    Ciencia de la Unin Sovitica.

    HISTORIA DEL TETRIS

    {lines++;ClearLine(row);row++;

    }

    Si hubo alguna lnea (es decir que isALine en este momento es igual a true), incre-mentamos en uno la variable lines para almacenar la cantidad encontrada, elimina-mos esa lnea por medio del mtodo ClearLines que veremos a continuacin y sal-tamos a la fila siguiente forzando a row a incrementarse.

    }return lines;

    }

    Por ltimo, retornamos la cantidad de filas encontradas.

    // Limpia una lnea especfica del tablerovoid BoardMap::ClearLine(int row) {

    // Copio las lneas hacia abajofor (int i=row; i>0; i)

    for (int col=0; col

  • Figura 19. Borrar una lnea consiste en copiar parte del mapa hacia otra posicin.

    Como podemos ver, detectada la lnea, debemos copiar parte del tablero a otra po-sicin dentro de l. Tambin observamos que parte del tablero permanece inaltera-do y que debemos modificar la lnea superior y fijarla en cero (valor que indica va-co en nuestra implementacin). Veamos la Figura 19 para aclarar el concepto.

    Figura 20. Viendo un ejemplo prctico, comprenderemos mejor de qu se trata.

    Para esto, bastarn dos iteraciones:

    // Copio las lneas hacia abajofor (int i=row; i>0; i)

    for (int col=0; col

  • En esta iteracin, hacemos la copia de cada elemento de la fila superior sobre la actual.

    // Elimino la primera lneafor (int col=0; col

  • Indicamos la posicin donde debe dibujarse el elemento del mapa. Para esto, toma-mos la posicin que almacenamos anteriormente en x e y y le sumamos los conta-dores j e i del bucle multiplicados por el ancho y el alto, respectivamente, y luegopor la escala que, si bien por defecto es igual a 1.0f, debemos tenerla en cuenta porsi ms adelante deseamos modificarla.

    Figura 21. En el diagrama, suprimimos la escala para simplificar la explicacin.

    Sprite::Draw();

    Ahora dibujamos el elemento con el color determinado y en su posicin.

    }}g_renderer.DisableModulate();

    Deshabilitamos la modulacin de color, puesto que ya finalizamos el bucle y, portanto, el trazado.

    SetPos(x,y);}

    ancho

    y

    x

    PosX = x + 0 * anchoPosY = y 0 * alto

    alto

    ancho

    PosX = x + 1 * anchoPosY = y 0 * alto

    alto

    ancho

    PosX = x + 2 * anchoPosY = y 0 * alto

    alto

    ancho

    PosX = x + 0 * anchoPosY = y 1 * alto

    altoPor lo tanto, concluimos que:PosX = x + i * anchoPosY = y j * alto

    La clase BoardMap

    37

  • Por ltimo, volvemos a indicar la posicin original almacenada en x e y.

    Queda como ejercitacin integrar el mapa en el juego para entender, de maneraprctica, cada uno de los mtodos.

    LA CLASE PRINCIPAL INGAMEHabiendo implementado las funciones ms importantes del juego (rotaciones,verificaciones de colisin, etctera), slo nos resta analizar cmo debe funcionarel juego en s mismo.

    Figura 22. Mientras programamos un clon, nunca debemos

    perder de vista el juego original para mantener su esencia.

    El objetivo aqu ser mover la ficha activa hacia el borde inferior del tablero e invo-car la verificacin de colisiones, estampa al tablero y verificacin de lneas armadas.

    Veamos entonces el archivo cabecera de la clase InGame:

    #pragma once

    #include zakengine/zak.h

    #include boardMap.h

    TETRIS

    38

  • #include piece.h

    using namespace zak;

    #define GAME_STATE_NONE 0#define GAME_STATE_EXIT 1#define GAME_STATE_NEXTLEVEL 2

    #define START_POS_X -100.0f#define START_POS_Y 100.0f

    #define NEXT_PIECE_POS_X -200.0f#define NEXT_PIECE_POS_Y 100.0f

    #define FALL_INTERVAL 1000.0f#define MOVE_INTERVAL 80.0f

    class InGame {public:

    bool Initialize(int level);bool Shutdown();

    void SetState(int state) { _state = state; }int GetState() { return _state; }

    void Update(float dt);void Draw();

    InGame();~InGame();

    private:int _state;Piece _piece;Piece _nextPiece;BoardMap _boardMap;float _fallInterval;int _score;

    };

    La clase principal InGame

    39

  • Como vemos, los mtodos son los mismos que en los casos desarrollados en captu-los anteriores. Por lo tanto, prestemos atencin a las nuevas constantes y propiedades.

    En cuanto a las constantes, tendremos las siguientes:

    #define START_POS_X -100.0f#define START_POS_Y 100.0f

    Las constantes START_POS_X y START_POS_Y identificarn la posicin del mapaen pantalla.

    #define NEXT_PIECE_POS_X -200.0f#define NEXT_PIECE_POS_Y 100.0f

    Luego, NEXT_PIECE_POS_X y NEXT_PIECE_POS_Y tendrn la posicin en pantalladonde deber dibujarse la pieza que utilizaremos luego de estampar la actual.

    #define FALL_INTERVAL 1000.0f#define MOVE_INTERVAL 80.0f

    Por ltimo, vemos FALL_INTERVAL y MOVE_INTERVAL, que sern los intervalos detiempo para el descenso y el movimiento de la pieza por parte del usuario, respec-tivamente.

    Veamos ahora las propiedades:

    Piece _piece;

    TETRIS

    40

    Modificando la constante FALL_INTERVAL, podemos acelerar o desacelerar el intervalo de des-

    censo de la pieza. Adems, si cambiamos la constante MOVE_INTERVAL, modificaremos la velo-

    cidad de reaccin de la pieza respecto de las teclas.

    CONSTANTES

  • El objeto _piece alojar la pieza que cae actualmente y que manipular el jugador.

    Piece _nextPiece;

    La instancia _nextPiece mostrar (como su nombre lo indica) la pieza que aparece-r luego de estampar la actual.

    BoardMap _boardMap;

    El objeto _boardMap identificar el mapa actual.

    float _fallInterval;

    La variable _fallInterval determinar el intervalo de tiempo al que ir descen-diendo la pieza.

    int _score;

    Por ltimo, tenemos la variable _score, que contendr el puntaje adquirido has-ta el momento.

    Ahora que conocemos de qu se tratan las constantes y las propiedades de la clase,pasemos al cdigo fuente:

    #include InGame.h

    InGame::InGame() {}

    InGame::~InGame() {

    }

    bool InGame::Initialize(int level) {

    // Inicializamos el estado

    La clase principal InGame

    41

  • _state = GAME_STATE_NONE;

    if (!_piece.LoadIni(data/graphics/piece.spr))return false;

    if (!_nextPiece.LoadIni(data/graphics/piece.spr))return false;

    if (!_boardMap.LoadIni(data/graphics/piece.spr))return false;

    _boardMap.Reset();_piece.SetRandomType();

    _piece.SetStartPos(START_POS_X,START_POS_Y);_nextPiece.SetStartPos(NEXT_PIECE_POS_X,NEXT_PIECE_POS_Y);

    _boardMap.SetPos(START_POS_X,START_POS_Y);

    _nextPiece.SetRandomType();

    _score = 0;

    return true;

    }

    bool InGame::Shutdown() {return true;

    }

    void InGame::Update(float dt) {static float fallAccumTime = 0;static float moveAccumTime = 0;

    _fallInterval = FALL_INTERVAL;

    if (KeyDown(DIK_UP)) {_piece.RotateCCW();if (_boardMap.CollideWith(_piece))

    _piece.RotateCW();

    TETRIS

    42

  • }if (KeyPressed(DIK_DOWN)) {_fallInterval = MOVE_INTERVAL;

    }

    moveAccumTime += dt;

    if (moveAccumTime >= MOVE_INTERVAL) {moveAccumTime = MOVE_INTERVAL;

    if (KeyPressed(DIK_LEFT)) {moveAccumTime -= MOVE_INTERVAL;_piece.MoveLeft();if (_boardMap.CollideWith(_piece))

    _piece.MoveRight();}if (KeyPressed(DIK_RIGHT)) {

    moveAccumTime -= MOVE_INTERVAL;_piece.MoveRight();if (_boardMap.CollideWith(_piece))

    _piece.MoveLeft();}

    }

    fallAccumTime += dt;

    if (fallAccumTime >= _fallInterval) {fallAccumTime = 0;

    _piece.MoveDown();if (_boardMap.CollideWith(_piece)) {

    int lines=0;

    _piece.MoveUp();_boardMap.StampPiece(_piece);

    lines = _boardMap.CheckForLines();

    if (lines == 1) {

    La clase principal InGame

    43

  • _score += 100;} else if (lines == 2) {

    _score += 300;} else if (lines == 3) {

    _score += 600;} else if (lines == 4) {

    _score += 1000;}

    _piece.SetType(_nextPiece.GetType(), _piece.GetColorByType(_nextPiece.GetType()));

    if (_boardMap.CollideWith(_piece)) {_state = GAME_STATE_EXIT;

    }

    _nextPiece.SetRandomType();}

    }

    // Si presionamos escape, salimos al menuif (KeyDown(DIK_ESCAPE))

    _state = GAME_STATE_EXIT;

    _piece.Update(dt);_nextPiece.Update(dt);_boardMap.Update(dt);

    }

    void InGame::Draw() {wstringstream ss;

    _boardMap.Draw();_piece.Draw();_nextPiece.Draw();

    ss

  • Mucho cdigo? No tanto, pero mejor vayamos por partes para entenderlo, comen-zando por el mtodo Initialize:

    bool InGame::Initialize(int level) {

    // Inicializamos el estado_state = GAME_STATE_NONE;

    if (!_piece.LoadIni(data/graphics/piece.spr))return false;

    if (!_nextPiece.LoadIni(data/graphics/piece.spr))return false;

    if (!_boardMap.LoadIni(data/graphics/piece.spr))return false;

    Comenzamos por inicializar la propiedad _state en GAME_STATE_NONE para indicar albucle principal que estamos jugando. Luego intentamos cargar los sprites correspon-dientes a la pieza, la pieza siguiente y el mapa. Si algo saliera mal, retornamos false.

    _boardMap.Reset();

    Vaciamos el mapa de cualquier elemento que contenga.

    _piece.SetRandomType();

    Tomamos una pieza al azar por medio del mtodo SetRandomType.

    _piece.SetStartPos(START_POS_X,START_POS_Y);_boardMap.SetPos(START_POS_X,START_POS_Y);

    Inicializamos la posicin inicial de la pieza y del mapa segn las constantes START_POS_X y START_POS_Y para que, de esta forma, coincidan los valores lgicos dentrode los arreglos y lo que se ve en pantalla.

    La clase principal InGame

    45

  • _nextPiece.SetRandomType();_nextPiece.SetStartPos(NEXT_PIECE_POS_X,NEXT_PIECE_POS_Y);

    Tomamos para la pieza siguiente una al azar e inicializamos su posicin en la pan-talla segn las constantes NEXT_PIECE_POS_X y NEXT_PIECE_POS_Y.

    _score = 0;

    return true;

    }

    Por ltimo, inicializamos el puntaje a cero y retornamos true si todo sali bien.

    Analicemos, ahora, el mtodo Update:

    void InGame::Update(float dt) {static float fallAccumTime = 0;static float moveAccumTime = 0;

    Declaramos dos variables estticas que acumularn el tiempo transcurrido para de-terminar cundo deber moverse la pieza. Una ser fallAccumTime, de la que depen-der la cada de la pieza. La otra, llamada moveAccumTime, medir el tiempo parael movimiento dependiente del usuario.

    _fallInterval = FALL_INTERVAL;

    Por cada iteracin, inicializamos la propiedad _fallInterval segn la constanteFALL_INTERVAL, puesto que slo cambiar este valor si el usuario presionar la teclacorrespondiente a la cada rpida.

    if (KeyDown(DIK_UP)) {_piece.RotateCCW();

    Si el usuario puls la tecla determinada por la constante DIK_UP (flecha arriba),la pieza rotar en el contrasentido de las agujas del reloj por medio del mtodoRotateCCW.

    TETRIS

    46

  • if (_boardMap.CollideWith(_piece))_piece.RotateCW();

    }

    Luego de haber rotado, verificaremos si esta accin provoc un choque, por mediodel mtodo CollideWith del objeto _boardMap pasndole por parmetro una referen-cia a la pieza. De ser afirmativo, volvemos a la posicin original rotando en el sen-tido contrario por medio del mtodo RotateCW.

    if (KeyPressed(DIK_DOWN)) {_fallInterval = MOVE_INTERVAL;

    }

    Si el usuario presion la tecla definida por la constante DIK_DOWN (tecla abajo), re-duciremos el intervalo de cada dado por la propiedad _fallInterval al valor indica-do por la constante MOVE_INTERVAL. De esta forma, el usuario podr acelerar el des-censo de la pieza.

    moveAccumTime += dt;

    Acumulamos el tiempo transcurrido entre iteraciones en moveAccumTime.

    if (moveAccumTime >= MOVE_INTERVAL) {moveAccumTime = MOVE_INTERVAL;

    Luego si el valor del tiempo acumulado en moveAccumTime es mayor o igual quelo que indica MOVE_INTERVAL, forzamos el valor al indicado por dicha constante.

    La clase principal InGame

    47

    Es posible jugar al Tetris por siempre? Esta pregunta se encontr en una tesis de John Brzus-

    towski en 1988 y fue investigada ms recientemente por Walter Kosters. La conclusin obtenida

    fue que el jugador, inevitablemente, est condenado a perder. La razn se debe a que, si ste re-

    cibe una secuencia prolongada de piezas Z y S, ser forzado a dejar un hueco en una esquina.

    EL JUGADOR EST CONDENADO A PERDER

  • if (KeyPressed(DIK_LEFT)) {moveAccumTime -= MOVE_INTERVAL;_piece.MoveLeft();if (_boardMap.CollideWith(_piece))

    _piece.MoveRight();}if (KeyPressed(DIK_RIGHT)) {

    moveAccumTime -= MOVE_INTERVAL;_piece.MoveRight();if (_boardMap.CollideWith(_piece))

    _piece.MoveLeft();}

    }

    Luego, si el usuario presion las teclas de cursor derecha o izquierda, decrementa-mos el tiempo acumulado por la constante MOVE_INTERVAL, movemos segn la di-reccin indicada por medio de los mtodos MoveRight y MoveLeft, respectivamen-te. Por ltimo, chequeamos si el movimiento provoc alguna colisin con el mapa.En caso afirmativo, realizamos la accin contraria.

    fallAccumTime += dt;

    Acumulamos el tiempo transcurrido entre iteraciones en la variable fallAccumTime.

    if (fallAccumTime >= _fallInterval) {fallAccumTime = 0;

    _piece.MoveDown();

    TETRIS

    48

    Alexey Pajitnov, nacido en 1956 en Rusia, es un ingeniero en computacin que, con la ayuda de

    Dmitry Pavlovsky y Vadim Gerasimov, cre el Tetris en 1985. Adems, desarroll la secuela (no

    tan conocida) llamada Welltris, que tiene el mismo principio, pero en tres dimensiones.

    EL CREADOR DEL TETRIS

  • Verificamos si el tiempo acumulado en fallAccumTime supera el intervalo indicadopor la propiedad _fallInterval. De ser as, igualamos a cero el contador y permiti-mos que la pieza descienda invocando a su mtodo MoveDown.

    if (_boardMap.CollideWith(_piece)) {

    Si hubo una colisin, quiere decir que debemos estampar la pieza, dado que como des-cenda, significa que colision contra el borde inferior del mapa o contra otra pieza.

    int lines=0;

    Creamos e inicializamos la variable lines en cero. Esta variable alojar la cantidadde lneas que fueron completadas con esta accin.

    _piece.MoveUp();

    Dado que la pieza descendi y, por tanto, provoc una colisin, debemos moverlaa su ltima posicin vlida. Para esto, invocamos al mtodo MoveUp, que produceel resultado contrario.

    _boardMap.StampPiece(_piece);

    Ahora s, estampamos la pieza en la ubicacin correcta llamando al mtodo StampPiece del mapa pasndole la pieza por parmetro.

    lines = _boardMap.CheckForLines();

    Chequeamos si la accin complet alguna lnea.

    if (lines == 1) {_score += 100;

    } else if (lines == 2) {_score += 300;

    } else if (lines == 3) {_score += 600;

    La clase principal InGame

    49

  • } else if (lines == 4) {_score += 1000;

    }

    Verificamos la cantidad de lneas completadas e incrementamos el puntaje segnsea conveniente.

    _piece.SetType(_nextPiece.GetType(), _piece.GetColorByType(_nextPiece.GetType()));

    Debemos lanzar una nueva pieza segn indica _nextPiece. Para esto, invocamos almtodo SetType pasndole por parmetro el tipo y color de la siguiente pieza.

    if (_boardMap.CollideWith(_piece)) {_state = GAME_STATE_EXIT;

    }

    Verificamos si la nueva pieza (que ahora se encuentra en el extremo superior, dadoque el mtodo SetType vuelve a la pieza a su posicin inicial) provoca una colisincon el mapa. Esto significara que las piezas estampadas en el mapa han llegado alextremo superior y, por tanto, el juego ha finalizado.

    _nextPiece.SetRandomType();}

    }

    Inicializamos la nueva pieza por otra aleatoria utilizando el mtodo SetRandomType.

    // Si presionamos escape, salimos al menuif (KeyDown(DIK_ESCAPE))

    _state = GAME_STATE_EXIT;

    _piece.Update(dt);_nextPiece.Update(dt);_boardMap.Update(dt);

    }

    TETRIS

    50

  • Por ltimo verificamos si se presion la tecla Escape para salir del juego y actualiza-mos todos los objetos de la escena.

    Figura 23. Al correr el juego, debemos ver una pantalla como sta.

    Genial! Ya tenemos un juego estilo Tetris completamente funcional. En los prxi-mos captulos, avanzaremos en complejidad y aprenderemos nuevos conceptos.

    La clase principal InGame

    51

    RESUMEN

    En este captulo, hemos aprendido el concepto de mapa y cmo se relaciona lo que est almace-

    nado lgicamente en l y lo que se ve en pantalla. En los prximos captulos, avanzaremos un

    poco ms sobre este tema, analizando los mapas de mosaicos. Adems, veremos cmo agregar

    sonido y msica, textos con fuentes creados a partir de bitmaps y mucho ms!

  • 52

    1 Qu elementos contiene el arreglo que

    aloja las piezas?

    2 Por qu la matriz que aloja las piezas de-

    be ser cuadrada?

    3 Al chequear la colisin de la pieza con el

    mapa, por qu el bucle recorre la pieza y

    no el mapa?

    4 En qu consiste estampar una pieza en el

    mapa?

    5 En qu consiste eliminar una lnea en el

    mapa?

    ACTIVIDADES

    6 Modificar el cdigo para que, cada cierta

    cantidad de puntos, cambie de nivel modi-

    ficando en cada uno la velocidad de des-

    censo de las piezas.

    7 Modificar el cdigo para que permita dos

    jugadores con dos mapas.

    8 Modificar las constantes MAX_PIECES,

    MAX_PIECE_SIZE y pieceType de manera

    tal de crear un juego estilo Tetris, pero con

    piezas formadas por la combinacin de

    cinco cuadrados (Pentris).