45. Introducción a los Scrolls.

Un juego de scroll es aquel en el que no se pasa de una pantalla con fondo estático a otra pantalla con fondo estático, sino que el fondo se va desplazando a la vez que nuestro personaje.

Según las características del juego, podemos necesitar un scroll horizontal (como el famoso "Mario"), o un scroll vertical (en muchos juegos "matamarcianos", como "Xevious"), o un scroll multidireccional, cuando el personaje se puede mover en cualquier dirección (habitual en juegos de estrategia, en los que no vemos todo el terreno).

Por supuesto, existen sistemas de scroll más complejos. Por ejemplo un "scroll parallax" horizontal muestra distintas capas de imagen, que se mueven a distinta velocidad, para dar una mayor sensación de profundidad (las montañas lejanas se mueven despacio, las montañas cercanas se mueven algo más rápido y los objetos de primer plano se mueven aún más deprisa).

Algunos sistemas permiten definir modos gráficos en los que la memoria de pantalla es más grande que la pantalla visible, y podemos mostrar sólo parte de esa memoria de pantalla, lo que serviría a hacer un scroll de forma sencilla con la ayuda del hardware. Nosotros supondremos que no tenemos un hardware que nos pueda ayudar, y haremos un primer ejemplo de scroll horizontal, de dos maneras distintas:

  • Como primer acercamiento, usaremos una imagen más ancha que la pantalla. Según en qué posición dibujemos esa imagen, parecerá que el fondo se mueve.
  • Eso de manejar imágenes muy grandes puede suponer un gasto muy elevado en disco y en memoria, así que en un segundo acercamiento usaremos una imagen definida a partir de un mapa de tiles (casillas), que en conjunto será más ancha que la pantalla, pero formada a partir de imágenes de pequeño tamaño.


Veamos cómo hacerlo...

Vamos a partir de una imagen de 640x480 puntos, y usaremos un modo de pantalla de 320x200 puntos. En un juego real, la pantalla de juego debería ser de al menos 800x600 puntos, por lo que la imagen del "mundo" debería ser mucho mayor. Esta imagen de fondo, nuestra nave y el enemigo podrían ser así:

 

 

Todo lo que vamos a hacer supondrá que nuestra biblioteca gráfica se encarga de recortar las imágenes que se salen de la pantalla visible de forma automática (algo que se conoce como "clipping"), lo que nos permite despreocuparnos de si algo se va a ver no: nosotros nos limitamos a dibujar todo, con la tranquilidad de que queda fuera de la pantalla por completo o en parte, la visualización será correcta en todo momento. Este es el caso de Allegro y de la gran mayoría de bibliotecas gráficas, así que no es una suposición demasiado restrictiva.

Para ganar tiempo y aprovechar trabajo anterior, nuestro juego utilizará las clases Hardware y ElementoGraf, que ya habíamos usado en el Miner. An así, va a ser tan sencillo que no crearemos nuevas clases para el personaje, el enemigo, etc., sino un único fuente que creará y manipulará tres elementos gráficos: el fondo, la nave y el enemigo..


En este primer acercamiento, nuestra nave estará siempre en el centro de la pantalla:

 
nave->moverA(160, 100);
hard.dibujarOculta( *nave );
 

Y cuando pulsemos una flecha del teclado, lo que se moverá es "el mundo": al pulsar la flecha hacia la derecha, el mundo se moverá hacia la izquierda. Como nuestra nave seguirá en el centro de la pantalla, dará la impresión de que efectivamente nuestra nave se desplaza hacia la derecha:

 
if (hard.comprobarTecla(TECLA_DCHA))
{
   if (xMundo < ANCHOMUNDO-ANCHOPANTALLA)
     xMundo += incrX;
}
 

pero el mundo lo dibujaremos en "coordenadas negativas", de modo que su esquina superior izquierda no coincida con la esquina superior izquierda de la pantalla, sino que esté "más arriba", por ejemplo, en las coordenadas (-160, -100):

 
fondo->moverA(-xMundo, -yMundo);
hard.dibujarOculta( *fondo );
 

Pero... ¿y si ahora añadimos un enemigo? (o varios). Vamos a dibujar un enemigo que se mueva horizontalmente, de lado a lado. En cuanto a su movimiento, no nos preocupamos: podemos incrementar o disminuir su X y su Y com siempre. La única diferencia es que haremos que se mueva con relación al tamaño del "mundo", no de la pantalla:

 
void moverElementos() {
  xEnemigo += incrEnemigo;
  if ((xEnemigo < 30) || (xEnemigo > ANCHOMUNDO-30))
    incrEnemigo = - incrEnemigo;
}
 

