theDarkDesigns (c) 2003
Detección de colisión en tile based games
(continuación del tutorial escrito por Silencer) Por Leandro Pelorosso (Wolfgang)
 

Este tutorial procurará ser la segunda parte del creado por Silencer de theDarkDesigns sobre Tile based games. El mismo dejó inconclusa una parte fundamental de todo juego: el jugador y la colisión.
Este será el primer tutorial que escribo, asi que disculpen si no llega a ser del todo entendible o agradable, prometo hacer mi mejor esfuerzo.

Antes de comenzar con los bloques, vamos a hablar un rato del mapa. Es altamente recomendable haber leído anteriormente el tutorial citado.
Nuestro  mapa es en realidad una matriz, una grilla, en cuyas componentes se me dice que clase de bloque hay, en caso de que haya uno, por ejemplo:

Donde los '1' representan un bloque fisico, digno de ser contemplado en la colisión, y los '0' bloques vacios.
Entonces, tenemos que la grilla es una matriz, de, por ejemplo, chars.

char mapa[MAP_HEIGHT][MAP_WIDTH]; // el mapa!

donde MAP_HEIGHT y MAP_WIDTH serán enteros, indicando el ancho del mapa. Definimos entonces:

const MAP_HEIGHT =16;//Alto en bloques del mapa
const MAP_WIDTH =20; //Ancho en bloques del mapa

Bien, vamos a suponer que ya tenemos el mapa almacenado en la matriz, (cosa explicada en el tutorial anterior, si tenes dudas hechale un ojo al fuente de ejemplo o consultame, es muy sencillo en verdad)
Comenzaremos viendo como incorporar a un personaje en la escena, y luego veremos que hacemos con la colisión.
Para almacenar los datos del juegador, utilizaremos una estructura con algunos miembros. Pero como uso algunas estructuras a modo de variables, las defino  antes:

struct point2f
{
float x,y;
};
-------------
struct point2i
{
int x,y;
};
-------------
struct datablock
{
point2i _pos;
};

Ahora si, definimos la estrucuta del jugador.

struct s_player //estructura jugador
{
point2f pos; // la posicion de nuestro cadrado jugador en pantalla
point2f center; //el centro de nuestro cuadrado del juegador
float p_dX,p_dY;  //desplazamientos, ya veran para que son :p
point2i in_matrix; //la posicion de nuestro jugador en la matriz
datablock data_block[8]; //numero de bloques que tenemos alrededor
float speed; //velocidad de nuestro jugador
};

Ya todo tendrá mucho sentido :) .
Como seguro estarás pensando, el jugador es simplemente un bloquecito mas, que lo dibujamos aparte, entonces para graficarlo es muy facil, es como cuando dibujas el mapa. Mirá:

drawQuad(player.pos.x, player.pos.y,b_width,b_height);

Definiendo anteriormente:
float b_width = 16; //Ancho en pixels del bloque
float b_height = 15; //Alto en pixels del bloque

Si estas pensando : "¿y este de donde sacó esta porquería?, Estas en tu derecho. drawQuad es una función que dibuja un bloque en la posición indicada por los primeros dos parametros, y del ancho y alto indicado en pixeles en los dos parametros restantes. Es importande decir que player.pos.x y player.pos.y corresponden a la esquina superior izquierda del jugador. Para que quede mas claro:


- "wiii!, ya tenemos dibujado al personaje en la escena...."
- "pero che.... no se mueve, ¿de que me sirve así?"
- "es muy cierto eso.. ahi vamos!"

Ahora que tenemos al personaje en la escena, necesitamos ver como hacemos para manejar la entrada de datos, vamos a usar eventos similares a key_down  y key_up en Visual basic. ( si estas programando en VB, usá justamente esos).

void pressKey(int key, int x, int y)  //equivalente a key_down en VB.
{
  switch (key)  //aca vemos que apretamos.
    {
      case GLUT_KEY_LEFT : key_left=1;break; //si apretamos para la izquierda
      case GLUT_KEY_RIGHT : key_right=1;break; //si apretamos para la derecha
      case GLUT_KEY_UP : key_up=1;break; //si apretamos para arriba
      case GLUT_KEY_DOWN : key_down=1;break; //si apretamos para abajo
    }
}

