viernes, 20 de marzo de 2009

Nueva versión del Tetris

Hacemos un paréntesis en la programación del NSMB y vamos a presentar la última versión del Tetris.

Se ha creado un menú en la pantalla táctil, por lo que usaremos el stylus por primera vez en este blog. La imagen es la siguiente:



Posibilita la selección de tres tipos de juegos.

PALib controla cuando se pulsa en la pantalla táctil y devuelve las coordenadas (x, y) donde se ha pulsado.

if (Stylus.Held) {
    tipo_juego = obtener_tipo_juego(Stylus.X, Stylus.Y);

El Stylus.Held indica que se está pulsando en la pantalla y las coordenadas quedan en Stylus.X y en Stylus.Y. Ahora para saber si se ha pulsado un botón u otro se ha creado esta función:


u8 obtener_tipo_juego(s32 xStylus, s32 yStylus) {
if (pulsado_boton_con_stylus(xStylus, yStylus, 60, 37, 205, 79)) {
return JUEGO_MARATON;
} else if (pulsado_boton_con_stylus(xStylus, yStylus, 74, 83, 224, 125)) {
return JUEGO_EXPERTO;
} else if (pulsado_boton_con_stylus(xStylus, yStylus, 61, 133, 226, 170)) {
return JUEGO_SUPER_BLOQUES;
} else {
return 0;
}
}

bool pulsado_boton_con_stylus(s32 xStylus, s32 yStylus, s32 x1, s32 y1, s32 x2, s32 y2) {
return (xStylus > x1 && xStylus <> y1 && yStylus < y2);
}


Esta última función es la que uso en todos los proyectos. Sirve para saber si un punto (x, y) esta dentro de un rectángulo (x1, y1) - (x2, y2).
Los rectángulos los sacamos de la imagen, y aunque los botones estén un poco torcidos más o menos he trazado unos rectángulos y son esas coordenadas las que he usado:


El modo superbloques muestra fichas de distintas formas, posibilidad que nos da el haber creado las fichas como matrices de 4x4. Y tanto este modo como el experto van incrementando las velocidad a medida que se consigue aumentar la puntuación.

Veamos la función actualizar_puntuacion:


if (tipo_juego == JUEGO_EXPERTO || tipo_juego == JUEGO_SUPER_BLOQUES) {
if (nivel != puntos / 100) {
nivel = puntos / 100; velocidad -= nivel;
}
}

Cada cien puntos aumentamos la velocidad (o disminuimos el tiempo de espera).

Os dejo todo el código en el lugar habitual para que echéis un vistazo. Como siempre nos vemos en los comentarios.

Saludos

sábado, 14 de marzo de 2009

Clases básicas: TileSet

TileSet es una clase que nos permite trabajar con los tiles. Los tiles son esas pequeñas imágenes, de 8x8 pixeles, que sirven para dibujar en las pantallas de la nintendo DS.

Para explicarlo mejor, las pantalla de la DS tienen 256x192 pixeles. Dividido entre 8 nos da 32x24 casillas. Es decir, cada pantalla tienen 32x24 casillas, y en cada casilla se puede dibujar un tile.

PaLib nos permite trabajar con estos tiles de una forma muy sencilla, pero la clase TileSet nos permite aún una mayor abstracción. Sólo contiene, por ahora, tres funciones.

El constructor de la clase nos permite cargar en memoria la imagen con todos los tiles de nuestro juego.

La función SetTile16x16 nos permitirá dibujar en una ubicación dada un tile.

La función GetTile nos permite ver que tile hay en una ubicación dada. Esto nos servirá para el tema de colisiones.

Esta es la imagen que usamos, en este momento, en el proyecto:



Si os fijáis bien el primer rectángulo es rosa, eso significa que el 0 será un recuadro transparente.

Un ejemplo de uso:

#define PLATAFORMAS (void *)plataformas_Pal, (void *)plataformas_Tiles, SIZEOF_16BIT(plataformas_Tiles), (void *)plataformas_Map

#define COLUMNAS_16x16_PANTALLA 16
#define COLUMNAS_16x16_MAPA 48
#define FILAS_16x16 12
#define MAPA_PLATAFORMAS (void *)mapa, COLUMNAS_16x16_PANTALLA, FILAS_16x16, COLUMNAS_16x16_MAPA

int mapa[COLUMNAS_16x16_MAPA * FILAS_16x16] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 5, 2, 4, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
7, 12, 8, 11, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
7, 8, 13, 12, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0,
7, 12, 8, 11, 14, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
7, 11, 12, 13, 14, 0, 0, 0, 0, 0, 0, 7, 9, 8, 10, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
7, 12, 13, 8, 14, 0, 0, 0, 0, 0, 0, 7, 12, 10, 13, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
7, 13, 9, 10, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 3, 2, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
7, 12, 8, 12, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
7, 8, 12, 13, 15, 4, 3, 3, 4, 2, 3, 4, 2, 5, 4, 4, 3, 5, 2, 3, 5, 4, 3, 3, 4, 2, 3, 4, 2, 5, 4, 2, 3, 5, 2, 3, 5, 4, 3, 3, 4, 2, 3, 4, 2, 5, 4, 6,
7, 11, 13, 12, 8, 10, 9, 11, 12, 11, 8, 12, 12, 13, 12, 8, 10, 9, 11, 12, 11, 8, 12, 9, 11, 12, 11, 8, 12, 9, 13, 12, 8, 10, 9, 11, 8, 12, 12, 13, 12, 10, 12, 11, 8, 12, 9, 14
};

