34. Añadiendo funcionalidades a "MiniMiner" (1): chocar con el fondo, saltar. (*)

En primer lugar, vamos a comprobar colisiones con el fondo, para que nuestro personaje se detenga cuando llegue a un obstáculo.

Nos puede bastar con añadir un método "esPosibleMover", que, a partir de las coordenadas de la nueva posición del personaje, nos digan si corresponden a una casilla "pisable" de nuestro mapa de juego. Basta con convertir de coordenadas de pantalla a coordenadas de mapa (restando el margen y dividiendo entre el ancho o el alto según corresponda), y luego comprobar qué símbolo aparecía en esa posición del mapa:

bool Nivel::esPosibleMover(int x1, int y1, int x2, int y2)
{
    int xCasilla1 = (x1 - margenIzq) / anchoImagen;
    int yCasilla1 = (y1 - margenSup) / altoImagen;
    int xCasilla2 = (x2 - margenIzq) / anchoImagen;
    int yCasilla2 = (y2 - margenSup) / altoImagen;
 
    if (mapa[yCasilla1][xCasilla1] != ' ')
        return false;
 
    if (mapa[yCasilla1][xCasilla2] != ' ')
        return false;
 
    if (mapa[yCasilla2][xCasilla2] != ' ')
        return false;
 
    if (mapa[yCasilla2][xCasilla1] != ' ')
        return false;
 
    return true;
}

Y entonces el personaje, antes de moverse a derecha o izquierda, deberá comprobar si es posible moverse a la nueva posición en la que debería quedar:

void Personaje::moverDerecha(Nivel n) {
    if (n.esPosibleMover(posX+4, posY, 
        posX+anchura+4, posY+altura))
        posX += 4;
}
 

(ese planteamiento no es de todo fiable: comprobamos si colisiona alguna de las esquinas del personaje; en un personaje "alargado" como el nuestro, puede chocar también la parte central... cuando salte... y eso va a ocurrir pronto).

Hacer que el personaje salte es un poco más complicado, porque supone varios cambios:

  • Por una parte, cuando haya comenzado a saltar deberá moverse solo, lo que implica que habrá un método "saltar", que comience la secuencia del salto, pero también un "mover", al igual que en el enemigo, que se encargará de continuar esa secuencia (cuando corresponda).
  • Por tanto, el cuerpo del juego deberá llamar a "saltar" cuando se pulse la tecla correspondiente, pero también a "mover" en cada pasada por la función "moverElementos" del bucle de juego.
  • Además, la secuencia de salto será más complicada que (por ejemplo) el rebote del cartel de presentación: en este juego (y en otros muchos), el personaje hace un movimiento parabólico. Vimos en el apartado 13 la forma básica de programar este tipo de movimientos, pero habrá que aplicarlo a nuestro personaje. Además, en el juego original que estamos imitando, el personaje saltaba en vertical si sólo se pulsaba la tecla de saltar, y lo hacia de forma parabólica a derecha o izquierda si se pulsa la tecla de salto junto con una de las teclas de dirección.

Una primera forma de conseguirlo sería usar realmente la ecuación de la parábola, con algo como:

void Personaje::mover(Nivel n) {
    if (! saltando)
       return;
    int xProxMov = posX + incrXSalto;
    int yProxMov = a*xProxMov*xProxMov + b*xProxMov + c;
 
    if (n.esPosibleMover(xProxMov, yProxMov, 
       xProxMov+anchura, yProxMov+altura))
    {
       posX += incrXSalto;
       posY = a*posX*posX + b*posX + c;        
    }
    else
        saltando = false;
}
 

Esa es la secuencia de salto casi completa: falta tener en cuenta que cuando el personaje deja de avanzar en forma parabolica bien porque realmente llegue a su destino o bien porque choque un obstáculo, deberíamos comprar si tiene suelo por debajo, para que caiga en caso contrario.

Los valores de a, b y c se calcularían en el momento en que se da la orden de saltar:

void Personaje::saltar() {
    if (saltando)
       return;
    saltando = true;
    xInicialSalto = posX;
    yInicialSalto = posY;
    incrXSalto = 0;
 
    // Calculo a, b y c de la parábola
    float xVerticeParabola = posX+32;
    float yVerticeParabola = posY-40;
    float pParabola = 12;
    a = 1 / (2*pParabola);
    b = -xVerticeParabola / pParabola; 
    c = ((xVerticeParabola*xVerticeParabola) / (2*pParabola) )
        + yVerticeParabola;
}