Lo que dice aqui es basicamente esto:  si apretaste la flechita para la izquierda, entonces, seteamos key_left en 1, si apretaste la de la derecha, entonces ponemos key_right en 1, y así con las demas.
Si estas programando en Visual Basic, Lo que tenes que haces es, en el evento Key_down, hacer cuatro if, o un select case, por ejemplo :

if KeyCode = vbKeyLeft Then key_left =1
if KeyCode = vbKeyRight Then key_right =1
y asi con los demás.

Ahora, para saber que ya no estamos apretando la tecla, es basicamente lo mismo:

void releaseKey(int key, int x, int y)  //equivalente al key_up del VB
{
 switch (key)
    {
      case GLUT_KEY_LEFT : key_left=0;break; //si dejamos de apretar la izquierda
      case GLUT_KEY_RIGHT : key_right=0;break; //si dejamos de apretar la derecha
      case GLUT_KEY_UP : key_up=0;break; //si dejamos de apretar para arriba
      case GLUT_KEY_DOWN : key_down=0;break; //si dejamos de apretar para abajo
    }
}

Si estas programando en VB, pones el codigo en el evento key_up:

if KeyCode = vbKeyLeft Then key_left =0
if KeyCode = vbKeyRight Then key_right =0
y asi con los demás.

- "genial, sabemos que tecla se esta apretando, y?"
- "ya, ya, ya casi hacemos que se mueva"

Ok. Ya sabemos que tecla de desplazamiento se apretó. Ahora, comenzamos a mover a nuestro jugador.

 // seteamos las variables de desplazamiento a cero
 player.p_dX=0;
 player.p_dY=0;

 // dependiendo de lo que apretemos cambiamos los desplazamientos
 if(key_left==1)  player.p_dX=-player.speed;
 if(key_right==1) player.p_dX=+player.speed;
 if(key_up==1)    player.p_dY=-player.speed;
 if(key_down==1)  player.p_dY=+player.speed;

- "Pero este chabon me esta re jodiendo!, me tira el codigo asi nomas!, yo no entiendo nada"
- "Bancá loco! (pateo la mesa), paso a explicar esta porqueria :)"

Lo que estamos haciendo primeramente es setear los desplazamientos de nuestro jugador en las coordenadas X e Y en 0. Según la tecla que esté apretando, hacemos que este desplazamiento tome como valor la velocidad asignada al jugador, tomando el signo según corresponda mover hacia la izquierda (-) o  derecha (+)  en coordenada X, o bien arriba (-) o abajo (+) en coordenada Y.
Fijate que no estamos cambiando la posicion del jugador, solo estamos cambiando su desplazamiento en los ejes. Luego sumaremos a la posicion X e Y sus correspondientes desplazamientos. Este metodo raro es para facilitar la detección de colision luego.
Vamos a olvidarnos por un rato de la colisión, y hagamos que nuestro jugador finalmente se mueva:

 //sumamos los desplazamientos a la posición
 player.pos.x+=player.p_dX;
 player.pos.y+=player.p_dY;

Si ahora ejecutamos nuestro programa, el jugador se moverá libremente en el mapa, sin detectar ningun tipo de colisión. Facil ¿verdad?. Bien, ahora olvidate de estas ultimas dos lineas, era solo para que veas como haremos que se mueva, estas las utilizaremos despues.

Antes de comenzar con algo de teoria, vamos a calcular un par de cosas que necesitaremos luego:

 //calculamos el centro del jugador
 player.center.x=player.pos.x+b_width/2;
 player.center.y=player.pos.y+b_height/2;

Bien, creo que no hay mucho que explicar aqui. Simplemente calculamos la posicion del centro de nuestro jugador a partir de su posicion en pantalla (recorda que player.pos.x y player.pos.y son la posicion de la esquina superior izquierda del player) y del ancho del bloque. (si no se entiende consultame, todo bien).

Calcularemos ahora la posicion del jugador respecto a la grilla, es decir, que cuadradito "teoricamente" esta ocupando nuestro jugador en la grilla. Veamos esto mas graficamente:

Fijate que en realidad, el problema esta en hallar en que cuadradito de la grilla se encuentra el centro de nuestro jugador. Bien ¿y como hacemos esto? facil.

 //calculamos la posicion en la matriz del jugador
 player.in_matrix.x = (player.center.x/b_width);
 player.in_matrix.y = (player.center.y/b_height);

Recordá que player.center.x y player.center.y son las coordenadas del centro de nuestro jugador y b_width y b_height son el ancho y el largo de nuestros bloques en pixeles.