TileSet *newTileSet = new TileSet(SCREEN_TOP, BACKGROUND_TWO, PLATAFORMAS);
newTileSet->SetTile16x16(MAPA_PLATAFORMAS, 10, 10);

Es muy sencillo. Me gusta usar los defines, porque el código queda mucho más claro, desaparecen * y (void *) y queda todo más cómodo.

Saludos

viernes, 6 de marzo de 2009

Clases básicas - Sprite

En nuestro NSMB vamos a empezar a definir una serie de clases que se encargarán, cada una de ellas, de un apartado propio del juego.

NOTA:
Aquí vamos a explicar como se han construido estas clases y como funcionan. Si no te interesa o tu nivel de programación aún no ha llegado a esto, podrías saltarte la explicación y únicamente aprender a usarlas.



Incluso podemos diferenciar la lógica del juego del apartado gráfico (las propias librerías PALib) por lo que sería más fácil pasar el juego a otro sistema. Vamos a presentar en este post la clase Sprite (vimos una primera versión en el post anterior).

Esta clase será la encargada de cargar y mostrar los personajes o los objetos con los que interactuaremos en nuestros juegos. Podremos animar los Sprites, rotarlos, moverlos, etc.

En C++ las clases se componen de dos ficheros, el .h y el .cpp.
En el .h se almacena la definición de la clase, su nombre, las funciones públicas, las privadas y las variables miembro.
En el .cpp está el código, la implementación de la propia clase.

Sprite.h
#ifndef SPRITE_H
#define SPRITE_H

class Sprite {

private:
    static unsigned int index_sprites;
    static unsigned int index_sprites_pals;

protected:
    unsigned int m_index_sprite;
    unsigned int m_index_pal;
    unsigned int m_screen;
    unsigned int m_background;
    unsigned int m_size;

    unsigned int m_firstframe;
    unsigned int m_lastframe;
    unsigned int m_speed;

    int m_x;
    int m_y;

    Sprite(unsigned int screen, unsigned int background, unsigned int size, int x, int y);

public:
    Sprite(unsigned int screen, unsigned int background, unsigned int shape, unsigned int size, void *palette, void *sprite, int x, int y, int hflip = 0);
    ~Sprite();

    unsigned int GetIndexSprite();
    unsigned int GetScreen();

    Sprite *Clone();
    Sprite *Clone(int x, int y);

    void SetAnim(unsigned int firstframe, unsigned int lastframe, unsigned int speed);
    void StartAnim(unsigned int firstframe, unsigned int lastframe, unsigned int speed);
    void StartAnim();
    void StopAnim();
    void RestartAnim();
    void PauseAnim();

    void Hflip(bool hflip);

    void SetX(int x);
    void SetY(int y);
    void SetXY(int x, int y);

    int GetX();
    int GetY();

    int MoveRight(int offset);
    int MoveLeft(int offset);
    int MoveUp(int offset);
    int MoveDown(int offset);
};

#endif


Las variables miembro son las que definen el estado de un objeto. En nuestro caso son las variables que tienen como prefijo una m_ :
m_index_sprite, m_index_pal, m_screen, m_background, m_firstframe, m_lastframe, m_speed, m_x, m_y.

Las variables miembro son las que almacenan el estado, es decir son las que diferencian un objeto de otro. Veamos cada una que hace:

m_index_sprite, m_index_pal quedan ocultas a la programación. Son valores necesarios para PALib. Si os fijaís existen unas variables (index_sprites, index_sprites_pals) que son estáticas, es decir son únicas para todos los objetos, y son las encargadas de crear los índices para cada Sprite y cada Paleta). (En el primer tutorial son los valores HEROE_PALLETE y HEROE_SPRITE, gracias a la POO nos olvidamos de ellas)

Como siempre es más difícil explicarlo que verlo o hacerlo.

El resto de variables miembro nos indican a que pantalla pertenece el Sprite y dentro de cada pantalla a que fondo, m_screen y m_background.

m_firstFrame y m_lastFrame, sirven para realizar la animación. m_speed nos indica la velocidad de la misma.

m_x y m_y almacenan las coordenadas del sprite.

El resto del fichero .h son las declaraciones de las funciones. Tenemos un constructor, con el último campo puesto = 0. Esto significa que se puede no usar y por defecto es cero. Es decir al crear el sprite lo podemos rotar horizontalmente si ponemos un 1 en ese campo, si no queremos rotarlo,no ponemos nada.

El resto son una serie de funciones que su nombre ya las explica.

Ahora veamos como usarlo:


#define COIN OBJ_SIZE_16X16, (void*)coin_Pal, (void*)coin_Sprite




