36. Añadiendo funcionalidades a "MiniMiner" (2): mejora del movimiento, recoger objetos (*)

En la séptima entrega de nuestro Miner vimos cómo hacer un movimiento de salto, tanto vertical como parabólico. Es un tipo de movimiento aplicable a muchos juegos de plataformas, pero que no funciona bien en nuestro caso, por un par de detalles:

  • El personaje es más alto que las casillas del fondo, de modo que si comprobamos sólo las 4 esquinas del personaje, se nos pueden escapar choques con la parte central del cuerpo, y eso da lugar a efectos no deseados cuando saltamos cerca de un plataforma.
  • No hacemos realmente lo mismo que en el juego original: nuestro personaje se para en cuanto toca una casilla del fondo (exceptuando el fallo indicado en el punto anterior), pero en el juego original, podía seguir subiendo si saltaba cuando estaba delante de una plataforma roja, y se detenía si chocaba con ella al caer (pero no al subir). En cambio, los ladrillos sí detienen su movimiento por completo.

Vamos a intentar mejorar ese comportamiento, y, de paso, recoger objetos en la pantalla...


Eso de que el personaje de comporte de forma incorrecta cuando choca con algo en medio salto se debe, como ya hemos comentado, a que el personaje es mucho más alto que las casillas del fondo, y se puede solucionar de dos formas:

  • Comprobando todas las 512 casillas del fondo (16 filas x 32 columnas) para ver si alguna de ellas colisiona con nuestro personaje. Esta solución es muy fácil de programar, pero hay alternativas más rápidas.
  • Comprobando qué casillas del fondo coinciden con las 4 esquinas del personaje, como hicimos en la entrega 7. Como el personaje es mucho más alto que el fondo, puede ser conveniente mirar los puntos que están también a media altura (con una y que llamaremos "yCasilla3", intermedia entre la "yCasilla1" y la "yCasilla2"). Esto supone comparar 6 posiciones, en vez de 512, por lo que es una alternativa mucho más rápida, que se podría implementar así:
bool Nivel::esPosibleMover(int x1, int y1, int x2, int y2)
{
	// Coordenadas de las esquinas del personaje
	int xCasilla1 = (x1 - margenIzq) / anchoImagen;
	int yCasilla1 = (y1 - margenSup) / altoImagen;
	int xCasilla2 = (x2 - margenIzq) / anchoImagen;
	int yCasilla2 = (y2 - margenSup) / altoImagen;
	// Tambien zona a media altura, porque el personaje es mucho 
	// más alto que las casillas del fondo
	int yCasilla3 = (yCasilla1 + yCasilla2) / 2;
 
	// En vez de comprobar todas las casillas del fondo,
	// miro solo las que están en las esquinas el personaje
	if (mapa[yCasilla1][xCasilla1] != ' ')
		return false;
 
	if (mapa[yCasilla1][xCasilla2] != ' ')
		return false;
 
	if (mapa[yCasilla2][xCasilla2] != ' ')
		return false;
 
	if (mapa[yCasilla2][xCasilla1] != ' ')
		return false;
 
	// Y las zonas a media altura, porque el personaje es alto
	if (mapa[yCasilla3][xCasilla2] != ' ')
		return false;
 
	if (mapa[yCasilla3][xCasilla1] != ' ')
		return false;
 
	return true;
}


Para que choque sólo cuando caiga y no cuando suba, podemos hacer dos cosas:

  • Por una parte, podemos tener una variable "booleana" llamada "subiendo", que tenga valor "true" cuando el incremento de Y sea positivo. De este modo, si "subiendo" es verdad, no deberíamos comprobar colisiones. (Siendo estrictos, en el juego original no es que no se compruebe colisiones mientras sube, sino que sólo se comprueba con ciertos elementos -los ladrillos- y cuando cae se comprueban colisiones con más elementos -ladrillos y todos los tipos de suelo-, pero nosotros no vamos a hilar tan fino, o al menos no por ahora).
  • Por otra parte, lo que nos interesa a efectos de movimiento es si chocan "sus pies": si su cabeza roza una plataforma mientras cae, supondremos que la plataforma queda por detrás del personaje y que no lo frena, sino que sigue cayendo. Una forma de conseguirlo puede ser no mirar si colisiona toda la superficie del personaje, sino sólo la parte inferior (una zona de, por ejemplo, 4 píxeles de alto):
