31. La aproximación orientada a objetos (4). MiniMiner 4: Una pantalla de juego real

Ahora, en vez de dibujar desde el cuerpo del progama un fondo sencillo formado por varias casillas repetitivas de suelo, tendremos una pantalla de fondo más compleja, formada por varios elementos de distinto tipo. En esta versión será un fondo estático, pero dentro de poco será capaz de indicar al personaje y los enemigos si se pueden mover a una cierta posición, y tambíen será capaz de mostrar objetos en movimiento.

Nuestro diagrama de clases incluirá una nueva clase, que llamaremos "Nivel":

Para diseñar como será cada nivel, usaremos un mapa de casillas, como ya hicimos con el juego de la serpiente. Como en este caso, estamos imitando un juego existente, cuadricularemos la pantalla original para tratar de descomponer en elementos básicos. Para eso nos pueden ayudar ciertos programas de retoque de imágenes, como el GIMP, que permite superponer a la imagen una cuadrícula, que en nuestro caso es de 16x16 píxeles (en la versión 2.6.8 se hace desde el menú "Filtros", en la opción Renderizado / Patrón / Rejilla):

Ahora podemos crear un array bidimensional de caracteres, que represente a esa misma pantalla, algo como:


L        V T    T            V L 
L               V              L 
L                              L 
L                              L 
L                      VA  A   L 
LSSSSSSSSSSSSSFFFFSFFFFSSSSSSSSL 
L                             VL 
LSSS                           L 
L                LLL A         L 
LSSSS   DDDDDDDDDDDDDDDDDDDD   L 
L                            SSL 
L                              L 
L            A      LLLFFFFFSSSL 
L    SSSSSSSSSSSSSSS         PPL 
L                            PPL 
LSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSL 

Es más, éste mapa podría estar en un fichero, de modo que nuestra clase Nivel lea dicho fichero, y se pueda modificar un nivel (o pronto, crear nuevos niveles) sin necesidad de alterar el código fuente.

Así, esta clase nivel podría tener apenas dos métodos: un "leerDeFichero" y un "dibujarOculta". El fichero de cabecera podría ser así:

/*------------------------------*/
/*  Intro a la programac de     */
/*  juegos, por Nacho Cabanes   */
/*                              */
/*   Nivel.h:                   */
/*     La pantalla del nivel 1  */
/*     Miniminer (version 0.04) */
/*     Fichero de cabecera      */
/*                              */
/*  Comprobado con:             */
/*  - DevC++ 4.9.9.2(gcc 3.4.2) */
/*    y Allegro 4.03 - WinXP    */
/*  - gcc 4.4.3 y Allegro 4.2   */
/*    en Ubuntu 10.04           */
/*------------------------------*/
 
 
#ifndef Nivel_h
#define Nivel_h
 
#include <vector>
#include <string>
using namespace std;
 
 
#include "ElementoGraf.h"
 
 
class Nivel {
 
 public:
 
    Nivel();
    void dibujarOculta(Hardware h);
    void leerDeFichero();
 
 private:
    int nivelActual;
 
       /* El mapa que representa a la pantalla */
       /* 16 filas y 32 columnas (de 16x16 cada una) */
    #define MAXFILAS 16
    #define MAXCOLS  32
 
    char mapa[MAXFILAS][MAXCOLS];
    ElementoGraf *fragmentoNivel[MAXFILAS][MAXCOLS];
 
};
#endif
 

Y su desarrollo:

/*------------------------------*/
/*  Intro a la programac de     */
/*  juegos, por Nacho Cabanes   */
/*                              */
/*   Nivel.cpp:                 */
/*     La pantalla del nivel 1  */
/*     Miniminer (version 0.04) */
/*     Fichero de desarrollo    */
/*                              */
/*  Comprobado con:             */
/*  - DevC++ 4.9.9.2(gcc 3.4.2) */
/*    y Allegro 4.03 - WinXP    */
/*  - gcc 4.4.3 y Allegro 4.2   */
/*    en Ubuntu 10.04           */
/*------------------------------*/
 
#include "Hardware.h"
#include "ElementoGraf.h"
#include "Nivel.h"
 
#ifdef __cplusplus
    #include <cstdlib>
    #include <cstdio>
#else
    #include <stdlib.h>
    #include <stdio.h>
#endif
 
#include <math.h>
#include <string>
 
#include <allegro.h>
 
Nivel::Nivel()
{
    nivelActual = 1;
 
    int i, j;
    int anchoImagen = 16, altoImagen = 16;
    int margenIzq = 64, margenSup = 48;
 
    for(i=0; i<MAXCOLS; i++)
      for (j=0; j<MAXFILAS; j++) 
      {
            fragmentoNivel[j][i] = new ElementoGraf();
            fragmentoNivel[j][i]->moverA(
                margenIzq + i*anchoImagen, margenSup + j*altoImagen);
      }
 
    leerDeFichero();
 
}
 
 
/** dibujarOculta: muestra el nivel sobre el fondo
 *  de pantalla, con su imagen correspondiente
 *  segun el tipo de ladrillo del que se trate
 */
 
void Nivel::dibujarOculta(Hardware h)
{
    int i, j;
 
    h.borrarOculta();
 
    for(i=0; i<MAXCOLS; i++)
      for (j=0; j<MAXFILAS; j++) 
        h.dibujarOculta( *fragmentoNivel[j][i] );
}
 
 
 
/** leerNivel: lee de fichero los datos del nivel actual
 */
