Desarrollo de una miniconsola de videojuegos portátil (2): tetris 
En esta segunda entrega de esta miniserie sobre el desarrollo de la GabrielBoy, se abordará el diseño y desarrollo del primero de los juegos: un tetris. Se parte del diseño original del tetris, que consiste en una cuadrícula de 10x20 posiciones en la que van cayendo piezas que el jugador debe ir colocando buscando que llenen filas enteras. No me pararé en explicar el juego porque todos los conocemos. Así que vamos a ello.

Mecánica del juego

Se trata de un tetris estándar: van apareciendo las piezas por arriba de forma aleatoria, con la cruceta movemos a los lados o hacemos que la pieza baje más rápido y con el botón A rotamos la pieza. Cuando conseguimos hacer una o varias líneas horizontales completas dichas líneas de borran del tablero y aumenta la velocidad de caida en función de la cantidad de líneas eliminadas. El jugador nunca "gana", las fichas siguen cayendo indefinidamente hasta que apaguemos la consola, reiniciemos o la última ficha en caer ya no quepa en el tablero porque este está lleno.

Diseño de la pantalla

La única pantalla que tiene el juego está gestionada por la clase TetrisMainScreen (en la carpeta games/tetris). Dibuja un tablero central, que alberga 10 x 20 huecos de 3 x 3 pixels cada uno (cada “cuadrado” del tetris es un bloque de 3 x 3 pixels). Con estas dimensiones tenemos un tablero que ocupa 30 x 60 pixels y que se coloca en el centro de la pantalla. Los huecos de los lados son de 49 pixels a izquierda y de 49 pixels a la derecha (49 + 30 + 49 = 128 pixels de anchura de la pantalla LCD).



El hueco de la izquierda se utiliza para indicar la siguiente figura que va a caer mientras que el hueco de la derecha se utiliza para indicar el nivel por el que se va: cada 5 filas eliminadas se sube de nivel y aumenta un 5% la velocidad de caida de las figuras. El nivel máximo es el 9 y a partir de ese nivel ya no se aumenta la velocidad de caida.

Mecánica interna

El código no trabaja con el framebuffer de la pantalla, sino que trabaja con una matriz de 10 x 20 enteros en la que cada elemento puede tener los siguientes valores:

0: hueco libre.

1: hueco ocupado por suelo.

2: hueco ocupado por una pieza que está aún cayendo

    static const int32_t BOARD_WIDTH = 10;
    static const int32_t BOARD_HEIGHT = 20;
    static const int32_t BOARD_SIZE = BOARD_WIDTH * BOARD_HEIGHT;
    uint8_t board[BOARD_SIZE]            __attribute__ ((aligned(4)));

Se define el tablero con el atributo "aligned(4)" de GCC para garantizar que el compilador aloja dicha variable en una dirección de memoria múltiplo de 4 bytes (32 bits), de esta manera las operaciones de inicialización y rrecorrido del tablero puede optimizarse un poco más. Las figuras están definidas en un array constante (en ROM) de 7 elementos y cada elemento del array (cada figura) es una matriz de 4x4 bytes.
class TetrisFigure {
    public:
        static const int32_t MAX_WIDTH = 4;
        static const int32_t MAX_SIZE = MAX_WIDTH * MAX_WIDTH;
        int32_t width;
        int32_t height;
        uint8_t data[MAX_SIZE]  __attribute__ ((aligned(4)));
        TetrisFigure &operator = (const TetrisFigure &other);
        void rotateInto(TetrisFigure &other);
        void rotate();
};

...

const TetrisFigure TetrisMainScreen::FIGURES[7] = {
    {
        4,
        1,
        {
            1, 1, 1, 1,
            0, 0, 0, 0,
            0, 0, 0, 0,
            0, 0, 0, 0
        }
    },
    {
        3,
        2,
        {
            1, 1, 0, 0,
            0, 1, 1, 0,
            0, 0, 0, 0,
            0, 0, 0, 0
        }
    },
    {
        3,
        2,
        {
            0, 1, 1, 0,
            1, 1, 0, 0,
            0, 0, 0, 0,
            0, 0, 0, 0
        }
    },
    {
        3,
        2,
        {
            1, 1, 1, 0,
            0, 0, 1, 0,
            0, 0, 0, 0,
            0, 0, 0, 0
        }
    },
    {
        3,
        2,
        {
            1, 1, 1, 0,
            1, 0, 0, 0,
            0, 0, 0, 0,
            0, 0, 0, 0
        }
    },
    {
        3,
        2,
        {
            0, 1, 0, 0,
            1, 1, 1, 0,
            0, 0, 0, 0,
            0, 0, 0, 0
        }
    },
    {
        2,
        2,
        {
            1, 1, 0, 0,
            1, 1, 0, 0,
            0, 0, 0, 0,
            0, 0, 0, 0
        }
    }
};