Sprite coin_i(BACKGROUND_THREE, COIN, 96, 32);
coin_i.StartAnim(0, 3, 5);
Sprite coin_ii = coin_i.Clone(112, 16);
Sprite coin_iii = coin_ii.Clone();
coin_iii.MoveRight(16);
Sprite coin_iv = coin_i.Clone(144, 32);


No creo que necesite más explicación, si no, nos vemos en los comentarios.


En cuanto a la librería PALib, en esta clase utilizo por primera vez la función PA_SpriteAnimPause. Esta función permite parar una animación y continuarla posteriormente. Utiliza como parámetros el fondo donde está el sprite, así como su índice (para saber cual parar) y el último parámetro es un 1 para parar la animación y 0 para reanudarla. Ejemplo de uso:

void Sprite::RestartAnim() {
    PA_SpriteAnimPause(m_screen, m_index_sprite, 0);
}

void Sprite::PauseAnim() {
    PA_SpriteAnimPause(m_screen, m_index_sprite, 1);
}


De esta forma al pulsar poco a poco el teclado, la animación se hace continua y no parece que esté andando a la pata coja.


Otra función que se utiliza por primera vez en este tutorial es la de Clonar un Sprite, PA_CloneSprite. Como parámetros se le pasa la pantalla de la DS donde está cargado el Sprite, el índice de este para saber cual clonar y, por último, el índice del nuevo Sprite. Clonar Sprites nos ahorra espacio en memoria, ya que comparten toda la información gráfica. En nuestro caso va a ser muy util para las monedas que aparecen a lo largo de todo el juego.

Saludos

PD: Voy a dejar todo el proyecto, tal y como está ahora, en la siguiente dirección: http://sites.google.com/site/devnintendods/Home/nsmb. Lo que dejo ahí es todo lo que tengo hecho hasta ahora (SuperMarioDS_0.2). Es todo el montaje que estoy haciendo. Lo iré explicando en los siguientes Post. Sólo decir que main.cpp únicamente llama a NSMB.cpp que es el que lo hace todo.

Aún así, se pueden usar las clases de forma independiente en cualquier otro proyecto.

Enlaces patrocinados

viernes, 27 de febrero de 2009

New Super Mario Bros

¡Cómo programar un juego tipo New Super Mario Bros (NSMB)!

Pues no es muy difícil, pero tampoco es fácil. Ante la cantidad de elementos que intervienen en el juego yo me inclinaría más por programar con orientación a objetos. Por lo que se necesita un poco más de nivel de programación del que hemos empleado hasta ahora.

Pero no nos desanimemos, utilizaremos la orientación a objetos de una forma sencilla y didáctica y, seguramente, tomaremos decisiones para que resulte lo más sencillo posible.

Otra cosa, en algún comentario se apunta a la posibilidad de hacer pantallas desde donde se puedan elegir opciones, y de esta forma, dejar un acabado más elegante a los juegos. El problema para mi estriba en que todo lleva tiempo y el poco de que dispongo me gusta más emplearlo en hacer el motor propio del juego. Aún así, intentaremos hacer cosas en ese sentido.

Empecemos con con la Orientación a objetos.

A la hora de ir a compilar un fichero .cpp (c++) PALib puede que de algún que otro error de compilación. En mi caso esta fue la pantalla del error:



La solución fue poner lo siguiente en el fichero PA_Sound.h:

línea 212: FS_wav[PA_Channel] = (u32*)PA_Malloc(FS_wav[PA_Channel], PA_FSFile[PAFS_wav_number].Length+4);

línea 430: FS_mod = (u32*)PA_Malloc(FS_mod, PA_FSFile[PAFS_mod_number].Length+4);

Antes de hacer este cambio mirar si os da error al compilar c++.

Y ahora ¿Qué es la orientación a objetos?

Pues es una forma de describir el mundo que estamos programando. Por ejemplo, en el primer tutorial de este blog presentamos un juego muy sencillo, un sprite moviéndose por un pueblecito. En este caso sólo era necesario las coordenadas de nuestro héroe y el mapa para las colisiones. Todo esto era lo único necesario para describir nuestro mundo.
En el Tetris las cosas se complicaban un poco más, teníamos un tablero y unas fichas que giraban. Para describir ese mundo tendríamos dos clases, la del tablero y la de ficha.

Ahora tenemos el NSMB, donde todo es mucho más complejo, no sólo en cantidad, si no en la forma de interactuar entre los distintos elementos del juego, los enemigos, las plataformas, las monedas, incluso nuestro héroe, Mario, puede cambiar de tamaño, salta, etc.

La orientación a objetos permite describir cada parte de nuestro mundo en un fichero aparte, teniendo todo muy bien organizado. Nos permitirá tener personajes con distinto comportamiento moviéndose por el juego. Incluso las propias plataformas serán una clase, desde donde se describirá su aspecto, así como su comportamiento.

Algunas de las clases que presentaremos podrían pasar perfectamente a un proyecto que se denominase PALib++, pero eso ya es harina de otro costal.

Podéis acceder a un primer proyecto para probar el compilador de c++. Esto el lo que se verá si ponéis en marcha el proyecto:



Este código contiene una clase llamada Sprite (en sprite.h está la definición de la clase y en Sprite.cpp el código) que es la responsable de cargar un sprite en la pantalla. Es mejorable, repite paletas y se puede simplificar, aún más, su utilización, pero lo dejaremos para el siguiente post.