Bien, solo resta hallar una cosa mas y estaremos listos para detectar colisión.
A modo de optimización, solo detectaremos colision con los bloques mas cercanos a nosotros, por ejemplo, imaginate que nuestro jugador es la tecla 5 de nuestro pad nuemerico, entonces detectaremos colision solo con los bloques de los bordes, es decir, con el 1,2,3,4,6,7,8 y 9. Veamoslo mas graficamente:

La informacion de la posicion de los bloques lindantes van almacenadas en el array player.data_block[n].
Por ejemplo, imaginate que queremos saber la posicion en la grilla del cuadrado que esta sobre nosotros (el 8 en el pad numerico), es decir, sobre el cuadrado que "teoricamente" ocupamos (cuadrado verde). Fijate que es muy simple en realidad, mirá: la coordenada X en la grilla es la misma que la nuestra, y la Y es menor que la nuestra en una unidad, es decir Y-1. Entonces:

 //para hallar las coordenadas del bloque que tenemos encima. (8)
 player.data_block[1]._pos.x= player.in_matrix.x+0;
 player.data_block[1]._pos.y= player.in_matrix.y-1;

Bien, este mismo proceso debemos realizar con los otros 7  bloques restantes. Aca te muestro el codigo de todos ellos:

 //calculamos los bloques lindantes al jugador para la colisión
 player.data_block[0]._pos.x= player.in_matrix.x-1; player.data_block[0]._pos.y= player.in_matrix.y+1;
 player.data_block[1]._pos.x= player.in_matrix.x+0; player.data_block[1]._pos.y= player.in_matrix.y+1;
 player.data_block[2]._pos.x= player.in_matrix.x+1; player.data_block[2]._pos.y= player.in_matrix.y+1;
 player.data_block[3]._pos.x= player.in_matrix.x-1; player.data_block[3]._pos.y= player.in_matrix.y+0;
 player.data_block[4]._pos.x= player.in_matrix.x+1; player.data_block[4]._pos.y= player.in_matrix.y+0;
 player.data_block[5]._pos.x= player.in_matrix.x-1; player.data_block[5]._pos.y= player.in_matrix.y-1;
 player.data_block[6]._pos.x= player.in_matrix.x+0; player.data_block[6]._pos.y= player.in_matrix.y-1;
 player.data_block[7]._pos.x= player.in_matrix.x+1; player.data_block[7]._pos.y= player.in_matrix.y-1;

Entonces tenemos que en player.data_block[0 a 7]._pos tenemos almacenada la coordenada x e y en la grilla de cada uno de los bloques lindantes. Ya vas a entender bien para que sirve :) .

Bien!, ya estamos listos para ver la teoria de la colisión! :D
Notemos primeramente, que nuestro problema se centra basicamente en ver cuando dos rectangulos de mismo tamaño chocan entre si. Veamos esta situación mas graficamente:

Notemos que en este caso, es el bloque A quien está dentro de B, diremos que A esta colisionando con B.
Bien, fijate que esta pasando en nuestro dibujo. La distancia en X entre el centro de A y el centro de B es menor que el ancho de un bloque, y la distancia en Y entre el centro de A y el centro de B es menor que el alto de un bloque. La conclusion que se desprende es la siguiente:

Si (centro_del_bloque_A.x - centro_del_bloque_B.x < ancho_del_bloque) y (centro_del_bloque_A.y - centro_del_bloque_B.y < alto_del_bloque) entonces estan colisionando!.

-"Barbaro!, entonces necesitamos el centro de los bloquecitos lindantes, pues el de nuestro jugador ya lo tenemos."
-"Exacto!, veamos como calcularlos"

// adquirimos el centro del bloque de colision en cuestión
o_center.x=((player.data_block[t]._pos.x)*b_width)+b_width/2;
o_center.y=((player.data_block[t]._pos.y)*b_height)+b_height/2;

Creo que no hay mucho que decir. Es muy parecido a como calculamos el centro de nuestro jugador!.
Voy a explicar el proceso con la coordenada X, con la Y es lo mismo. Lo que estamos haciendo es  multiplicar la cantidad de cuadraditos que tenemos a nuestra izquierda (que es en realidad la posicion X en la grilla del bloque en cuestion) por el ancho de los bloquecitos. Asi objenemos la coordenada X en pantalla del bloque, y luego sumamos la mitad de b_width (ancho de los bloques) para obtener el centro.

