24. Un "bucle de juego" clásico: aproximación a Columnas.(*)

Vamos a imitar un juego clásico, muy entretenido y razonablemente fácil. Lo llamaremos Columnas, para evitar problemas de Copyright. La apariencia del original es ésta:

Columns Original

Para conseguirlo, en vez de crear un "main" cada vez más grande, como pasaba con el "matamarcianos", lo haremos más modular desde un principio. Seguiremos la apariencia de un "bucle de juego" clásico, que podría ser algo así:


repetir
  comprobar teclas
  calcular siguiente posición de elementos
  comprobar colisiones entre elementos
  dibujar elementos en pantalla
  pausa hasta tiempo final de fotograma
hasta final de partida 

Si nos ayudamos de funciones auxiliares, esto se convertiría simplemente en algo como:

 
void buclePrincipal() {
  partidaTerminada = FALSE;
  do {
    comprobarTeclas();
    moverElementos();
    comprobarColisiones();
    dibujarElementos();
    pausaFotograma();
  } while (partidaTerminada != TRUE);
}
 

Donde, a su vez "comprobarTeclas" sería algo tan sencillo como:

 
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_SPACE )
       rotarColores();
     clear_keybuf();
   }
}
 

Mientras que "moverElementos" no haría casi nada en un juego como éste, al no existir enemigos que se muevan solos. Se limitaría a encargarse del movimiento "automático" de nuestro personaje, que baja automáticamente poco a poco, pero eso lo haremos en la próxima entrega; por ahora estará vacío:

 
void moverElementos() {
  // Ningun elemento extra que mover por ahora
}
 

De igual modo, "comprobarColisiones" tampoco haría nada en este juego, al no existir "disparos" ni nada parecido. En la siguiente entrega sí deberemos ver si nuestra ficha realmente se puede mover a la derecha o existe algún obstáculo, pero eso quizá sea más razonable hacerlo desde el "intentarMoverDerecha", cuando el jugador indica que se quiere mover en cierta dirección.

 
void comprobarColisiones() {
  // Por ahora, no hay colisiones que comprobar
}
 

"dibujarElementos" se limitará a dibujar el fondo (en la siguiente entrega) y la pieza (ya en esta entrega) en la pantalla oculta, y luego volcar esa pantalla oculta a la visible para evitar parpadeos (la técnica del "doble buffer" que ya habíamos mencionado).

 
void dibujarElementos() {
 
  // Borro la pantalla y dibujo la nave
  clear_bitmap(pantallaOculta);
 
  // Dibujo el "fondo", con los trozos de piezas anteriores
  // (aun no)
 
  // Dibujo la "pieza"
  draw_sprite(pantallaOculta, pieza, x, y);
 
  // Sincronizo con el barrido para evitar parpadeos
  // y vuelco la pantalla oculta
  vsync();
  blit(pantallaOculta, screen, 0, 0, 0, 0,
    ANCHOPANTALLA, ALTOPANTALLA);
 
}
 

Finalmente, "pausaFotograma" esperará el tiempo que sea neceasrio hasta el momento de dibujar el siguiente fotograma en pantalla. Si queremos 25 fotogramas por segundo, como en la televisión, para lograr una sensación de movimiento continuo, deberíamos hacer una pausa de 40 milisegundos entre un fotograma y otro (1000 / 25 = 40). Estamos suponiendo que el tiempo de cálculo y dibujado es despreciable, pero esto es verdad en un juego tan sencillo como éste. En un juego "más real", calcularíamos cuanto se tarda en calcular posiciones, colisiones, dibujar elementos, etc., y lo restaríamos. Por ejemplo, si se tarda 10 milisegundos en esas tareas, la espera que realmente haríamos sería 40-10 = 30 milésimas de segundo. Esas mejoras las dejamos para más adelante. De momento, la pausa de 40 ms la haremos así:

 
void pausaFotograma() {
  // Para 25 fps: 1000/25 = 40 milisegundos de pausa
  rest(40);
}
 

De momento usaremos esta imagen: columnas_pieza0.bmp

Y el fuente completo sería por ahora poco más que juntar esas rutinas, una función de inicializar que entrase a modo gráfico y un "main" que cargase e bucle principal del juego, de modo que podría quedar así

/*------------------------------*/
/*  Intro a la programac de     */
/*  juegos, por Nacho Cabanes   */
/*                              */
/*    ipj24c.c                  */
/*                              */
/*  Ejemplo:                    */
/*    Primer 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 MARGENSUP   20
#define MARGENDCHO  25
#define MARGENIZQDO 25
#define MARGENINF   30
 
/* -------------- Variables globales -------------- */
PALETTE pal;
BITMAP *pieza;
BITMAP *pantallaOculta;
 
int partidaTerminada;
int x = 300;
int y = 200;
int incrX = 4;
int incrY = 4;
int tecla;
 
// Prototipos de las funciones que usaremos
void comprobarTeclas();
void moverElementos();
void comprobarColisiones();
void dibujarElementos();
void pausaFotograma();
void intentarMoverDerecha();
void intentarMoverIzquierda();
void rotarColores();
 
 
 
// --- 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_SPACE )
       rotarColores();
     clear_keybuf();
   }
}
 
 
// -- Intenta mover la "pieza" hacia la derecha
void intentarMoverDerecha() {
  x += incrX;
}
 
 
// -- Intenta mover la "pieza" hacia la izquierda
void intentarMoverIzquierda() {
  x -= incrX;
}
 
 
// -- Rotar los colores de la "pieza"
void rotarColores() {
  // (Todavia no)
}
 
 
// -- Mover otros elementos del juego 
void moverElementos() {
  // Ningun elemento extra que mover por ahora
}
 
 
// -- 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() {
 
  // Borro la pantalla y dibujo la nave
  clear_bitmap(pantallaOculta);
 
  // Dibujo el "fondo", con los trozos de piezas anteriores
  // (aun no)
 
  // Dibujo la "pieza"
  draw_sprite(pantallaOculta, pieza, x, y);
 
  // 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);
}
 
 
 
/* -------------- 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
    pieza = load_bmp("columnas_pieza0.bmp", pal);
    if (!pieza) {
        set_gfx_mode(GFX_TEXT, 0, 0, 0, 0);
        allegro_message("No se ha podido abrir la imagen\n");
        return 1;
    }
    set_palette(pal);
 
    // Pantalla oculta para evitar parpadeos
    // (doble buffer)
    pantallaOculta = create_bitmap(ANCHOPANTALLA, ALTOPANTALLA);
 
   // 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);
 
    buclePrincipal();
 
    destroy_bitmap(pieza);
    destroy_bitmap(pantallaOculta);
 
    rest(2000);
    return 0;
 
}
 
            /* Termino con la "macro" que me pide Allegro */
END_OF_MAIN();