29. La aproximación orientada a objetos (1). MiniMiner 2: Aislando del hardware. (*)

Queremos que nuestro juego siga los principios de la "Programación orientada a objetos": en vez de tratarse de un único "macroprograma", lo plantearemos como una serie de objetos que se pasan mensajes unos a otros. Esto tendrá una serie de ventajas, entre las que podemos destacar:

  • Es un planteamiento "más natural", porque nuestro juego tiene un "personaje" que manejar, varias "pantallas" que recorrer, en las cuales hay "llaves" que recoger, pero existen "enemigos" que tratarán de evitarlo. Todo ello suena a que participan "objetos" de distintas "clases".
  • La "herencia" nos ayudará a escribir menos código repetitivo: si creamos una clase de objetos llamada "ElementoGrafico", que sea capaz de mostrarse en una cierta posición de pantalla, y decimos que nuestro "Personaje" es un tipo de "ElementoGrafico", no será necesario indicarle cómo debe dibujarse en pantalla. Como es un "ElementoGráfico", esa será una de las cosas que "sabrá hacer".
  • Como ahora el fuente estará desglosado en varios objetos, cada uno de los cuales tiene unas funciones claras, sería más fácil repartir el trabajo entre varias personas, si formáramos parte de un grupo de programadores que trabaja en un proyecto común.
  • Además, podemos crear clases auxiliares, que no sean estrictamente necesarias para nuestro juego, pero que nos puedan permitir trabajar con más comodidad. Por ejemplo, podemos definir una clase Hardware que nos oculte los detalles de Allegro o de la biblioteca gráfica que estamos empleando, de modo que el trabajo de adaptar nuestro juego para que funcione usando SDL o cualquier otra biblioteca gráfica alternativa (si llegara a ser necesario) sea mucho menor.

La primera mejora que vamos a hacer va a estar relacionada con la última ventaja que hemos mencionado: aislaremos bastante el juego de la biblioteca Allegro, ayudándonos de dos primeras clases:

  • Una clase "Hardware", que nos oculte el acceso a teclado (con funciones para comprobar qué tecla se ha pulsado) y ha pantalla (con funciones para borrar la pantalla, dibujar una imagen en la pantalla oculta o hacer visible esa pantalla oculta -lo que habíamos llamado el "doble buffer"-).
  • Una clase "Elemento Gráfico" (ElementoGraf), con funciones para crearlo a partir de un fichero, moverlo a ciertas coordenadas, comprobar si ha "chocado" con otro elemento gráfico, etc.
  • El resto del juego seguirá estando contenido en el fichero "miner.cpp", que será bastante parecido a como era antes (pero algo más sencillo, si lo hemos hecho bien).

Necesitaremos un fichero de cabecera (.h) y uno de desarrollo (.cpp) para cada clase de objetos que queramos definir. Por tanto, en este momento tendremos 5 ficheros: los dos de la clase Hardware, los dos de la clase ElementoGraf, y el cuerpo del juego.

El fichero de cabecera de Hardware podría ser así:

/*------------------------------*/
/*  Intro a la programac de     */
/*  juegos, por Nacho Cabanes   */
/*                              */
/*   Hardware.h:                */
/*     Clase "Hardware"         */
/*       para Allegro           */
/*     Fichero de cabecera      */
/*                              */
/*     Parte de "MiniMiner"     */
/*                              */
/*  Ejemplo:                    */
/*    Primer acercamiento a     */
/*    "MiniMiner"               */
/*                              */
/*  Comprobado con:             */
/*  - DevC++ 4.9.9.2(gcc 3.4.2) */
/*    y Allegro 4.03 - WinXP    */
/*------------------------------*/
 
#ifndef Hardware_h
#define Hardware_h
 
#include <vector>
#include <string>
using namespace std;
 
#include <allegro.h>
 
 
#include "ElementoGraf.h"
 