Muy bien, veamos entonces como saber que chocamos:

//si va a chocar en general
if ((fabs(o_center.x - player.center.x)<b_width)
&&  (fabs(o_center.y - player.center.y)<b_height)) EJECUTAR_ACCION;
(Recorda que float fabs(float) devuelve el valor absoluto de lo que le pasas como parametro)

Si lo estas codeando en VB sería algo asi:

If Abs(o_center.x - player.center.x) < b_width And (Abs(o_center.y - player.center.y) < b_height) Then
End If

Solo con esta simple linea sabemos si player esta colisionando con algun bloque.
Hagamos entonces que si estan chocando, el jugador no se mueva mas, es decir, que su desplazamiento sea cero. Ahora el metodo raro de manejo empieza a tener alguna utilidad. :)  mirá:

//si va a chocar en general
if ((fabs(o_center.x - player.center.x)<b_width)
&&  (fabs(o_center.y - player.center.y)<b_height))
{
player.p_dX = 0;
player.p_dY = 0;
};

Si probas esto, vas a ver que cuando nuestro jugador choca con otro bloque, el mismo se queda detenido. Pero, hay un problema muy grave... el jugador está DENTRO del bloque!, nustro sistema no evitó que el player entre dentro del otro cuadradito, y ademas, el jugador ya no se mueve mas! :(
Bueno, la verdad no se como arreglarlo... mentira :P.
Lo que tenemos que hacer, es una suerte de detección de colisión a futuro, osea, antes de mover al jugador, hacemos que detecte si va a chocar o no, si va a pasar, entonces dejamos de mover al jugador en esa dirección.
Te preguntarás como hacer eso, es muy facil. Cuando detectamos colision, usamos como coordenadas del centro del player las reales mas el desplazamiento en X e Y.  (ie: (player.center.x+player.p_dX))

 //si va a chocar en general
 if ((fabs(o_center.x - player.center.x- player.p_dX)<b_width)
 &&(fabs(o_center.y - player.center.y-player.p_dY )<t;b_height))
 {
   hacer EVACION_DE_MOVIMIENTO aqui!
 };

Muy bien, ya sabemos entonces que el jugador va a chocar en el proximo movimiento que haga. Pero no sabemos si va a chocar de costado o bien  hacia arriba o hacia abajo. Esto debemos saberlo para hacer que el player se deje de mover para el lado en que no puede hacerlo, pero que si pueda desplazarse en las direcciones restantes.
Esto no es muy complicado en verdad. Simplemente debemos hacer el mismo testeo nuevamente, pero, si queremos detectar si la colision se produce de costado, sumamos UNICAMENTE el desplazamiento en X al player.center.x, y dejamos el player.center.y sin modificacion alguna.

(Esto que sigue iría dentro de EVACION_DE_MOVIMIENTO)

 //si yendo para los costados en particular va a chocar
 if ((fabs(o_center.x - player.center.x-player.p_dX)<b_width)
 &&(fabs(o_center.y - player.center.y)<b_height))
 {
 hacer arreglo de alineación en X aqui.
 player.p_dX =0; //hacemos que no se mueva en el eje X
 }

Traduciendo un poco lo que dice aqui: Si con el desplazamiento en X que se tiene por haber apretado alguna de las teclas para los costados el player va a chocar, entonces hacemos que el desplazamiento en X sea cero, para que el jugador no se mueva para ese lado.
(lo del arreglo de alineación lo voy a explicar en un minuto)
Hacemos lo mismo si el jugador se mueve para arriba o para abajo:

 // si yendo para arriba o abajo en particular va a chocar
 if ((fabs(o_center.x - player.center.x)<b_width)
 &&(fabs(o_center.y - player.center.y-player.p_dY )<t;b_height))
 {
 hacer arreglo de alineación en Y aqui.
 player.p_dY =0;
 }

Barbaro!, ya tenemos un sistemita de colisión que anda CASI de pelos. A primera vista no vas a notar ningun problema, pero existe uno, y muy grave.  Imaginate que estamos moviendonos para la izquierda y detectamos que vamos a chocar, entonces hacemos que el desplazamiento en X sea cero y nuestro player deja de moverse en esa dirección. Muy lindo, pero es muy probable que esté quedando una distancia muy pequeñita entre el bloque y mi jugador. Más graficamente:

Este problema es en verdad muy facil de solucionar. Hacemos que cuando choque contra el bloque de la izquierda, la posicion X de nuestro jugador será la del bloque con quien chocamos mas el ancho de un bloque.
De una manera similar solucionamos el problema si el choque es hacia la derecha.
La posicion X de nuestro jugador será la del bloque con quien chocamos menos el ancho de un bloque.
Como las posiciones de los bloques de mapa los tenemos dados por el centro (lo calculamos arriba), el codigo sería el siguiente:

 //derecha
if(player.p_dX>0) player.pos.x=o_center.x-(b_width+b_width/2);
//izquierda
if(player.p_dX<0) player.pos.x=o_center.x+b_width/2;

De una manera similar lo hacemos para arriba y abajo:

//baja
if(player.p_dY>0) player.pos.y =o_center.y-b_height-b_height/2;
// sube
if(player.p_dY<0) player.pos.y=o_center.y+b_height/2;

Ahora que tenemos detectada la colision, y evitamos que el jugador se mueva cuando no debe hacerlo. Podemos modificar la posicion del jugador:

 //sumamos los desplazamientos a la posición
 player.pos.x+=player.p_dX;
 player.pos.y+=player.p_dY;

Muy bien!, ya esta listo!. Veamos como se vé el código de chequeo de colisión completo:
 

------------------------------------------------------------------------------------------------------
 //  AQUI ARRIBA VAN TODAS LAS COSAS QUE CALCULAMOS ANTES  //
 //  HECHALE UN OJO AL FUENTE DE EJEMPLO, EN LA SUB <IDLE> //

 // chequeo de colisión
 for (int t=0;t<8;t++) //barremos todos los bloques lindantes
 {
  //si es un bloque digno de ser considerado en la colision
   if (mapa[player.data_block[t]._pos.y][player.data_block[t]._pos.x] !='0')
   {

     // adquirimos el centro del bloque de colision en cuestión
      o_center.x=((player.data_block[t]._pos.x)*b_width)+b_width/2;
      o_center.y=((player.data_block[t]._pos.y)*b_height)+b_height/2;

      //si va a chocar en general
      if ((fabs(o_center.x - player.center.x- player.p_dX)<b_width)
      &&(fabs(o_center.y - player.center.y-player.p_dY )<b_height))
      {

         // si yendo para arriba o abajo en particular va a chocar
          if ((fabs(o_center.x - player.center.x)<b_width)
           &&(fabs(o_center.y - player.center.y-player.p_dY )<b_height))
           {
            //baja
            if(player.p_dY>0) player.pos.y =o_center.y-b_height-b_height/2;
           // sube
            if(player.p_dY<0) player.pos.y=o_center.y+b_height/2;
            player.p_dY =0; //hacemos que no nos podamos mover en Y
           }

         // si yendo para los costados en particular va a chocar
          if ((fabs(o_center.x - player.center.x-player.p_dX)<b_width)
          &&(fabs(o_center.y - player.center.y)<b_height))
               {
                //derecha
                if(player.p_dX>0) player.pos.x=o_center.x-(b_width+b_width/2);
               //izquierda
                if(player.p_dX<0) player.pos.x=o_center.x+b_width/2;
                player.p_dX =0; //hacemos que no nos podamos mover en X
               }
          }
    }
------------------------------------------------------------------------------------------------------

Barbaro, es un codigo re chiquito por ser de detección de colisión :D
Muy bien, espero se haya entendido algo. Si queda alguna duda, pueden mandar mail, yo responderé mas que gustoso.

Aqui pueden bajar un codigo de ejemplo hecho en Vc++ con OpenGl: Ejemplo.zip

Ahora si, la parte mas divertida de todo tutorial, los greetz!! :)
Y estos son para:

- Luci  - Silencer - Nahog - FaQ - Numlock - Okasion - Body - Darquiel -

--------------------------------------------------------------------------------------------------------
Si quieren contactarme:
mail: vertexar@yahoo.com  (escriban, por favor!, no me llega mas que propaganda :(  )
icq: 136373996 (en epoca de facultad nunca estoy)
msn: vertexar@hotmail.com  (en epoca de facultad nunca estoy)

!Quiero ver sus proyectos!, A ver cuando juego un tile based game de ustedes! :D
Saludos!. Hasta el proximo tutorial!

-------------------------
Wolfgang of dade (c) 2003
"programo, luego existo"