Una de las mejoras que nos da el uso de clases es el poder simplificar el algoritmo propio del juego. Main.cpp queda más sencillo, y eso mejora la legibilidad del código. Por ejemplo, si quiero añadir un nuevo Sprite, únicamente serían dos líneas de código.

//Con clases
Sprite koopa(SCREEN_TOP, BACKGROUND_THREE, OBJ_SIZE_32X32, (void*)koopa_Pal, (void*)koopa_Sprite, 128, 140);
koopa.rotar_horizontal();
koopa.animar(0, 15, 15);

//Sin clases
PA_LoadSpritePal(SCREEN_TOP, KOOPA_PALLETE,(void*)koopa_Pal);
PA_CreateSprite(SCREEN_TOP, KOOPA_SPRITE, (void*)koopa_Sprite, OBJ_SIZE_32X32, COLOR_MODE_256, KOOPA_PALLETE, 128, 140);
PA_SetSpritePrio(SCREEN_TOP, KOOPA_SPRITE, BACKGROUND_THREE);
PA_SetSpriteHflip(SCREEN_TOP, KOOPA_SPRITE, 1);
PA_StartSpriteAnim(SCREEN_TOP, KOOPA_SPRITE, 0, 15, 15);


En las siguientes entregas iremos mejorando la clase Sprite. Estoy pensando en hacerla en Inglés para que se pueda distinguir bien del algoritmo del juego.

Saludos

viernes, 20 de febrero de 2009

La ficha siguiente

Para poder mostrar la siguiente ficha que va a salir tendremos que tener dos variables 'ficha' y 'ficha_siguiente'.

Modificaremos la función 'obtener_ficha' para que en vez de asignar a la variable 'ficha' la ficha buscada de forma aleatoria, se le asigne a la variable 'ficha_siguiente'. La función 'obtener_ficha' quedará de la siguiente manera:

u8 obtener_ficha() {
    u8 tipo = PA_RandMax(6); //0..6
    u8 color = PA_RandMinMax(2, 7); //2..7
    ficha.y = 0;
    ficha.x = TABLERO_ANCHO / 2;
    copiar_ficha_siguiente();
    if (tipo == 0) crear_ficha_t(color);
    else if (tipo == 1) crear_ficha_l(color);
    else if (tipo == 2) crear_ficha_o(color);
    else if (tipo == 3) crear_ficha_z(color);
    else if (tipo == 4) crear_ficha_s(color);
    else if (tipo == 5) crear_ficha_j(color);
    else crear_ficha_i(color);
    dibujar_ficha_siguiente();
    return existe_colision(ficha.x, ficha.y);
}

void copiar_ficha_siguiente() {
    u8 i, j;
    ficha.radio = ficha_siguiente.radio;
    for(i = 0; i < FICHA_ANCHO; i++) 
        for(j = 0; j < FICHA_ALTO; j++)
            ficha.ficha[i][j] = ficha_siguiente.ficha[i][j];
}


Con esto ya dejamos más o menos el juego terminado. En está página está disponible el código completo del proyecto.

Hay un tema en cuanto a la forma de programar.

En los dos juegos que hemos programado hasta la fecha, hemos utilizado una serie de variables globales y funciones que trabajan sobre ellas, siendo esto válido para juegos sencillos pero no tanto para juegos más complicados.

Conceptos como el ámbito de las variables han de ser considerados a la hora de realizar este tipo de programas. Por ejemplo, la función obtener_ficha, así como las de rotar deberían de trabajar sobre un parámetro ficha, y no sobre las variables globales. Esto dejaría el código más limpio y daría mayor flexibilidad si se quisieran introducir más funcionalidades.

Por ahora lo dejaremos así, pero si queremos hacer el New Super Mario Bros, deberemos empezar a hacer las cosas más limpias.

Saludos

viernes, 13 de febrero de 2009

Volcar las fichas al tablero

En este post vamos a dar forma completamente al motor del juego. Lo que nos queda es pasar las fichas al tablero cuando estas colisiones. Una vez hecho esto mirar si se ha hecho una línea y posteriormente quitarla.

Así que manos a la obra.

Para pasar las fichas al tablero necesitaremos esta función:
void copiar_ficha_a_tablero() {
    u8 i, j;

    for(i = 0; i < FICHA_ANCHO; i++) 
        for(j = 0; j < FICHA_ALTO; j++)
            if (ficha.ficha[i][j] != CASILLA_VACIA)
                tablero[ficha.x + i][ficha.y + j] = ficha.ficha[i][j];
}


Para saber si se ha hecho una o más líneas:
u8 es_linea_completa(u8 fila) {
    u8 i;

    for(i = 0; i < TABLERO_ANCHO; i++) 
        if (tablero[i][fila] == CASILLA_VACIA) return 0;
    return 1;
}


Para quitar esa línea que se hallado:
void quitar_linea(u8 fila) {
    u8 i, j;

    for(i = 0; i < TABLERO_ANCHO; i++) 
        for(j = fila; j > 0; j--)
            tablero[i][j] = tablero[i][j - 1];
}


