26. Avanzando Columnas: segunda parte de la lógica de juego.(*)
La entrega anterior ya permitía que cayeran las piezas, y que pudiéramos "rotarlas" mientras caían. Aun así, quedaban varias cosas para que el juego realmente fuera jugable, como por ejemplo:
- Las piezas "caían" siempre hasta el fondo de la pantalla, aunque ya hubiera otras piezas por debajo, en vez de depositarse sobre ellas.
- Las piezas se podían mover a un lado y a otro, aunque existieran otras piezas con las que "chocaran", en vez de quedar bloquedas por ellas.
- Cuando las piezas habían terminado de caer, no se eliminaban los fragmentos de el mismo color que estuvieran en línea.
Vayamos por partes...
Para que la pieza no caiga siempre hasta el fondo, sino que se compruebe si en algún momento toca alguna otra pieza anterior, deberemos cambiar un poco la comprobación de si debe dejar de moverse. Antes era:
if (y >= ALTOPANTALLA-MARGENINF-3*TAMANYOPIEZA) colocarPiezaEnFondo();
Y ahora en su lugar podríamos crear una función más completa, que además de comprobar si ha llegado a la parte inferior la pantalla, viera si existe ya alguna pieza en el fondo ("tablero"), debida a algún movimiento anterior. Para eso calculamos la posición X y la posición Y de la parte inferior de nuestra pieza, y miramos si en el tablero
// -- Devuelve "true" si toca fondo y no puede bajar mas bool tocadoFondo() { bool colision = false; // Si llega abajo del todo if (y >= ALTOPANTALLA-MARGENINF-3*TAMANYOPIEZA) colision = true; // o si toca con una pieza interior int posX = (x-MARGENIZQDO)/TAMANYOPIEZA; int posYfinal = (y-MARGENSUP)/TAMANYOPIEZA + 3; if (tablero[posX][posYfinal] != -1) colision = true; return colision; }
De modo que ahora "intentarBajar" quedaría así, usando esta nueva función:
// -- Intenta mover la "pieza" hacia abajo void intentarBajar() { y += incrY; if ( tocadoFondo() ) colocarPiezaEnFondo(); }
De igual modo, al mover hacia los lados, podríamos comprobar si colisiona con:
// -- Intenta mover la "pieza" hacia la izquierda void intentarMoverIzquierda() { if (x > MARGENIZQDO) // Si no he llegado al margen { // Y la casilla no esta ocupada int posX = (x-incrX-MARGENIZQDO)/TAMANYOPIEZA; int posYfinal = (y-MARGENSUP)/TAMANYOPIEZA + 3; if (tablero[posX][posYfinal] == SINPIEZA) x -= incrX; } }
Y además, despues de colocar la pieza como parte del fondo deberemos eliminar los bloques de piezas iguales que estén conectados:
// -- Coloca una pieza como parte del fondo, cuando // llegamos a la parte inferior de la pantalla // o tocamos una pieza inferior colocada antes void colocarPiezaEnFondo(){ int i; for (i=0; i<3; i++) { tablero [ (x-MARGENIZQDO)/TAMANYOPIEZA ] [ (y-MARGENSUP)/TAMANYOPIEZA + i] = tipoFragmento[i]; } eliminarBloquesConectados(); crearNuevaPieza(); }
Una forma sencilla (pero no totalmente fiable) de comprobar si 3 piezas están alineadas en horizontal podría ser
for (i=0; i<ANCHOTABLERO-2; i++) for (j=0; j<ALTOTABLERO; j++) if (tablero[i][j] != SINPIEZA) // Si hay pieza if ((tablero[i][j] == tablero[i+1][j]) && (tablero[i][j] == tablero[i+2][j])) { tablero[i][j] = PIEZAEXPLOSION; tablero[i+1][j] = PIEZAEXPLOSION; tablero[i+2][j] = PIEZAEXPLOSION; hayQueBorrar = true; }
Si hay 3 piezas alineadas, las convertimos en "piezas que explotan" (PIEZAEXPLOSION), como paso previo a que desaparezcan. Esta forma de comprobar si están en línea no es la mejor: fallará cuando haya 4 piezas iguales en horizontal, y sólo señalará 3 de ellas. Aun así, como primera aproximación puede servir, y la terminaremos de corregir en la siguiente entrega. La forma de comprobar si hay 3 piezas en vertical sería básicamente igual:
for (i=0; i<ANCHOTABLERO; i++) for (j=0; j<ALTOTABLERO-2; j++) if (tablero[i][j] != SINPIEZA) // Si hay pieza if ((tablero[i][j] == tablero[i][j+1]) && (tablero[i][j] == tablero[i][j+2])) { tablero[i][j] = PIEZAEXPLOSION; tablero[i][j+1] = PIEZAEXPLOSION; tablero[i][j+2] = PIEZAEXPLOSION; hayQueBorrar = true; }
Y tampoco hay grandes cambios si están alineadas en diagonal, caso en el que aumentaremos X e Y a la vez, o disminuiremos una mientras aumentamos la otra:
// Diagonal 1 for (i=0; i<ANCHOTABLERO-2; i++) for (j=0; j<ALTOTABLERO-2; j++) if (tablero[i][j] != SINPIEZA) // Si hay pieza if ((tablero[i][j] == tablero[i+1][j+1]) && (tablero[i][j] == tablero[i+2][j+2])) { tablero[i][j] = PIEZAEXPLOSION; tablero[i+1][j+1] = PIEZAEXPLOSION; tablero[i+2][j+2] = PIEZAEXPLOSION; hayQueBorrar = true; } // Diagonal 2 for (i=2; i<ANCHOTABLERO; i++) for (j=0; j<ALTOTABLERO-2; j++) if (tablero[i][j] != SINPIEZA) // Si hay pieza if ((tablero[i][j] == tablero[i-1][j+1]) && (tablero[i][j] == tablero[i-2][j+2])) { tablero[i][j] = PIEZAEXPLOSION; tablero[i-1][j+1] = PIEZAEXPLOSION; tablero[i-2][j+2] = PIEZAEXPLOSION; hayQueBorrar = true; }
Finalmente, cuando ya hemos marcado con la "explosión" todas las piezas que debemos borrar, las mostramos un instante:
if (hayQueBorrar) { dibujarElementos(); rest(200); ...
y después las borramos, dejando "caer" las piezas que tuvieran por encima:
// Mientras haya "explosiones", recoloco algoHaCambiado = false; for (i=0; i<ANCHOTABLERO; i++) for (j=ALTOTABLERO-1; j>=0; j--) // Abajo a arriba if (tablero[i][j] == PIEZAEXPLOSION) { // Bajo las superiores for (k=j; k>0; k--) tablero[i][k] = tablero[i][k-1]; tablero[i][0] = SINPIEZA; }
Y finalmente, si realmente hemos borrado alguna pieza, hacemos una pequeña pausa y mostramos cómo queda la pantalla de juego. Además, en ese caso deberemos comprobar si al caer las piezas se ha formado algún nuevo bloque que hay que eliminar:
if (algoHaCambiado) { dibujarElementos(); rest(200); eliminarBloquesConectados(); }
Por supuesto, quedan cosas por hacer: la rutina de marcado de piezas no es buena (falla si hay 4 o más en línea), la presentación es muy pobre, y no se lleva cuenta de la puntuación. Por tanto, prepararemos una cuarta entrega que borre correctamente, que dibuje los límites de la pantalla de juego y que calcule y muestre la puntuación de la partida.
El fuente completo de esta tercera entrega podría ser;
/*------------------------------*/ /* Intro a la programac de */ /* juegos, por Nacho Cabanes */ /* */ /* ipj26c.c */ /* */ /* Ejemplo: */ /* Tercer acercamiento a */ /* Columnas */ /* */ /* Comprobado con: */ /* - DevC++ 4.9.9.2(gcc 3.4.2) */ /* y Allegro 4.03 - WinXP */ /*------------------------------*/ #include <allegro.h> /* -------------- Constantes globales ------------- */ #define ANCHOPANTALLA 640 #define ALTOPANTALLA 480 #define ANCHOTABLERO 10 #define ALTOTABLERO 14 #define MARGENSUP 16 #define MARGENDCHO 160 #define MARGENIZQDO 160 #define MARGENINF 16 // Nmero de fragmentos distintos con los que // formar las piezas #define NUMFRAGMENTOS 7 // Ancho y alto de cada pieza #define TAMANYOPIEZA 32 // Pieza especial como paso previo cuando explotan #define PIEZAEXPLOSION 6 #define SINPIEZA -1 /* -------------- Variables globales -------------- */ PALETTE pal; BITMAP *imagenFragmentos; BITMAP *pantallaOculta; BITMAP *pieza[NUMFRAGMENTOS]; int partidaTerminada; int x = MARGENIZQDO + TAMANYOPIEZA*5; int y = MARGENSUP; int incrX = TAMANYOPIEZA; int incrY = 4; int tecla; // Tipo de imagen que corresponde a cada fragmento de pieza int tipoFragmento[3]; // El tablero de fondo int tablero[ANCHOTABLERO][ALTOTABLERO]; // Contador de fotogramas, para regular la velocidad de juego int contadorFotogramas = 0; // Prototipos de las funciones que usaremos void comprobarTeclas(); void moverElementos(); void comprobarColisiones(); void dibujarElementos(); void pausaFotograma(); void intentarMoverDerecha(); void intentarMoverIzquierda(); void intentarBajar(); void rotarColores(); void colocarPiezaEnFondo(); // --- Bucle principal del juego ----- void buclePrincipal() { partidaTerminada = FALSE; do { comprobarTeclas(); moverElementos(); comprobarColisiones(); dibujarElementos(); pausaFotograma(); } while (partidaTerminada != TRUE); } // -- Comprobac de teclas para mover personaje o salir void comprobarTeclas() { if (keypressed()) { tecla = readkey() >> 8; if ( tecla == KEY_ESC ) partidaTerminada = TRUE; if ( tecla == KEY_RIGHT ) intentarMoverDerecha(); if ( tecla == KEY_LEFT ) intentarMoverIzquierda(); if ( tecla == KEY_DOWN ) intentarBajar(); if (( tecla == KEY_SPACE ) || ( tecla == KEY_UP )) rotarColores(); clear_keybuf(); } } // -- Intenta mover la "pieza" hacia la derecha void intentarMoverDerecha() { // Si no he llegado al margen if (x < ANCHOPANTALLA-MARGENDCHO-TAMANYOPIEZA) { // Y la casilla no esta ocupada int posX = (x+incrX-MARGENIZQDO)/TAMANYOPIEZA; int posYfinal = (y-MARGENSUP)/TAMANYOPIEZA + 3; if (tablero[posX][posYfinal] == SINPIEZA) x += incrX; } } // -- Intenta mover la "pieza" hacia la izquierda void intentarMoverIzquierda() { if (x > MARGENIZQDO) // Si no he llegado al margen { // Y la casilla no esta ocupada int posX = (x-incrX-MARGENIZQDO)/TAMANYOPIEZA; int posYfinal = (y-MARGENSUP)/TAMANYOPIEZA + 3; if (tablero[posX][posYfinal] == SINPIEZA) x -= incrX; } } // -- Devuelve "true" si toca fondo y no puede bajar mas bool tocadoFondo() { bool colision = false; // Si llega abajo del todo if (y >= ALTOPANTALLA-MARGENINF-3*TAMANYOPIEZA) colision = true; // o si toca con una pieza interior int posX = (x-MARGENIZQDO)/TAMANYOPIEZA; int posYfinal = (y-MARGENSUP)/TAMANYOPIEZA + 3; if (tablero[posX][posYfinal] != SINPIEZA) colision = true; return colision; } // -- Intenta mover la "pieza" hacia abajo void intentarBajar() { y += incrY; if ( tocadoFondo() ) colocarPiezaEnFondo(); } // -- Rotar los colores de la "pieza" void rotarColores() { int auxiliar = tipoFragmento[0]; tipoFragmento[0] = tipoFragmento[1]; tipoFragmento[1] = tipoFragmento[2]; tipoFragmento[2] = auxiliar; } // -- Mover otros elementos del juego void moverElementos() { // Las piezas bajan solas, pero unicamente un // fotograma de cada 10 contadorFotogramas ++; if (contadorFotogramas >= 10) { y += incrY; contadorFotogramas = 0; if ( tocadoFondo() ) colocarPiezaEnFondo(); } } // -- Comprobar colisiones de nuestro elemento con otros, o disparos con enemigos, etc void comprobarColisiones() { // Por ahora, no hay colisiones que comprobar } // -- Dibujar elementos en pantalla void dibujarElementos() { int i,j; // Borro la pantalla y dibujo la pieza clear_bitmap(pantallaOculta); // Dibujo el "fondo", con los trozos de piezas anteriores for (i=0; i<ANCHOTABLERO; i++) for (j=0; j<ALTOTABLERO; j++) if (tablero[i][j] != SINPIEZA) draw_sprite(pantallaOculta, pieza[ tablero[i][j] ], MARGENIZQDO + TAMANYOPIEZA*i, MARGENSUP + TAMANYOPIEZA*j); // Dibujo la "pieza" for (i=0; i<3; i++) draw_sprite(pantallaOculta, pieza[ tipoFragmento[i] ], x, y+32*i); // Sincronizo con el barrido para evitar parpadeos // y vuelco la pantalla oculta vsync(); blit(pantallaOculta, screen, 0, 0, 0, 0, ANCHOPANTALLA, ALTOPANTALLA); } // -- Pausa hasta el siguiente fotograma void pausaFotograma() { // Para 25 fps: 1000/25 = 40 milisegundos de pausa rest(40); } // -- Crea una nueva pieza con componentes al azar void crearNuevaPieza() { int i; for (i=0; i<3; i++) tipoFragmento[i] = rand() % (NUMFRAGMENTOS-1); x = MARGENIZQDO + TAMANYOPIEZA*5; y = MARGENSUP; } // -- Revisa el fondo para comprobar si hay varias // piezas iguales unidas, que se puedan eliminar void eliminarBloquesConectados() { int i,j,k; bool hayQueBorrar = false; // Busco en horizontal for (i=0; i<ANCHOTABLERO-2; i++) for (j=0; j<ALTOTABLERO; j++) if (tablero[i][j] != SINPIEZA) // Si hay pieza if ((tablero[i][j] == tablero[i+1][j]) && (tablero[i][j] == tablero[i+2][j])) { tablero[i][j] = PIEZAEXPLOSION; tablero[i+1][j] = PIEZAEXPLOSION; tablero[i+2][j] = PIEZAEXPLOSION; hayQueBorrar = true; } // Busco en vertical for (i=0; i<ANCHOTABLERO; i++) for (j=0; j<ALTOTABLERO-2; j++) if (tablero[i][j] != SINPIEZA) // Si hay pieza if ((tablero[i][j] == tablero[i][j+1]) && (tablero[i][j] == tablero[i][j+2])) { tablero[i][j] = PIEZAEXPLOSION; tablero[i][j+1] = PIEZAEXPLOSION; tablero[i][j+2] = PIEZAEXPLOSION; hayQueBorrar = true; } // Diagonal 1 for (i=0; i<ANCHOTABLERO-2; i++) for (j=0; j<ALTOTABLERO-2; j++) if (tablero[i][j] != SINPIEZA) // Si hay pieza if ((tablero[i][j] == tablero[i+1][j+1]) && (tablero[i][j] == tablero[i+2][j+2])) { tablero[i][j] = PIEZAEXPLOSION; tablero[i+1][j+1] = PIEZAEXPLOSION; tablero[i+2][j+2] = PIEZAEXPLOSION; hayQueBorrar = true; } // Diagonal 2 for (i=2; i<ANCHOTABLERO; i++) for (j=0; j<ALTOTABLERO-2; j++) if (tablero[i][j] != SINPIEZA) // Si hay pieza if ((tablero[i][j] == tablero[i-1][j+1]) && (tablero[i][j] == tablero[i-2][j+2])) { tablero[i][j] = PIEZAEXPLOSION; tablero[i-1][j+1] = PIEZAEXPLOSION; tablero[i-2][j+2] = PIEZAEXPLOSION; hayQueBorrar = true; } // Borro, si es el caso if (hayQueBorrar) { dibujarElementos(); rest(200); bool algoHaCambiado; do { // Mientras haya "explosiones", recoloco algoHaCambiado = false; for (i=0; i<ANCHOTABLERO; i++) for (j=ALTOTABLERO-1; j>=0; j--) // Abajo a arriba if (tablero[i][j] == PIEZAEXPLOSION) { algoHaCambiado = true; // Bajo las superiores for (k=j; k>0; k--) tablero[i][k] = tablero[i][k-1]; tablero[i][0] = SINPIEZA; } } while (algoHaCambiado); // Y vuelvo a comprobar si hay nuevas cosas que borrar if (algoHaCambiado) { dibujarElementos(); rest(200); eliminarBloquesConectados(); } } } // -- Coloca una pieza como parte del fondo, cuando // llegamos a la parte inferior de la pantalla // o tocamos una pieza inferior colocada antes void colocarPiezaEnFondo(){ int i; for (i=0; i<3; i++) { tablero [ (x-MARGENIZQDO)/TAMANYOPIEZA ] [ (y-MARGENSUP)/TAMANYOPIEZA + i] = tipoFragmento[i]; } eliminarBloquesConectados(); crearNuevaPieza(); } /* -------------- Rutina de inicializacin -------- */ int inicializa() { int i,j; allegro_init(); // Inicializamos Allegro install_keyboard(); install_timer(); // Intentamos entrar a modo grafico set_color_depth(32); if (set_gfx_mode(GFX_SAFE,ANCHOPANTALLA, ALTOPANTALLA, 0, 0) != 0) { set_gfx_mode(GFX_TEXT, 0, 0, 0, 0); allegro_message( "Incapaz de entrar a modo grafico\n%s\n", allegro_error); return 1; } // e intentamos abrir imgenes imagenFragmentos = load_bmp("columnas_piezas.bmp", pal); if (!imagenFragmentos) { set_gfx_mode(GFX_TEXT, 0, 0, 0, 0); allegro_message("No se ha podido abrir la imagen\n"); return 1; } // Ahora reservo espacio para los otros sprites for (i=0; i<NUMFRAGMENTOS; i++) { pieza[i] = create_bitmap(32, 32); // Y los extraigo de la imagen "grande" blit(imagenFragmentos, pieza[i] // bitmaps de origen y destino , 32*i, 0 // coordenadas de origen , 0, 0 // posicin de destino , 32, 32); // anchura y altura } set_palette(pal); srand(time(0)); // Pantalla oculta para evitar parpadeos // (doble buffer) pantallaOculta = create_bitmap(ANCHOPANTALLA, ALTOPANTALLA); // Vaco el tablero de fondo for (i=0; i<ANCHOTABLERO; i++) for (j=0; j<ALTOTABLERO; j++) tablero[i][j] = SINPIEZA; crearNuevaPieza(); // Y termino indicando que no ha habido errores return 0; } /* ------------------------------------------------ */ /* */ /* -------------- Cuerpo del programa ------------- */ int main() { int i,j; // Intento inicializar if (inicializa() != 0) exit(1); // Bucle principal del juego buclePrincipal(); // Libero memoria antes de salir for (i=0; i<NUMFRAGMENTOS; i++) destroy_bitmap(pieza[i]); destroy_bitmap(pantallaOculta); rest(1000); return 0; } /* Termino con la "macro" que me pide Allegro */ END_OF_MAIN();