La figura que cae es una copia en RAM de la figura correspondiente de ese array, pues puede ser necesario rotarla. La rotación, como siempre es en pasos de 90 grados, se realiza por la técnica de la transposición y a continuación aplicar función espejo vertical u horizontal, y así no hay que hacer cálculos trigonométricos.

Máquina de estados

La máquina de estados consta de 3 estados:

1. NEW_FIGURE (estado inicial): En este estado se coge la figura siguiente y se intenta colocar en la parte superior del tablero para que vaya cayendo:

1.1. Si se puede colocar, se pasa al estado FALLING y se calcula una nueva figura para que sea la p2róxima siguiente".

1.2. Si no se puede colocar porque ya toca con suelo o con figuras anteriores "consolidadas", se pasa al estado GAME_OVER.

2. FALLING: Este es el estado principal del juego, la figura actual va cayendo y en el momento que se detecta que toca contra suelo o con borde inferior del tablero, la figura se convierte en suelo (se "consolida"). Cuando se detecta que se ha "generado suelo nuevo" se recorre el tablero, se eliminan las filas llenas y se comprueba si se debe subir de nivel.

2.1. Si la figura que está cayendo toca suelo, se pasa al estado NEW_FIGURE.

3. GAME_OVER: Por ahora es un estado "muerto". El juego se cuelga intencionadamente y el jugador debe reiniciar la consola si quiere seguir jugando o empezar de nuevo.



Screen *TetrisMainScreen::onUpdate() {
    bool boardChanged = false;
    uint8_t b = this->buttons.getValue();
    LevelChanged levelChanged = LevelChanged::NO;
    if (this->status == St::NEW_FIGURE) {
        this->generateNewRandomFigure();
        this->nextFigureChanged = true;
        this->figureX = this->rnd.getNextValue() % (10 - this->figure.width);
        this->figureY = 0;
        if (this->getFigureValidAt(this->figure, this->figureX, this->figureY)) {
            this->status = St::FALLING;
            this->ticksBetweenMovs = this->initialTicksBetweenMovs;
            boardChanged = true;
        }
        else {
            this->status = St::GAME_OVER;    // game over is a dead state (console must be reseted)
        }
    }
    else if (this->status == St::FALLING) {
        this->ticksBetweenMovs--;
        if ((this->ticksBetweenMovs <= 0) || (b & Buttons::MASK_DOWN)) {
            if (this->getCanMoveFigureTo(Direction::DOWN)) {
                this->figureY++;
            }
            else {
                this->finalizeFigure(levelChanged);
                this->status = St::NEW_FIGURE;
            }
            if (this->ticksBetweenMovs <= 0)
                this->ticksBetweenMovs = this->initialTicksBetweenMovs;
            boardChanged = true;
        }
        else if ((b & Buttons::MASK_A) && this->getCanRotateFigure()) {
            this->figure.rotate();
            boardChanged = true;
        }
        else if ((b & Buttons::MASK_LEFT) && this->getCanMoveFigureTo(Direction::LEFT)) {
            this->figureX--;
            boardChanged = true;
        }
        else if ((b & Buttons::MASK_RIGHT) && this->getCanMoveFigureTo(Direction::RIGHT)) {
            this->figureX++;
            boardChanged = true;
        }
    }
    if (boardChanged) {
        this->updateBoardWithFigure();
        if (levelChanged == LevelChanged::YES)
            this->drawLevelLabel();
        this->drawBoardWithFigureOnFrameBuffer();
        if (this->nextFigureChanged) {
            this->drawNextFigureOnFrameBuffer();
            this->nextFigureChanged = false;
        }
        this->display.notifyFrameBufferChanged();
    }
    return nullptr;
}


Siguiente entrega

En la siguiente entrega se analizará el segundo de los juegos que incluye la consola. Un shooter 3D muy sencillo implementado con la técnica del raycasting



Todo el código y los diseños están en la sección soft.

[ añadir comentario ] ( 116 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente  |   ( 3.1 / 95 )

<Anterior | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Siguiente> >>