Pero esta aproximación tiene tres problemas:

  • Supone hacer muchos cálculos cada vez que se pide que el personaje salte... a pesar de que todos los saltos son iguales.
  • No es aplicable directamente a otros movimientos más complejos.
  • Al ser una función matemática, una misma coordenada X no puede corresponder a varias Y distintas. Eso supone que este sistema no sirva para movimientos circulares (por ejemplo), pero tampoco (y eso es más grave en nuestro caso) a movimientos verticales.

Por eso, podemos hacerlo de otra forma que, a pesar de que pueda parecer "menos natural", evita todos esos problemas: precalcular el movimiento, y guardar las coordenadas de cada nuevo paso en un array:

int pasosSaltoArriba[] = { -6, -6, -6, -6, -4, -4, -2, -2, 0,
                           0, 2, 2, 4, 4, 6, 6, 6, 6 };
 
void Personaje::mover(Nivel n) {
    if (saltando)
    {
 
        int xProxMov = posX + incrXSalto;
        int yProxMov = posY + pasosSaltoArriba[ fotogramaMvto ];
 
        // Si todavía se puede mover, avanzo
        if (n.esPosibleMover(xProxMov, yProxMov, 
           xProxMov+anchura, yProxMov+altura))
        {
           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, 
           posX+anchura, posY+desplazVertical+altura))
        {
           posY += desplazVertical;
        }
        else
            cayendo = false;         
    }
}

En ese ejemplo, las coordenadas verticales cambian, para dar la impresión de un movimiento que va disminuyendo su velocidad al separarse del suelo, y vuelve a aumentar a medida que vuelve a acercarse al suelo; por el contrario, las coordenadas horizontales cambian siempre en un mismo valor, que es positivo si salta hacia la derecha, negativo si salta hacia la izquierda o 0 si salta en vertical.

(Nota: esa forma de declarar el array puede dar problemas en muchos compiladores, diciendo que ya se ha definido con anterioridad el valor de "pasosSaltoArriba". Para evitarlo, podríamos definir el array (sin detallar su contenido) en el fichero de cabecera de la clase:

 
class Personaje: public ElementoGraf {
 
  public:
    ...
 
  private:
    ...
    int pasosSaltoArriba[MVTOS_SALTO];
};
 
 

y rellenar el contenido del array en el constructor):

Personaje::Personaje() {
    ...
    pasosSaltoArriba[0] = -6; pasosSaltoArriba[1] = -6;
    pasosSaltoArriba[2] = -6; pasosSaltoArriba[3] = -6;
    pasosSaltoArriba[4] = -6; pasosSaltoArriba[5] = -4;
    pasosSaltoArriba[6] = -4; pasosSaltoArriba[7] = -2;
    pasosSaltoArriba[8] = -2; pasosSaltoArriba[9] = 0;
    pasosSaltoArriba[10] = 0; pasosSaltoArriba[11] = 2;
    pasosSaltoArriba[12] = 2; pasosSaltoArriba[13] = 4;
    pasosSaltoArriba[14] = 4; pasosSaltoArriba[15] = 4;
    pasosSaltoArriba[16] = 6; pasosSaltoArriba[17] = 6;
    pasosSaltoArriba[18] = 6; pasosSaltoArriba[19] = 6;
 
}
 

La forma de hacer que caiga cuando termina el salto y no está sobre una plataforma (porque la plataforma acabe o porque choque con un obstáculo) sería:

void Personaje::mover(Nivel n) {
    [...]
    else if (cayendo)
    {
        if (n.esPosibleMover(posX, posY+desplazVertical, 
           posX+anchura, posY+desplazVertical+altura))
        {
           posY += desplazVertical;
        }
        else
            cayendo = false;         
    }
}

También podemos hacer que caiga después de andar normalmente si se termina la plataforma, simplemente añadiendo "cayendo = true;" al final de las funciones de movimiento a derecha o izquierda, de modo que tras cada paso comprobará si debe caer.

En la apariencia no hay cambios en esta versión, salvo por el hecho de que nuestro personaje se puede separar del suelo:

De paso, en la pantalla de bienvenida podemos añadir el texto "Pulse T para terminar", para que sea más amigable.

Y 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.