pero a la hora de dibujarlo sí tendremos en cuenta su posición y la posición "del mundo" (que indica la parte de pantalla que vemos):

 
enemigo->moverA(xEnemigo-xMundo, yEnemigo-yMundo);
hard.dibujarOculta( *enemigo );
 

Con estas consideraciones (que no son muchas), el fuente completo (a falta de las clases auxiliares Hardware y ElementoGraf) podría quedar así:

/*------------------------------*/
/*  Intro a la programac de     */
/*  juegos, por Nacho Cabanes   */
/*                              */
/*    scroll.cpp                */
/*                              */
/*  Ejemplo basico de scroll    */
/*                              */
/*  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 "stdio.h"
 
/* -------------- Constantes globales ------------- */
#define ANCHOPANTALLA 320
#define ALTOPANTALLA 200
#define ANCHOMUNDO 640
#define ALTOMUNDO 480
 
/* -------------- Variables globales -------------- */
Hardware hard;
ElementoGraf *fondo;
ElementoGraf *nave;
ElementoGraf *enemigo;
 
bool partidaTerminada;
bool sesionTerminada;
int xMundo = 160, yMundo = 100;
int xEnemigo = 200, yEnemigo = 120, incrEnemigo = 2;
int incrX = 4; int incrY = 4;
int tecla;
 
// Prototipos de las funciones que usaremos
void comprobarTeclas();
void moverElementos();
void comprobarColisiones();
void dibujarElementos();
void pausaFotograma();
 
// --- 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))
  {
     if (xMundo < ANCHOMUNDO-ANCHOPANTALLA)
       xMundo += incrX;
  }
 
  if (hard.comprobarTecla(TECLA_IZQD))
  {
     if (xMundo > 0)
       xMundo -= incrX;
  }
 
  if (hard.comprobarTecla(TECLA_ABAJ))
  {
     if (yMundo < ALTOMUNDO-ALTOPANTALLA)
       yMundo += incrY;
  }
 
  if (hard.comprobarTecla(TECLA_ARRB))
  {
     if (yMundo > 0)
       yMundo -= incrY;
  }
 
 
  //if (hard.comprobarTecla(TECLA_IZQD))
  //   personaje->moverIzquierda( *nivelActual );
}
 
 
// -- Mover otros elementos del juego 
void moverElementos() {
  xEnemigo += incrEnemigo;
  if ((xEnemigo < 30) || (xEnemigo > ANCHOMUNDO-30))
    incrEnemigo = - incrEnemigo;
}
 
 
// -- Comprobar colisiones de nuestro elemento con otros, o disparos con enemigos, etc
void comprobarColisiones() {
  // Nada por ahora
}
 
 
// -- Dibujar elementos en pantalla
void dibujarElementos() {
 
  hard.borrarOculta();
 
  fondo->moverA(-xMundo, -yMundo);
  hard.dibujarOculta( *fondo );
 
  enemigo->moverA(xEnemigo-xMundo, yEnemigo-yMundo);
  hard.dibujarOculta( *enemigo );
 
  nave->moverA(160, 100);
  hard.dibujarOculta( *nave );
 
  hard.visualizarOculta();
}
 
 
// -- Pausa hasta el siguiente fotograma
void pausaFotograma() {
  // Para 25 fps: 1000/25 = 40 milisegundos de pausa
  hard.pausa(40);
}
 
 
 
/* -------------- Rutina de inicializacin -------- */
int inicializa()
{
    hard.inicializar(ANCHOPANTALLA, ALTOPANTALLA);
    nave = new ElementoGraf();
    nave->crearDesdeFichero( "nave.bmp" );
    fondo = new ElementoGraf();
    fondo->crearDesdeFichero( "fondo.bmp" );
    enemigo = new ElementoGraf();
    enemigo->crearDesdeFichero( "enemigo.bmp" );
 
    return 0;
}
 
 
 
 
/* ------------------------------------------------ */
/*                                                  */
/* -------------- Cuerpo del programa ------------- */
 
int main()
{
    // Intento inicializar
    if (inicializa() != 0)
        exit(1);
 
    buclePrincipal();
    return 0;
}
 
            /* Termino con la "macro" que me pide Allegro */
END_OF_MAIN();
 

Como es habitual, puedes descargar los fuentes completos y las imágenes, en un fichero ZIP (pero esta vez no incluye proyecto de Dev-C++, sólo el fichero "compila.sh" para compilar en Linux, y el ejecutable ya creado para este sistema).