Por último, actualizaremos la puntuación, dependiendo del número de lineas que se hagan de una sola vez:

void actualizar_puntuacion(u8 lineas) {
    if (lineas == 1) puntos += 10;
    else if (lineas == 2) puntos += 30;
    else if (lineas == 3) puntos += 60;
    else if (lineas == 4) puntos += 100;

    PA_OutputText(SCREEN_TOP, 25, 12, "%d   ", puntos);
}


El último código lo podeis descargar, junto a todo el proyecto, desde esta página.

En esta versión he puesto una pantalla de entrada, para ir decorando el juego. Se nota que no soy diseñador gráfico!!!

Saludos

viernes, 6 de febrero de 2009

Controlar las fichas

El siguiente paso es crear las fichas y que empiecen a caer por el tablero. Las funciones ya las presentamos en los primeros post de este tutorial así que aquí se presentan todas las de las fichas.

Hemos introducido el control el teclado, donde se puede ver la diferencia entre pulsar una tecla y tenerla pulsada.

Usaremos Pad.Newpress.Right/Left para controlar que pulsen, y suelten, la tecla de ir a la derecha. Esto es para controlar el movimiento horizontal de la ficha de un modo más preciso. Sin embargo, la tecla de bajar se quiere que sea algo rápido, por lo que usaremos Pad.Held.Down (estar pulsado).

Por último, merece la pena ver la función de existe_colision:

01.u8 existe_colision(int x, int y) {
02.    u8 i, j;

03.    for(i = 0; i < FICHA_ANCHO; i++) 
04.        for(j = 0; j < FICHA_ALTO; j++)
05.            if (ficha.ficha[i][j] != CASILLA_VACIA) {
06.                if (x + i < 0) return 1;
07.                if (x + i >= TABLERO_ANCHO) return 1;
08.                if (y + j >= TABLERO_ALTO) return 1;
09.                if (tablero[x + i][y + j] != CASILLA_VACIA) return 1;
10.            }
11.    return 0; //false
12}


Se utiliza el tipo 'int' porque x puede ser negativo.
En la línea 5 se puede ver que únicamente se mirará si alguna casilla de la ficha produce colisión si no está vacía.
Las líneas 6, 7 y 8 miran los bordes del tablero y por fin la línea 9 es la que mira que si en el tablero hay ocupada una casilla se produzca colición.

Otro tema es el de la variable ficha_timer y la constante VELOCIDAD_MINIMA. Se utilizan para que las fichas caigan a una velocidad controlada.

El código:

#include <PA9.h>

#include "gfx/all_gfx.h"
#include "gfx/all_gfx.c"

#include "devnintendods.h"

#define TABLERO_ANCHO 10
#define TABLERO_ALTO 21

#define FICHA_ANCHO 4
#define FICHA_ALTO 4

#define CASILLA_VACIA 1
#define CASILLA_ROJA 2
#define CASILLA_AZUL 3
#define CASILLA_VERDE 4
#define VELOCIDAD_MINIMA 25
#define BORDE_IZQUIERDO 1

//La fila 0 es la de más arriba y la 16 la de la base. La columna 0 es la de la izquierda.
u8 tablero[TABLERO_ANCHO][TABLERO_ALTO]; 

typedef struct {
    int x, y;
    u8 radio;
    u8 ficha[FICHA_ANCHO][FICHA_ALTO];
} tipo_ficha;

tipo_ficha ficha;

u8 ficha_timer = 0;

void inicializar_tablero();
void dibujar_tablero();
void dibujar_casilla(int x, int y, u8 ficha);

void dibujar_ficha();

u8 existe_colision(int x, int y);
u8 obtener_ficha();

void crear_ficha_t(u8 color);
void crear_ficha_l(u8 color);
void crear_ficha_o(u8 color);
void crear_ficha_z(u8 color);
void crear_ficha_i(u8 color);
void crear_ficha_s(u8 color);
void crear_ficha_j(u8 color);
void girar_ficha_derecha();
void girar_ficha_izquierda();

int main(int argc, char ** argv) {
    PA_Init();
    PA_InitVBL();
    PA_InitText(SCREEN_TOP, BACKGROUND_TWO);

    PA_DualLoadBgPal(BACKGROUND_THREE, (void *)tiles_Pal);
    PA_DualLoadSimpleBg(BACKGROUND_THREE, tiles_Tiles, Blank, BG_256X256, 0, 1);
    PA_EasyBgScrollXY(SCREEN_BOTTOM, BACKGROUND_THREE, 0, 0);

    PA_EasyBgLoad(SCREEN_TOP, BACKGROUND_ONE, fondo_top);
    PA_EasyBgLoad(SCREEN_BOTTOM, BACKGROUND_ONE, fondo_bottom);

    inicializar_tablero();
    dibujar_tablero();
    obtener_ficha();

    while  (1) {
        if (Pad.Newpress.Left) {
            if (!existe_colision(ficha.x - 1, ficha.y)) ficha.x--;
        } else if (Pad.Newpress.Right) {
            if (!existe_colision(ficha.x + 1, ficha.y)) ficha.x++;
        } else if (Pad.Held.Down) {
            if (!existe_colision(ficha.x, ficha.y + 1)) ficha.y++;
        } else if (Pad.Newpress.Y) {//girar izquierda
            girar_ficha_izquierda();
            if (existe_colision(ficha.x, ficha.y)) girar_ficha_derecha();
        } else if (Pad.Newpress.A) {//girar derecha
            girar_ficha_derecha();
            if (existe_colision(ficha.x, ficha.y)) girar_ficha_izquierda();
        } else {
            ficha_timer++;
            if (ficha_timer > VELOCIDAD_MINIMA) {
                ficha_timer = 0;
                if (!existe_colision(ficha.x, ficha.y + 1)) {
                    ficha.y++;    
                } else {
                    obtener_ficha();
                }
            }
        }
        dibujar_tablero();
        dibujar_ficha();
        PA_WaitForVBL();
    }
}

