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:
(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();