37. Añadiendo funcionalidades a "MiniMiner" (3): avanzar a otra pantalla. (*)

Normalmente, en un juego de plataformas como este, podremos avanzar por diferentes pantallas. Hay jugos en los que podemos avanzar de una pantalla a otra en cuanto lleguemos a un extremo, o cuando pasemos por una puerta, y otros juegos (como éste) en los que antes debemos recoger una serie de objetos para que así se abra la puerta.

Por otra parte, hay juegos en los que se nos felicita (y termina la partida) cuando recorremos todos los niveles, y otros juegos en los que tras recorrer el útimo nivel volvemos al primero y seguimos acumulando puntos.

En el caso concreto de nuestro Miner, la lógica del sistema de niveles es la siguiente:

  • Hay que superar 20 niveles, y tras completar el último se vuelve a comenzar por el primero (conservando la puntuación conseguida, claro).
  • En cada nivel hay que recoger varias "llaves". Cuando recogemos todas, se abre la puerta que nos permite pasar al siguiente nivel.
  • Si perdemos una vida (porque nos toque un enemigo, o choquemos con alguno de los obstáculos que son capaces de matarnos, o caigamos desde mucha altura), volvemos a comenzar el nivel, y todas las llaves aparecen en su posición inicial y la puerta aparece cerrada.

Esa serie de niveles se representarán típicamente como un "array" de niveles, y llevaremos cuenta del número de nivel en el que nos encontramos, para poder volver al nivel 1 cuando completemos el último. Ahora accederíamos a cada "casilla" de la pantalla no como "mapa[y][x]" sino como "mapa[nivelActual][y][x]". Así, en cuanto cambie el valor de "nivelActual", estaríamos dibujando automáticamente las casillas del nuevo nivel, y comprobando colisiones con ellas.

Otra posibilidad es tener un array de mayor tamaño que nuestra pantalla visible, y recorrer sólo la zona que nos interesa. Por ejemplo, si trabajamos con dos pantallas, podríamos tener un array de 64 columnas de ancho, de modo que de la columna 1 a la 32 representaran la primera pantalla, y de la 33 a la 64 serían para la segunda pantalla.

Otra alternativa, que es la que usaremos para este juego en concreto, es mantener en memoria sólo un array que tenga el mismo tamaño que la pantalla, y rellenarlo desde fichero con datos nuevos cada vez que cambiemos de nivel.

En cualquier caso, para cada uno de los niveles que componen el juego, deberemos diseñarlo "en papel" (si estamos haciendo un juego desde cero) o crearlo a partir del original (si estamos "imitando un clásico"). Por ejemplo, en el caso de nuestro Miner, el tercer nivel es así:

Y deberíamos descomponerlo en componentes, como ya hicimos en la entrega 4 para el primer nivel:

Como se ve en este ejemplo, es habitual que aparezcan nuevos elementos (distintos tipos de suelo, obstáculos, enemigos) a medida que avanzamos por los niveles.

Un consejo para la hora de implementar todo esto: normalmente será conveniente no modificar directamente el array cada vez que recojamos un objeto, sino trabajar sobre una copia. Así, cuando perdamos una vida (en este juego) o si volvamos a comenzar una partida (en cualquier juego), restauraremos el nivel (o todos ellos) a su estado inicial, de modo que los objetos vuelvan a aparecer en sus posiciones iniciales. Si volvemos a leer desde fichero los datos del nivel, entonces sí podríamos modificar directamente los datos del array.

Nosotros crearemos sólo dos niveles (y tras el segundo se volverá al primero), porque añadir más niveles supone emplear más tiempo, pero no debería suponer dificultad extra. Por si alguien quiere crear más niveles, éstos eran los que componían el juego original:


Vamos con ello...

Por una parte, si este nuevo nivel tiene alguna casilla que sea nueva, tenemos que preparar las imágenes y crear el mapa, que podría ser:


M    V   T     T  T    V   T   M 
M                              M 
M                              M 
M                              M 
M                              M 
MNNNNOOOOOOOOOOOOOOOOOOOOOO  OOM 
M                    V        VM 
MNNNNNN                    NNNNM 
MT                             M 
M     DDDDDD                   M 
M                        NNNNNNM 
M             NNNNN          PPM 
M    NNNNNN                  PPM 
M                    NNNNNNNNNNM 
M                              M
MNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNM 

lo que daría lugar a una pantalla de juego como esta:

(para probar esta pantalla cuando todavía estamos empezando, basta con renombrar el fichero como "nivel01.dat" en vez de "nivel02.dat", para que se cargue en primer lugar, o bien podemos dar el valor 2 a la variable "nivelActual", para que el segundo nivel sea el que se cargue en primer lugar; cuando este nivel se comporte correctamente, volvemos a dejar todo como estaba, para que una partida real comience por el primer nivel).

(Otra alternativa es prepararnos una "tecla oculta", que un usuario normal no conozca, pero que nos permita avanzar de un nivel al siguiente para poder probar todo el juego de forma rápida:)

 
if (hard.comprobarTecla(TECLA_L))
   avanzarNivel();
 

Se puede ver que nuestro mapa no es exactamente igual al original (corresponde con la tercera pantalla), pero es que nuestra versión del juego no permite (todavía) que "se hundan" ciertos fragmentos del suelo, algo que sí hacía el original, de modo que no tendríamos forma de volver a bajar desde la parte superior.

Precisamente para poder recoger la llave que hay en esa zona (en la séptima fila, entre dos plataformas, debemos hacer un par de cambios:

  • Por una parte, como hay dos plataformas muy cercanas, las rutinas de mover a derecha y a izquierda deberán comprobar sólo las colisiones con la parte inferior de nuestro personaje, o no podríamos llegar andando hasta esa llave.
 
void Personaje::moverDerecha(Nivel n) {
    if (saltando || cayendo)
       return;
    if (n.esPosibleMover(posX+desplazHorizontal, posY+altura-4, 
           posX+anchura+desplazHorizontal, posY+altura))
		posX += desplazHorizontal;
    ...
 
  • Por otra parte, la rutina encargada de ver si es posible mover a una cierta posición sólo la acepta si es un espacio en blanco. Deberemos ampliarla para que también acepte como casillas pisables las de "llave" y las de "puerta". Lo que antes era
 
if (mapa[yCasilla1][xCasilla1] != ' ')
  return false;
 

ahora se convertirá en

 
if ( ! esPisableCasilla( yCasilla1,xCasilla1 ) )
  return false;
 

donde esta función "esPisable" se podría escribir así:

 
bool Nivel::esPisableCasilla(int y, int x)
{
   if (mapa[y][x] == ' ')  // Si es espacio
      return true;
 
   if (mapa[y][x] == 'V')  // Si es llave
      return true;
 
   if (mapa[y][x] == 'P')  // Si es puerta
      return true;
 
   return false;  // Resto de casos: no pisable
}
 

Además, vamos a ampliar la clase Hardware para que nos permita escribir texto, de modo que podamos ver el número de nivel en el que estamos y la puntuación que hemos obtenido hasta ese momento. De esto se encargará la clase "Marcador":

 
void Marcador::dibujarOculta(Hardware h)
{
  // Dibujamos el número de vidas: pronto pasará a una clase "Marcador"
  for (int i = 0; i < numVidas-1; i++)
  {
    imagenVidas->moverA(10 + i*20, 400);
    h.dibujarOculta( *imagenVidas );
  }
 
  h.escribirOculta("Nivel:", 10,330, 200,200,200);
  h.escribirOculta(numNivelActual, 110,330, 200,200,200);      
  h.escribirOculta("Puntos:", 10,350, 200,200,200);
  h.escribirOculta(puntos, 110,350, 200,200,200);      
}
 

Y a su vez, la clase principal del juego sería la que indicase esta información al marcador cuando cambien los puntos o el nivel:

 
void comprobarColisiones() {
  // Colisiones de personaje con fondo: obtener puntos o perder vida
  int puntosMovimiento = nivelActual->obtenerPuntosPosicion(
    personaje->leerX(), 
    personaje->leerY(),
    personaje->leerX()+personaje->leerAnchura(),
    personaje->leerY()+personaje->leerAltura());
 
  // Si realmente ha recogido un objeto, sumamos los puntos en el juego
  if (puntosMovimiento > 0)
  {
	puntos += puntosMovimiento;
	marcador->indicarPuntos(puntos);
  }
  ...
 

Por otra parte, para que se pueda pasar de un nivel a otro, debemos llevar cuenta de cuantas llaves nos quedan por recoger. Si se han recogido todas y tocamos la puerta, debemos avanzar de nivel. Podemos hacer que esa situación nos permita obtener 50 puntos, y que además estos 50 puntos sirvan de aviso a cuerpo del juego de que debemos avanzar de nivel: En la clase Nivel haríamos

 
// Si toca la puerta y no quedan llaves, 50 puntos
if ( ((mapa[yCasilla1][xCasilla1] == 'P')
   || ... )
   && (llavesRestantes == 0) )
    return 50;
 

Como es habitual, aquí 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.