void inicializar_tablero() {
    u8 i, j;

    for(i = 0; i < TABLERO_ANCHO; i++) 
        for(j = 0; j < TABLERO_ALTO; j++) 
            tablero[i][j] = CASILLA_VACIA;
}

void dibujar_tablero() {
    u8 i, j;

    for(i = 0; i < TABLERO_ANCHO; i++) 
        for(j = 0; j < TABLERO_ALTO; j++)
            dibujar_casilla(i, j, tablero[i][j]);
}

void dibujar_casilla(int x, int y, u8 ficha) {
    u8 x1 = x * 2;
    u8 y1 = y;
    u8 screen = SCREEN_TOP;
    if (y1 >= 12) {
        screen = SCREEN_BOTTOM;
        y1 = y1 - 12;
    }
    y1 = y1 * 2;
    x1 += BORDE_IZQUIERDO;
    PA_SetMapTileEx(screen, BACKGROUND_THREE, x1, y1, ficha, 0, 0, 0);
    PA_SetMapTileEx(screen, BACKGROUND_THREE, x1, y1 + 1, ficha, 0, 1, 0);
    PA_SetMapTileEx(screen, BACKGROUND_THREE, x1 + 1, y1, ficha, 1, 0, 0);
    PA_SetMapTileEx(screen, BACKGROUND_THREE, x1 + 1, y1 + 1, ficha, 1, 1, 0);
}

void dibujar_ficha() {
    u8 i, j;

    for(i = 0; i < FICHA_ANCHO; i++) 
        for(j = 0; j < FICHA_ALTO; j++)
            if (ficha.ficha[i][j] != CASILLA_VACIA) dibujar_casilla(ficha.x + i, ficha.y + j, ficha.ficha[i][j]);
}

u8 existe_colision(int x, int y) {
    u8 i, j;

    for(i = 0; i < FICHA_ANCHO; i++) 
        for(j = 0; j < FICHA_ALTO; j++)
            if (ficha.ficha[i][j] != CASILLA_VACIA) {
                if (x + i < 0) return 1;
                if (x + i >= TABLERO_ANCHO) return 1;
                if (y + j >= TABLERO_ALTO) return 1;
                if (tablero[x + i][y + j] != CASILLA_VACIA) return 1;
            }
    return 0; //false
}

u8 obtener_ficha() {
    u8 tipo = PA_RandMax(6); //0..6
    u8 color = PA_RandMinMax(2, 7); //2..7
    ficha.y = 0;
    ficha.x = TABLERO_ANCHO / 2;
    if (tipo == 0) crear_ficha_t(color);
    else if (tipo == 1) crear_ficha_l(color);
    else if (tipo == 2) crear_ficha_o(color);
    else if (tipo == 3) crear_ficha_z(color);
    else if (tipo == 4) crear_ficha_s(color);
    else if (tipo == 5) crear_ficha_j(color);
    else crear_ficha_i(color);
    return existe_colision(ficha.x, ficha.y);
}

void crear_ficha_t(u8 color) {
    ficha.radio = 3;
    ficha.ficha[0][0] = color;
    ficha.ficha[1][0] = color;
    ficha.ficha[2][0] = color;
    ficha.ficha[3][0] = CASILLA_VACIA;
    ficha.ficha[0][1] = CASILLA_VACIA;
    ficha.ficha[1][1] = color;
    ficha.ficha[2][1] = CASILLA_VACIA;
    ficha.ficha[3][1] = CASILLA_VACIA;
    ficha.ficha[0][2] = CASILLA_VACIA;
    ficha.ficha[1][2] = CASILLA_VACIA;
    ficha.ficha[2][2] = CASILLA_VACIA;
    ficha.ficha[3][2] = CASILLA_VACIA;
    ficha.ficha[0][3] = CASILLA_VACIA;
    ficha.ficha[1][3] = CASILLA_VACIA;
    ficha.ficha[2][3] = CASILLA_VACIA;
    ficha.ficha[3][3] = CASILLA_VACIA;
}