#define TECLA_ESC KEY_ESC
#define TECLA_DCHA KEY_RIGHT
#define TECLA_ARRB KEY_UP
#define TECLA_ABAJ KEY_DOWN
#define TECLA_IZQD KEY_LEFT
#define TECLA_ESPACIO KEY_SPACE
#define TECLA_A KEY_a
#define TECLA_B KEY_b
#define TECLA_C KEY_c
#define TECLA_D KEY_d
#define TECLA_E KEY_e
#define TECLA_F KEY_f
#define TECLA_G KEY_g
#define TECLA_H KEY_h
#define TECLA_I KEY_i
#define TECLA_J KEY_j
#define TECLA_K KEY_k
#define TECLA_L KEY_l
#define TECLA_M KEY_m
#define TECLA_N KEY_n
#define TECLA_O KEY_o
#define TECLA_P KEY_p
#define TECLA_Q KEY_q
#define TECLA_R KEY_r
#define TECLA_S KEY_s
#define TECLA_T KEY_t
#define TECLA_U KEY_u
#define TECLA_V KEY_v
#define TECLA_W KEY_w
#define TECLA_X KEY_x
#define TECLA_Y KEY_y
#define TECLA_Z KEY_z
 
#define TECLA_0 KEY_0
#define TECLA_1 KEY_1
#define TECLA_2 KEY_2
#define TECLA_3 KEY_3
#define TECLA_4 KEY_4
#define TECLA_5 KEY_5
#define TECLA_6 KEY_6
#define TECLA_7 KEY_7
#define TECLA_8 KEY_8
#define TECLA_9 KEY_9
 
#define TECLA_F1 KEY_F1
 
class Hardware {
 
 public:
 
    void inicializar(int ancho, int alto);
    bool comprobarTecla();
 
    bool comprobarTecla(int codigoTecla);
    int esperarTecla();
    void vaciarBufferTeclado();
    bool algunaTeclaPulsada();
 
 
    void borrarOculta();
    void dibujarOculta(ElementoGraf e);
 
    void visualizarOculta();
 
    void pausa(long ms);
 
 private:
 
    int anchoPantalla;
    int altoPantalla;
 
    BITMAP *pantallaOculta;
    int maxX;
    int maxY;
    int colores;
    int teclaPulsada;
    int posXRaton;
    int posYRaton;
 
 private:
 
    BITMAP pantallaVisible;
    BITMAP fondo;
 
 
};
#endif
 

Y el desarrollo podría ser así:

/*------------------------------*/
/*  Intro a la programac de     */
/*  juegos, por Nacho Cabanes   */
/*                              */
/*   Hardware.cpp:              */
/*     Clase "Hardware"         */
/*       para Allegro           */
/*     Fichero de desarrollo    */
/*                              */
/*     Parte de "MiniMiner"     */
/*                              */
/*  Ejemplo:                    */
/*    Primer acercamiento a     */
/*    "MiniMiner"               */
/*                              */
/*  Comprobado con:             */
/*  - DevC++ 4.9.9.2(gcc 3.4.2) */
/*    y Allegro 4.03 - WinXP    */
/*------------------------------*/
 
#include "Hardware.h"
#include "ElementoGraf.h"
#include "iostream"
 
#ifdef __cplusplus
    #include <cstdlib>
#else
    #include <stdlib.h>
#endif
 
#include <math.h>
#include <string>
 
#include <allegro.h>
 
 
void Hardware::inicializar(int ancho, int alto)
{
 
    cout << "hard-ctor";
    allegro_init();        // Inicializamos Allegro
    install_keyboard();
    install_timer();
 
    anchoPantalla = ancho;
    altoPantalla = alto;
 
                           // Intentamos entrar a modo grafico
    set_color_depth(32);
    if (set_gfx_mode(GFX_SAFE, ancho, alto, 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);
        exit( 1 );
    }
 
    // Pantalla oculta para evitar parpadeos
    // (doble buffer)
    pantallaOculta = create_bitmap(ancho, alto);
 
 
}
 
 
/** teclaPulsada: devuelve TRUE si alguna tecla
 *  ha sido pulsada
 */
bool Hardware::algunaTeclaPulsada()
{
    return keypressed;
}
 
 
/** comprobarTecla: devuelve TRUE si una cierta tecla
 *  ha sido pulsada
 */
bool Hardware::comprobarTecla(int codigoTecla)
{
      return key[codigoTecla];
}
 
 
/** esperarTecla: pausa hasta que se pulse una tecla,
 *  devuelve el codigo de tecla pulsada
 */
int Hardware::esperarTecla()
{
    return readkey() >> 8;
}
 
 
 
 
void Hardware::borrarOculta()
{
    // Borrar pantalla de fondo
    clear_bitmap(pantallaOculta);
}
 
 
void Hardware::dibujarOculta(ElementoGraf e)
{
    draw_sprite( pantallaOculta, e.leerImagen(), e.leerX(), e.leerY() );
}
 
 
 