void Personaje::mover(Nivel n) {
    // Tamaño vertical: personaje + 20% aprox
    // Tamaño horizontal: 4.5 casillas aprox
    if (saltando)
    {
 
        int xProxMov = posX + incrXSalto;
        int yProxMov = posY + pasosSaltoArriba[ fotogramaMvto ];
        bool subiendoSalto = (pasosSaltoArriba[ fotogramaMvto ] < 0);
 
        // Si todavía se puede mover, avanzo
        if (n.esPosibleMover(xProxMov, yProxMov+altura-4, 
           xProxMov+anchura, yProxMov+altura)
           || subiendoSalto )
        {
           posX = xProxMov;
           posY = yProxMov;        
        }
        // Y si no, quizá esté cayendo
        else
        {
            saltando = false;
            cayendo = true;
        }
 
        fotogramaMvto ++;    
        if (fotogramaMvto >= MVTOS_SALTO)
        {
           saltando = false;
           cayendo = true;
        }
    }
    else if (cayendo)
    {
        if (n.esPosibleMover(posX, posY+desplazVertical+altura-4, 
           posX+anchura, posY+desplazVertical+altura))
        {
           posY += desplazVertical;
        }
        else
            cayendo = false;         
    }
}


Finalmente, para recoger objetos, basta comprobar colisiones con otros objetos del fondo, de tipos distintos: en vez de ser ladrillos o fragmentos de suelo, serán llaves. Por tanto, la rutina de ver si hay algún objeto que recoger será muy similar a la "esPosibleMover".

De hecho, en el juego original también hay objetos que nos matan si chocamos con ellos. Son como "estalactitas", que nos hacen perder una vida si las rozamos al saltar. Para controlar ambos casos a la vez (objetos a recoger y objetos que nos maten), podríamos hacer una única rutina "obtenerPuntos" que, a partir de unas coordenadas, devuelva 0 si no hay nada, un valor positivo (por ejemplo, 10) si hay una llave, o un valor negativo (como -1) si hay una estalactita que nos haría perder una vida.

Esta rutina podría ser así:

int Nivel::obtenerPuntosPosicion(int x1, int y1, int x2, int y2)
{
	// V indica una llave en el mapa (10 puntos)
	// T indica un trozo de techo que nos mata
	//     (estalactita, -1 puntos)
 
 
	int xCasilla1 = (x1 - margenIzq) / anchoImagen;
	int yCasilla1 = (y1 - margenSup) / altoImagen;
	int xCasilla2 = (x2 - margenIzq) / anchoImagen;
	int yCasilla2 = (y2 - margenSup) / altoImagen;
	// Tambien zona a media altura, como en esPosibleMover
	int yCasilla3 = (yCasilla1 + yCasilla2) / 2;
 
	// Primero veo si choca con una estalactita
	// (puntuacion -1: perder vida)
	if ((mapa[yCasilla1][xCasilla1] == 'T') 
	   || (mapa[yCasilla1][xCasilla2] == 'T')
	   || (mapa[yCasilla2][xCasilla2] == 'T')
	   || (mapa[yCasilla2][xCasilla1] == 'T')
	   || (mapa[yCasilla3][xCasilla2] == 'T')
	   || (mapa[yCasilla3][xCasilla1] == 'T'))
		return -1;
 
	// Despues veo si toca una llave
	// (puntuacion 10, y se borra la llave)
  if (mapa[yCasilla1][xCasilla1] == 'V')
	{
		fragmentoNivel[yCasilla1][xCasilla1] = imagenFondo;
		mapa[yCasilla1][xCasilla1] = ' ';
		return 10;
	}
 
	if (mapa[yCasilla1][xCasilla2] == 'V')
	{
		fragmentoNivel[yCasilla1][xCasilla2] = imagenFondo;
		mapa[yCasilla1][xCasilla2] = ' ';
		return 10;
	}
 
	if (mapa[yCasilla2][xCasilla2] == 'V')
	{
		fragmentoNivel[yCasilla2][xCasilla2] = imagenFondo;
		mapa[yCasilla2][xCasilla2] = ' ';
		return 10;
	}
 
	if (mapa[yCasilla2][xCasilla1] == 'V')
	{
		fragmentoNivel[yCasilla2][xCasilla1] = imagenFondo;
		mapa[yCasilla2][xCasilla1] = ' ';
		return 10;
	}
 
	if (mapa[yCasilla3][xCasilla2] == 'V')
	{
		fragmentoNivel[yCasilla3][xCasilla2] = imagenFondo;
		mapa[yCasilla3][xCasilla2] = ' ';
		return 10;
	}
 
	if (mapa[yCasilla3][xCasilla1] == 'V')
	{
		fragmentoNivel[yCasilla3][xCasilla1] = imagenFondo;
		mapa[yCasilla3][xCasilla1] = ' ';
		return 10;
	}
 
	// Si no ha pasado nada de lo anterior, no hay puntos que obtener	
	return 0;
}
 
 

Y su uso desde el programa principal sería:

void comprobarColisiones() {
  // Colisiones de personaje con fondo: obtener puntos o perder vida
  int puntosMovimiento = primerNivel->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;
 
  // Y si es -1, ha chocaco con el fondo: igual caso que las
  // colisiones de personaje con enemigo: recolocar y perder vida
  if (personaje->colisionCon( *enemigo ) 
    || (puntosMovimiento < 0) )
    {
    ...

Como casi siempre, 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.