void crear_ficha_l(u8 color) {
    ficha.radio = 3;
    ficha.ficha[0][0] = color;
    ficha.ficha[1][0] = CASILLA_VACIA;
    ficha.ficha[2][0] = CASILLA_VACIA;
    ficha.ficha[3][0] = CASILLA_VACIA;
    ficha.ficha[0][1] = color;
    ficha.ficha[1][1] = CASILLA_VACIA;
    ficha.ficha[2][1] = CASILLA_VACIA;
    ficha.ficha[3][1] = CASILLA_VACIA;
    ficha.ficha[0][2] = color;
    ficha.ficha[1][2] = color;
    ficha.ficha[2][2] = CASILLA_VACIA;
    ficha.ficha[3][2] = CASILLA_VACIA;
    ficha.ficha[0][3] = CASILLA_VACIA;
    ficha.ficha[1][3] = CASILLA_VACIA;
    ficha.ficha[2][3] = CASILLA_VACIA;
    ficha.ficha[3][3] = CASILLA_VACIA;
}

void crear_ficha_j(u8 color) {
    ficha.radio = 3;
    ficha.ficha[0][0] = CASILLA_VACIA;
    ficha.ficha[1][0] = color;
    ficha.ficha[2][0] = CASILLA_VACIA;
    ficha.ficha[3][0] = CASILLA_VACIA;
    ficha.ficha[0][1] = CASILLA_VACIA;
    ficha.ficha[1][1] = color;
    ficha.ficha[2][1] = CASILLA_VACIA;
    ficha.ficha[3][1] = CASILLA_VACIA;
    ficha.ficha[0][2] = color;
    ficha.ficha[1][2] = color;
    ficha.ficha[2][2] = CASILLA_VACIA;
    ficha.ficha[3][2] = CASILLA_VACIA;
    ficha.ficha[0][3] = CASILLA_VACIA;
    ficha.ficha[1][3] = CASILLA_VACIA;
    ficha.ficha[2][3] = CASILLA_VACIA;
    ficha.ficha[3][3] = CASILLA_VACIA;
}

void crear_ficha_o(u8 color) {
    ficha.radio = 2;
    ficha.ficha[0][0] = color;
    ficha.ficha[1][0] = color;
    ficha.ficha[2][0] = CASILLA_VACIA;
    ficha.ficha[3][0] = CASILLA_VACIA;
    ficha.ficha[0][1] = color;
    ficha.ficha[1][1] = color;
    ficha.ficha[2][1] = CASILLA_VACIA;
    ficha.ficha[3][1] = CASILLA_VACIA;
    ficha.ficha[0][2] = CASILLA_VACIA;
    ficha.ficha[1][2] = CASILLA_VACIA;
    ficha.ficha[2][2] = CASILLA_VACIA;
    ficha.ficha[3][2] = CASILLA_VACIA;
    ficha.ficha[0][3] = CASILLA_VACIA;
    ficha.ficha[1][3] = CASILLA_VACIA;
    ficha.ficha[2][3] = CASILLA_VACIA;
    ficha.ficha[3][3] = CASILLA_VACIA;
}

void crear_ficha_z(u8 color) {
    ficha.radio = 3;
    ficha.ficha[0][0] = color;
    ficha.ficha[1][0] = color;
    ficha.ficha[2][0] = CASILLA_VACIA;
    ficha.ficha[3][0] = CASILLA_VACIA;
    ficha.ficha[0][1] = CASILLA_VACIA;
    ficha.ficha[1][1] = color;
    ficha.ficha[2][1] = color;
    ficha.ficha[3][1] = CASILLA_VACIA;
    ficha.ficha[0][2] = CASILLA_VACIA;
    ficha.ficha[1][2] = CASILLA_VACIA;
    ficha.ficha[2][2] = CASILLA_VACIA;
    ficha.ficha[3][2] = CASILLA_VACIA;
    ficha.ficha[0][3] = CASILLA_VACIA;
    ficha.ficha[1][3] = CASILLA_VACIA;
    ficha.ficha[2][3] = CASILLA_VACIA;
    ficha.ficha[3][3] = CASILLA_VACIA;
}

void crear_ficha_s(u8 color) {
    ficha.radio = 3;
    ficha.ficha[0][0] = CASILLA_VACIA;
    ficha.ficha[1][0] = color;
    ficha.ficha[2][0] = color;
    ficha.ficha[3][0] = CASILLA_VACIA;
    ficha.ficha[0][1] = color;
    ficha.ficha[1][1] = color;
    ficha.ficha[2][1] = CASILLA_VACIA;
    ficha.ficha[3][1] = CASILLA_VACIA;
    ficha.ficha[0][2] = CASILLA_VACIA;
    ficha.ficha[1][2] = CASILLA_VACIA;
    ficha.ficha[2][2] = CASILLA_VACIA;
    ficha.ficha[3][2] = CASILLA_VACIA;
    ficha.ficha[0][3] = CASILLA_VACIA;
    ficha.ficha[1][3] = CASILLA_VACIA;
    ficha.ficha[2][3] = CASILLA_VACIA;
    ficha.ficha[3][3] = CASILLA_VACIA;
}