void Hardware::visualizarOculta()
{
    // Sincronizo con el barrido para evitar parpadeos
    // y vuelco la pantalla oculta
    vsync();
    blit(pantallaOculta, screen, 0, 0, 0, 0,
      anchoPantalla, altoPantalla);
}
 
 
void Hardware::pausa(long ms)
{
    rest(ms);
}
 
 

Del mismo modo, el fichero de cabecera de ElementoGraf podría ser:

/*------------------------------*/
/*  Intro a la programac de     */
/*  juegos, por Nacho Cabanes   */
/*                              */
/*   ElementoGraf.h:            */
/*     Clase "elemento grfico" */
/*       para Allegro           */
/*     Fichero de cabecera      */
/*                              */
/*     Parte de "MiniMiner"     */
/*                              */
/*  Ejemplo:                    */
/*    Primer acercamiento a     */
/*    "MiniMiner"               */
/*                              */
/*  Comprobado con:             */
/*  - DevC++ 4.9.9.2(gcc 3.4.2) */
/*    y Allegro 4.03 - WinXP    */
/*------------------------------*/
 
#ifndef ElementoGraf_h
#define ElementoGraf_h
 
#include <allegro.h>
 
class ElementoGraf {
 
 public:
 
    void moverA(int x, int y);
    void indicarAnchoAlto(int an, int al);
    void crearDesdeFichero(char *nombre);
    BITMAP* leerImagen();
    int leerX();
    int leerY();
    int leerAnchura();
    int leerAltura();
 
    bool colisionCon(ElementoGraf e2);
    bool colisionCon(int x, int y, int ancho, int alto);
 
 protected:
 
    int posX;
    int posY;
    int anchura, altura;
    int anchuraOrig, alturaOrig;
    int colorTransp;
 
 private:
 
    BITMAP* imagen;
 
};
#endif
 

Y su desarrollo:

/*------------------------------*/
/*  Intro a la programac de     */
/*  juegos, por Nacho Cabanes   */
/*                              */
/*   ElementoGraf.cpp:          */
/*     Clase "elemento grfico" */
/*       para Allegro           */
/*     Fichero de desarrollo    */
/*                              */
/*     Parte de "MiniMiner"     */
/*                              */
/*  Ejemplo:                    */
/*    Primer acercamiento a     */
/*    "MiniMiner"               */
/*                              */
/*  Comprobado con:             */
/*  - DevC++ 4.9.9.2(gcc 3.4.2) */
/*    y Allegro 4.03 - WinXP    */
/*------------------------------*/
 
#include "ElementoGraf.h"
 
#ifdef __cplusplus
    #include <cstdlib>
#else
    #include <stdlib.h>
#endif
 
#include <allegro.h>
 
/** moverA: cambia la posicion del elemento grafico
 *  (actualiza tambien las coordenadas del centro)
 */
void ElementoGraf::moverA(int x, int y)
{
    posX = x;
    posY = y;
}
 
/** indicarAnchoAlto: indica el ancho y el alto del
 *  elemento grfico, para que se pueda calcular colisiones
 */
void ElementoGraf::indicarAnchoAlto(int an, int al)
{
    anchura = an;
    altura = al;
}
 
/** leerX: devuelve la coordenada X de la posicion
 */
int ElementoGraf::leerX()
{
    return posX;
}
 
/** leerY: devuelve la coordenada Y de la posicion
 */
int ElementoGraf::leerY()
{
    return posY;
}
 
/** leerAnchura: devuelve la anchura del elemento grafico
 */
int ElementoGraf::leerAnchura()
{
    return anchura;
}
 
/** leerAltura: devuelve la altura del elemento grafico
 */
int ElementoGraf::leerAltura()
{
    return altura;
}
 
 
/** leerImagen: devuelve la imagen (bitmap) del elemento grafico
 */
BITMAP* ElementoGraf::leerImagen()
{
    return imagen;
}
 
/** crearDesdeFichero: lee desde fichero el bitmap, y actualiza
 *  su anchura y altura
 */
void ElementoGraf::crearDesdeFichero(char *nombre)
{
    imagen = load_bmp(nombre, NULL);
    if (!imagen) {
        set_gfx_mode(GFX_TEXT, 0, 0, 0, 0);
        allegro_message("No se ha podido abrir la imagen\n");
        exit( 1 );
    }
}
 
 
 