void Nivel::leerDeFichero()
{
    // Si hay fichero de datos, lo leo
    FILE *fichDatos;
    char linea[MAXCOLS+2];
    char nombreFich[50];
    sprintf(nombreFich, "nivel%03d.dat", nivelActual);
    fichDatos = fopen(nombreFich, "rt"); //### Mas adelante: creciente
    if (fichDatos != NULL)
        for (int j=0; j<MAXFILAS;j++)   {
            fgets(linea,MAXCOLS+1,fichDatos);
            if (strlen(linea) < 5) // Salto los avances de linea de Windows
              fgets(linea,MAXCOLS+1,fichDatos);
            for (int i=0; i<MAXCOLS;i++)   {
              switch(linea[i]) {
                  case 'S': 
                       fragmentoNivel[j][i]->crearDesdeFichero("suelo.bmp");
                       break;
                  case 'F': 
                       fragmentoNivel[j][i]->crearDesdeFichero("sueloFragil.bmp");
                       break;
                  case 'L': 
                       fragmentoNivel[j][i]->crearDesdeFichero("ladrillo.bmp");
                       break;
                  case 'V': 
                       fragmentoNivel[j][i]->crearDesdeFichero("llave.bmp");
                       break;
                  case 'P': 
                       fragmentoNivel[j][i]->crearDesdeFichero("puerta.bmp");
                       break;
                  case 'D': 
                       fragmentoNivel[j][i]->crearDesdeFichero("deslizante.bmp");
                       break;
                  case 'A': 
                       fragmentoNivel[j][i]->crearDesdeFichero("arbol.bmp");
                       break;
                  case 'T': 
                       fragmentoNivel[j][i]->crearDesdeFichero("techo.bmp");
                       break;
                  default:
                       fragmentoNivel[j][i]->crearDesdeFichero("fondoVacio.bmp");
                       break;
              }
 
            }
        }
 
}
 
 

Y el cuerpo del programa, apenas cambia en que ya no es necesaria la función "dibujarFondo()", sino que se crea un objeto de la clase "Nivel" y se llama a su método "dibujarOculta()":

/*------------------------------*/
/*  Intro a la programac de     */
/*  juegos, por Nacho Cabanes   */
/*                              */
/*    miner04.cpp               */
/*                              */
/*  Ejemplo:                    */
/*   "MiniMiner" (version 0.04) */
/*                              */
/*  Comprobado con:             */
/*  - DevC++ 4.9.9.2(gcc 3.4.2) */
/*    y Allegro 4.03 - WinXP    */
/*  - gcc 4.4.3 y Allegro 4.2   */
/*    en Ubuntu Linux 10.04     */
/*------------------------------*/
 
#include "Hardware.h"
#include "ElementoGraf.h"
#include "Personaje.h"
#include "Enemigo.h"
#include "Presentacion.h"
#include "Nivel.h"
 
/* -------------- Constantes globales ------------- */
#define ANCHOPANTALLA 640
#define ALTOPANTALLA 480
 
/* -------------- Variables globales -------------- */
Hardware hard;
Personaje *personaje;
Enemigo *enemigo;
Presentacion *presentacion;
Nivel *primerNivel;
 
int partidaTerminada;
int incrX = 4;
int incrY = 4;
int tecla;
int ySuelo = 232;
 
 
// Prototipos de las funciones que usaremos
void comprobarTeclas();
void moverElementos();
void comprobarColisiones();
void dibujarElementos();
void pausaFotograma();
void moverDerecha();
void moverIzquierda();
void lanzarPresentacion();
void moverEnemigo();
void dibujarFondo();
 
 
 
// --- 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 (hard.comprobarTecla(TECLA_ESC))
     partidaTerminada = true;
 
  if (hard.comprobarTecla(TECLA_DCHA))
     personaje->moverDerecha();
  else if (hard.comprobarTecla(TECLA_IZQD))
     personaje->moverIzquierda();
}
 
 
// -- Mover otros elementos del juego 
void moverElementos() {
  enemigo->mover();
}
 
 
// -- 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() {
 
  hard.borrarOculta();
  primerNivel->dibujarOculta(hard);
  hard.dibujarOculta( *enemigo );
  hard.dibujarOculta( *personaje );
  hard.visualizarOculta();
}
 
 
// -- Pausa hasta el siguiente fotograma
void pausaFotograma() {
  // Para 25 fps: 1000/25 = 40 milisegundos de pausa
  hard.pausa(40);
}
 
 
// -- Funciones que no son de la logica de juego, sino de 
// funcionamiento interno de otros componentes
 
// -- Pantalla de presentacion
void lanzarPresentacion() {
  presentacion->mostrar(hard);
}
 
 
/* -------------- Rutina de inicializacin -------- */
int inicializa()
{
    hard.inicializar(640,480);
    personaje = new Personaje();
    enemigo = new Enemigo();
    presentacion = new Presentacion();
    primerNivel = new Nivel();
 
   // 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);
 
    lanzarPresentacion();
    buclePrincipal();
 
    hard.pausa(1000);
    return 0;
}
 
            /* Termino con la "macro" que me pide Allegro */
END_OF_MAIN();
 

Y la apariencia resultante ya es mucho más cercana a la del juego original: en windows 7 se vería así:

y en Ubuntu 10.04 así:

Puedes descargar toda esta versión, en un fichero ZIP, que incluye todos los fuentes, las imágenes, el proyecto de Dev-C++ listo para compilar en Windows, y un fichero "compila.sh" para compilar en Linux.