void crear_ficha_i(u8 color) {
    ficha.radio = 4;
    ficha.ficha[0][0] = color;
    ficha.ficha[1][0] = CASILLA_VACIA;
    ficha.ficha[2][0] = CASILLA_VACIA;
    ficha.ficha[3][0] = CASILLA_VACIA;
    ficha.ficha[0][1] = color;
    ficha.ficha[1][1] = CASILLA_VACIA;
    ficha.ficha[2][1] = CASILLA_VACIA;
    ficha.ficha[3][1] = CASILLA_VACIA;
    ficha.ficha[0][2] = color;
    ficha.ficha[1][2] = CASILLA_VACIA;
    ficha.ficha[2][2] = CASILLA_VACIA;
    ficha.ficha[3][2] = CASILLA_VACIA;
    ficha.ficha[0][3] = color;
    ficha.ficha[1][3] = CASILLA_VACIA;
    ficha.ficha[2][3] = CASILLA_VACIA;
    ficha.ficha[3][3] = CASILLA_VACIA;
}

void girar_ficha_derecha() {
    u8 nueva_ficha[FICHA_ANCHO][FICHA_ALTO];
    u8 i, j;

    for(i = 0; i < FICHA_ANCHO; i++) 
        for(j = 0; j < FICHA_ALTO; j++)
            nueva_ficha[i][j] = ficha.ficha[i][j];
    if (ficha.radio == 3) {
        ficha.ficha[0][0] = nueva_ficha[0][2];
        ficha.ficha[1][0] = nueva_ficha[0][1];
        ficha.ficha[2][0] = nueva_ficha[0][0];
        ficha.ficha[0][1] = nueva_ficha[1][2];
        //ficha.ficha[1][1] = nueva_ficha[1][1];
        ficha.ficha[2][1] = nueva_ficha[1][0];
        ficha.ficha[0][2] = nueva_ficha[2][2];
        ficha.ficha[1][2] = nueva_ficha[2][1];
        ficha.ficha[2][2] = nueva_ficha[2][0];
    } else if (ficha.radio == 4) {
        ficha.ficha[0][0] = nueva_ficha[0][3];
        ficha.ficha[1][0] = nueva_ficha[0][2];
        ficha.ficha[2][0] = nueva_ficha[0][1];
        ficha.ficha[3][0] = nueva_ficha[0][0];
        ficha.ficha[0][1] = nueva_ficha[1][3];
        ficha.ficha[1][1] = nueva_ficha[1][2];
        ficha.ficha[2][1] = nueva_ficha[1][1];
        ficha.ficha[3][1] = nueva_ficha[1][0];
        ficha.ficha[0][2] = nueva_ficha[2][3];
        ficha.ficha[1][2] = nueva_ficha[2][2];
        ficha.ficha[2][2] = nueva_ficha[2][1];
        ficha.ficha[3][2] = nueva_ficha[2][0];
        ficha.ficha[0][3] = nueva_ficha[3][3];
        ficha.ficha[1][3] = nueva_ficha[3][2];
        ficha.ficha[2][3] = nueva_ficha[3][1];
        ficha.ficha[3][3] = nueva_ficha[3][0];
    }
}

void girar_ficha_izquierda() {
    u8 nueva_ficha[FICHA_ANCHO][FICHA_ALTO];
    u8 i, j;

    for(i = 0; i < FICHA_ANCHO; i++) 
        for(j = 0; j < FICHA_ALTO; j++)
            nueva_ficha[i][j] = ficha.ficha[i][j];
    if (ficha.radio == 3) {
        ficha.ficha[0][0] = nueva_ficha[2][0];
        ficha.ficha[1][0] = nueva_ficha[2][1];
        ficha.ficha[2][0] = nueva_ficha[2][2];
        ficha.ficha[0][1] = nueva_ficha[1][0];
        //ficha.ficha[1][1] = nueva_ficha[1][1];
        ficha.ficha[2][1] = nueva_ficha[1][2];
        ficha.ficha[0][2] = nueva_ficha[0][0];
        ficha.ficha[1][2] = nueva_ficha[0][1];
        ficha.ficha[2][2] = nueva_ficha[0][2];
    } else if (ficha.radio == 4) {
        ficha.ficha[0][0] = nueva_ficha[3][0];
        ficha.ficha[1][0] = nueva_ficha[3][1];
        ficha.ficha[2][0] = nueva_ficha[3][2];
        ficha.ficha[3][0] = nueva_ficha[3][3];
        ficha.ficha[0][1] = nueva_ficha[2][0];
        ficha.ficha[1][1] = nueva_ficha[2][1];
        ficha.ficha[2][1] = nueva_ficha[2][2];
        ficha.ficha[3][1] = nueva_ficha[2][3];
        ficha.ficha[0][2] = nueva_ficha[2][0];
        ficha.ficha[1][2] = nueva_ficha[1][1];
        ficha.ficha[2][2] = nueva_ficha[1][2];
        ficha.ficha[3][2] = nueva_ficha[1][3];
        ficha.ficha[0][3] = nueva_ficha[0][0];
        ficha.ficha[1][3] = nueva_ficha[0][1];
        ficha.ficha[2][3] = nueva_ficha[0][2];
        ficha.ficha[3][3] = nueva_ficha[0][3];
    }
}


En el próximo post hablaremos de como manejar el tablero.

Saludos

P.D.: Supongo que os estáis dando cuenta que el código se está haciendo muy grande. En el próximo post publicaremos un zip con todo el proyecto.

Enlaces patrocinados: