13. Un poco de matemáticas para juegos. Sexto Juego: TiroAlPlato.

Siempre que queramos imitar fenómenos físicos, necesitaremos tener unos ciertos conocimientos de matemáticas, saber qué fórmulas nos permitirán representar esos fenómenos de una forma creíble. De momento veremos unas primeras operaciones básicas (distancias, círculos, parábolas y su utilización); otras operaciones algo más avanzadas las dejamos para más adelante (por ejemplo, la forma de representar figuras 3D y de rotarlas).

Distancias . La ecuación que nos da la distancia entre dos puntos procede directamente del teorema de Pitágoras, aquello de "el cuadrado de la hipotenusa es igual a la suma de los cuadrados de los catetos". Si unimos dos puntos con una línea recta, podríamos formar un triángulo rectángulo: su hipotenusa sería la recta que une los dos puntos, el tamaño de un cateto sería la diferencia entre las coordenadas horizontales (x) de los puntos, y el tamaño del otro cateto sería la diferencia entre las coordenadas verticales (y), así:

Por tanto, la forma de hallar la distancia entre dos puntos sería

d = raíz (  (x2 - x1)2 + (y2 - y1)2 )

Así, en nuestro último juego (puntería) podríamos haber usado un blanco que fuera redondo, en vez de rectangular. Comprobaríamos si se ha acertado simplemente viendo si la distancia desde el centro del círculo hasta el punto en el que se ha pulsado el ratón es menor (o igual) que el radio del círculo.

Círculo . La ecuación que nos da las coordenadas de los puntos de una circunferencia a partir del radio de la circunferencia y del ángulo en que se encuentra ese punto son

x = radio * coseno (ángulo)
y = radio * seno (ángulo)

- Nota: eso es en el caso de que el centro sea el punto (0,0); si no lo fuera, basta con sumar a estos valores las coordenadas x e y del centro del círculo -

Normalmente no necesitaremos usar estas expresiones para dibujar un círculo, porque casi cualquier biblioteca de funciones gráficas tendrá incluidas las rutinas necesarias para dibujarlos. Pero sí nos pueden resultar tremendamente útiles si queremos rotar un objeto en el plano. Lo haremos dentro de poco (y más adelante veremos las modificaciones para girar en 3D). 

Parábola . La forma general de un parábola es y = ax2 + bx + c. Los valores de a, b, y c dependen de cómo esté de desplazada la parábola y de lo "afilada" que sea. La aplicación, que veremos en la práctica en el próximo ejemplo, es que si lanzamos un objeto al aire y vuelve a caer, la curva que describe es una parábola (no sube y baja a la misma velocidad en todo momento):




Eso sí, al menos un par de comentarios:

  • En "el mundo real", una parábola como la de la imagen anterior debe tener el coeficiente "a" (el número que acompaña a x2) negativo; si es un número positivo, la parábola sería "al revés", con el hueco hacia arriba. Eso sí, en la pantalla del ordenador, las coordenadas verticales (y) se suelen medir de arriba a abajo, de modo que esa ecuación será la útil para nosotros, tal y como aparece.
  • Nos puede interesar saber valores más concretos de "a", "b" y "c" en la práctica, para conseguir que la parábola pase por un cierto punto. Detallaré sólo un poco más:
Una parábola que tenga su vértice en el punto (x1, y1) y que tenga una distancia "p" desde dicho vértice hasta un punto especial llamado "foco" (que está "dentro" de la parábola, en la misma vertical que el vértice, y tiene ciertas propiedades que no veremos) será:

(x-x1)2 = 2p (y-y1)

Podemos desarrollar esta expresión y obtener cuanto tendría que ser la "a", la "b" y la "c" a partir de las coordenadas del vértice (x1, y1) y de la distancia "p" (de la que sólo diré que cuanto menor sea p, más "abierta" será la parábola):

a = 1 / 2p
b = -x1 / p
c = (x1 2 / 2p ) + y1

¿Un ejemplo de cómo se usa esto? Claro, en el siguiente juego...

13b. Sexto Juego: TiroAlPlato.

Ahora ya sabemos cómo utilizar el ratón, como medir el tiempo y conocemos algunas herramientas matemáticas sencillas. Con todo esto, podemos mejorar el juego de puntería, hacerlo "más jugable". Ahora los blancos estarán en movimiento, siguiendo una curva que será una parábola, y serán circulares. La puntuación dependerá del tiempo que se tarde en acertar. Habrá un número limitado de "platos", tras el cual se acabará la partida.

Con todo esto, la mecánica del juego será:

  Inicializar variables
  Repetir para cada plato:
     Dibujar plato a la izqda de la pantalla
     Repetir
       Si se pulsa el ratón en plato
         Aumentar puntuación
       Si no, al cabo de un tiempo
         Calcular nueva posición del plato
         Redibujar
     Hasta que el plato salga (dcha) o se acierte
  Hasta que se acaben los platos
  Mostrar puntuación final

No suena difícil, ¿no?

La apariencia (todavía muy sobria) podría ser

Y el fuente podría ser así:

 

/*------------------------------*/ 
/*  Intro a la programac de     */ 
/*  juegos, por Nacho Cabanes   */ 
/*                              */ 
/*    ipj13c.c                  */ 
/*                              */ 
/*  Decimotercer ejemplo: juego */ 
/*    de "Tiro Al Plato"        */ 
/*                              */ 
/*  Comprobado con:             */
/*  - Djgpp 2.03 (gcc 3.2)      */
/*    y Allegro 4.02 - MsDos    */
/*  - MinGW 2.0.0-3 (gcc 3.2)   */
/*    y Allegro 4.02 - Win      */
/*  - DevC++ 4.9.9.2(gcc 3.4.2) */
/*    y Allegro 4.03 - Win XP   */
/*------------------------------*/ 
 
#include <stdlib.h>         // Para "rand"
#include <math.h>           // Para "sqrt"
#include <allegro.h>
 
 
/* -------------- Constantes globales ------------- */
#define ANCHOPANTALLA 320
#define ALTOPANTALLA 200
#define MAXRADIODIANA 25
#define MINRADIODIANA 5
#define NUMDIANAS 12
#define MAXINCREMXDIANA 20
#define MININCREMXDIANA 10
#define RETARDO 7
 
 
/* -------------- Variables globales -------------- */
int 
   TamanyoDianaActual,
   numDianaActual,
   posXdiana,
   posYdiana,
   radioDiana,
   incremXdiana,
   incremYdiana,
   acertado = 0;  // Si se acierta -> plato nuevo
 
long int 
   puntos   = 0,
   contadorActual = 0;
 
float   
   a,b,c;         // Para la parbola del plato
 
 
/* -------------- Rutina de inicializacin -------- */
int inicializa()
{
    allegro_init();        // Inicializamos Allegro
    install_keyboard();
    install_timer();
    install_mouse();
 
                           // Intentamos entrar a modo grafico
    if (set_gfx_mode(GFX_SAFE, ANCHOPANTALLA, ALTOPANTALLA, 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);
        return 1;
    }
 
    // Si he podido entrar a modo grfico, 
    // ahora inicializo las variables
    numDianaActual = 1;
    srand(time(0));
    show_mouse(screen);
 
    // Y termino indicando que no ha habido errores
    return 0;
}
 
 
/* -------------- Rutina de nuevo plato ----------- */
void nuevoPlato()
{
    int xVerticeParabola, 
        yVerticeParabola;
    float pParabola;
 
    // Un radio al azar entre el valor mximo y el mnimo
    radioDiana = (rand() % (MAXRADIODIANA - MINRADIODIANA))
        + MINRADIODIANA;
    // La velocidad (incremento de X), similar
    incremXdiana = (rand() % (MAXINCREMXDIANA - MININCREMXDIANA))
        + MININCREMXDIANA;
 
    // Vrtice de la parbola, cerca del centro en horizontal
    xVerticeParabola = ANCHOPANTALLA/2 + (rand() % 40)  - 20;
    // Y mitad superior de la pantalla, en vertical
    yVerticeParabola = (rand() % (ALTOPANTALLA/2));
 
 
    // Calculo a, b y c de la parbola
    pParabola = ALTOPANTALLA/2;
    a = 1 / (2*pParabola);
    b = -xVerticeParabola / pParabola; 
    c = ((xVerticeParabola*xVerticeParabola) / (2*pParabola) )
        + yVerticeParabola;
 
 
    // Posicin horizontal: junto margen izquierdo
    posXdiana = radioDiana;
    // Posicin vertical: segn la parbola
    posYdiana = 
        a*posXdiana*posXdiana +
        b*posXdiana +
        c;    
}
 
 
/* -------------- Rutina de redibujar pantalla ---- */
void redibujaPantalla()
{
    // Oculto ratn
    scare_mouse();
    // Borro pantalla
    clear_bitmap(screen);
    // Sincronizo con barrido para menos parpadeos
    vsync(); 
 
    // Y dibujo todo lo que corresponda
    rectfill(screen,0,0,ANCHOPANTALLA,ALTOPANTALLA-40,
        makecol(70, 70, 255));   //Cielo
    textprintf(screen, font, 4,4, palette_color[13], 
        "Puntos: %d", puntos);   // Puntuacin    
    rectfill(screen,0,ALTOPANTALLA-40,ANCHOPANTALLA,ALTOPANTALLA,
        makecol(0, 150, 0));     //Suelo
    circlefill(screen, 
        posXdiana, posYdiana, radioDiana,
        palette_color[15]);      // Diana
    if (numDianaActual <= NUMDIANAS) {
        textprintf(screen, font, 4,190, palette_color[13], 
            "Platos: %d", NUMDIANAS-numDianaActual); 
    }                            // Restantes, si no acab
 
    unscare_mouse();
}
 
 
/* -------------- Distancia entre dos puntos ------ */
float distancia(int x1, int x2, int y1, int y2) {
    return (sqrt((x1-x2)*(x1-x2) + (y1-y2)*(y1-y2)) );
}
 
 
/* -------------- Rutinas de temporizacin ---- */
volatile long int contador = 0;
 
void aumentaContador(void) {  contador++;  }
END_OF_FUNCTION(aumentaContador);
 
 
 
/* ------------------------------------------------ */
/*                                                  */
/* -------------- Cuerpo del programa ------------- */
 
int main()
{
 
    // Intentamos inicializar
    if (inicializa() != 0)
        exit(1);
 
    // Bloqueamos la variable y la funcin del temporizador
    LOCK_VARIABLE( contador );
    LOCK_FUNCTION( aumentaContador );
 
    // Y ponemos el temporizador en marcha: cada 10 milisegundos
    install_int(aumentaContador, 10);    
 
 
    do {  // Parte que se repite para cada plato
 
        nuevoPlato();        // Calculo su posicin inicial
        redibujaPantalla();  // Y dibujo la pantalla
        acertado = 0;        // Todava no se ha acertado, claro
 
        do { // Parte que se repite mientras se mueve
 
            // Compruebo el ratn
            if (mouse_b & 1) {        
                if (distancia(mouse_x, posXdiana, mouse_y,posYdiana)
                        <= radioDiana) {
                    puntos += ANCHOPANTALLA-posXdiana;
                    acertado = 1;
                }
            }
 
            // Si ya ha pasado el retardo, muevo
            if (contador >= contadorActual+RETARDO) {
                contadorActual = contador+RETARDO;
                posXdiana += incremXdiana;
                posYdiana = 
                    a*posXdiana*posXdiana +
                    b*posXdiana +
                    c;
                redibujaPantalla();
            }
 
        } while ((posXdiana <= ANCHOPANTALLA - radioDiana)
            && (acertado == 0));
 
        numDianaActual ++;  // Siguiente diana
 
    } while (numDianaActual <= NUMDIANAS);
 
 
    redibujaPantalla();
    scare_mouse();
    textprintf(screen, font, 40,100, palette_color[15], 
        "Partida terminada"); 
    unscare_mouse();
    readkey();
    return 0;
 
}
 
                     /* Termino con la "macro" que me pide Allegro */
END_OF_MAIN();