/** colisionCon: devuelve si hay colision del ElementoGraf con otro
 */
bool ElementoGraf::colisionCon(ElementoGraf e2)
{
    return colisionCon(e2.posX, e2.posY, e2.anchura, e2.altura);
}
 
 
/** colisionCon: devuelve si hay colision del ElementoGraf con
 *  un rectangulo dado por sus coordenadas
 */
bool ElementoGraf::colisionCon(int x, int y, int ancho, int alto)
{
    if ((this->posX+this->anchura > x)
        && (this->posY+this->altura > y)
        && (x+ancho > this->posX)
        && (y+alto > this->posY))
      return true;
    else
      return false;
}
 

Y el cuerpo del programa, a pesar de que todavía tiene partes repetitivas (como eso de que haya una variable x para el personaje, pero también otra xEnemigo y otra xSuelo), ya ocupa unas 60 líneas menos:

/*------------------------------*/
/*  Intro a la programac de     */
/*  juegos, por Nacho Cabanes   */
/*                              */
/*    miner02.cpp               */
/*                              */
/*  Ejemplo:                    */
/*    Primer acercamiento a     */
/*    "MiniMiner"               */
/*                              */
/*  Comprobado con:             */
/*  - DevC++ 4.9.9.2(gcc 3.4.2) */
/*    y Allegro 4.03 - WinXP    */
/*------------------------------*/
 
#include "Hardware.h"
#include "ElementoGraf.h"
 
/* -------------- Constantes globales ------------- */
#define ANCHOPANTALLA 640
#define ALTOPANTALLA 480
 
/* -------------- Variables globales -------------- */
PALETTE pal;
/*BITMAP *personaje;
BITMAP *enemigo;
BITMAP *presentacion;
BITMAP *fragmentoSuelo;
BITMAP *pantallaOculta;*/
Hardware hard;
ElementoGraf personaje, enemigo, presentacion, fragmentoSuelo;
 
int partidaTerminada;
int x = 200;
int y = 200;
int incrX = 4;
int incrY = 4;
int tecla;
int xEnemigo = 500;
int incrXEnemigo = 2;
int yEnemigo = 200;
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))
     moverDerecha();
  else if (hard.comprobarTecla(TECLA_IZQD))
     moverIzquierda();
}
 
 
// -- Intenta mover el personaje hacia la derecha
void moverDerecha() {
  x += incrX;
}
 
 
// -- Intenta mover el personaje hacia la izquierda
void moverIzquierda() {
  x -= incrX;
}
 
 
// -- Mover otros elementos del juego 
void moverElementos() {
  moverEnemigo();
}
 
 
// -- 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();
  dibujarFondo();
  enemigo.moverA(xEnemigo, yEnemigo);
  hard.dibujarOculta(enemigo);
  personaje.moverA(x, y);
  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 juego, sino de 
// funcionamiento interno de otros componentes
 
// -- Pantalla de presentacion
void lanzarPresentacion() {
  presentacion.moverA(0,0);
  hard.dibujarOculta(presentacion);
  hard.visualizarOculta();
  hard.esperarTecla();
}
 
// -- Mover el enemigo a su siguiente posicion
void moverEnemigo() {
  xEnemigo += incrXEnemigo;
  // Da la vuelta si llega a un extremo
  if ((xEnemigo > ANCHOPANTALLA-30) || (xEnemigo < 30))
    incrXEnemigo = -incrXEnemigo;
}
 
// -- Dibuja el fondo (por ahora, apenas un fragmento de suelo)
void dibujarFondo() {
  int i;
  int anchoImagen = 16;
  for (i=0; i<15; i++)
  {
      fragmentoSuelo.moverA(i*anchoImagen, ySuelo);
      hard.dibujarOculta(fragmentoSuelo);
  }
}
 
/* -------------- Rutina de inicializacin -------- */
int inicializa()
{
    hard.inicializar(640,480);
 
    personaje.crearDesdeFichero("personaje.bmp");
    enemigo.crearDesdeFichero("enemigo.bmp");
    fragmentoSuelo.crearDesdeFichero("suelo.bmp");
    presentacion.crearDesdeFichero("miner.bmp");
 
   // 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();