Lección 10 – Colisiones

Aventuras en Megadrive: Colisiones

 

Antes de colisionar

Llamamos colisión a dos sprites ocupando el mismo espacio en pantalla, tanto parcial como totalmente.

Las colisiones son uno de los elementos más difíciles de dominar cuando programas para sistemas clásicos. En el desarrollo multi-plataforma moderno, las colisiones se gestionan internamente y el programador sólo tiene que elegir qué hacer cuando ocurre una colisión. En un sistema de 8/16 bits, es el programador quien debe gestionar tanto la detección como las consecuencias de una colisión.

 

¿Cuándo y cómo comprobamos colisiones?

La respuesta, de nuevo, no es fácil y depende del juego en cuestión. Por ejemplo en un mata-marcianos ¿comprobamos las colisiones naves vs disparos enemigos vs disparos del player en cada frame? Podemos tener un bonito lag cuando aparezcan muchas balas en pantalla. No hay una respuesta genérica y dependerá del juego.

 

El registro de colisión

El VDP de la Mega tiene un registro, llamado registro de colisión, que se activa automáticamente si hay colisión entre dos sprites no transparentes. Es herencia del chip TMS9918A/TMS9928A, el germen a partir del cual aparece el chip de la Master System y más tarde de la Megadrive.

Para consultarlo usando el SGDK, hay que configurar una interrupción horizontal que lance una función dentro de la cual se consulta el registro:

SYS_setHIntCallback(mi_funcion); //función a lanzar
VDP_setHIntCounter(8);  //Cada 8 scanlines, lanza la interrupción
VDP_setHInterrupt(1);   //Activa la interrupción horizontal

Hecho esto, creamos la función en cuestión:

void mi_funcion()
{
if(GET_VDPSTATUS(VDP_SPRCOLLISION_FLAG)!=0)  //haz algo
}

GET_VDPSTATUS(VDP_SPRCOLLISION_FLAG) = 0 si no hay colisión, otro valor si la hay.

Casi ningún juego de la Mega utiliza este registro, ¿por qué?

  • Aunque marque que hay colisión, no sabemos quién ha colisionado con quién.
  • Si hay más de una colisión, no lo sabremos.
  • Hemos de tener la suerte de leer el registro (=lanzar la interrupción) justo cuando ocurra la colisión, en ese scanline, en cuanto pase a otra línea sin colisiones, el registro vuelve a valer 0.
  • A consecuencia de lo anterior, tendremos que lanzar la interrupción cada pocos scanlines… pero esto hace que el procesador pierda el tiempo cuando podría estar haciendo otras cosas más útiles.
  • El período hblank es muy corto, por tanto si queremos usarlo para otra tareas no podremos.

En los sistemas más antiguos el registro de colisión tenía cierta utilidad pues apenas habían sprites en pantalla, pero en la negrita, que pone hasta 80 sprites en pantalla, debemos atacar el problema desde otro punto de vista.

 

 

Colisión por DISTANCIA

Una forma sencilla de gestionar colisiones es calcular la distancia entre dos sprites o Tolerancia. Por debajo de dicha distancia existe colisión. Dicha distancia puede ser fija (utilizaremos una constante) o variable según necesidades (en ese caso usamos una variable).

En geometría la distancia entre dos puntos (en un plano 2D) se calcula así:

distancia-entre-dos-puntos

Sin embargo hacer esto para cada par de puntos sería demasiado lento. Hay formas más sencillas. Por ejemplo, si tenemos 2 sprites sp1 y sp2 con variables de posición sp1x,sp1y, sp2x, sp2y una forma de calcular si hay o no colisión podría ser:

IF(abs(sp1x-sp2x)<TOLERANCIA && abs(sp1y-sp2y)<TOLERANCIA)
    //haz algo
else
    //haz otra cosa

Esta fórmula es tan válida como cualquier otra

 

 

Colisión por DISTANCIA: Punto de Origen

Si utilizamos la fórmula anterior, pronto detectaremos que algo no va bien. Tal y como expliqué en su momento en la primera entradas sobre sprites, la posición en pantalla de un sprite viene indicada por el punto sup-izq de dicho sprite.

captura29

Por tanto puede darse el caso que dos sprites no estén tocándose (su parte visible) pero sí tengamos colisión, al estar sus puntos de origen por debajo de la TOLERANCIA.

Fíjate en la captura anterior, el punto sup-izq de Sonic está alejado de su parte visible. Si otro sprite se sitúa en dicho punto, aunque no toque la parte visible de Sonic, detectaremos una colisión.

En el ejemplo de esta entrada, dos sprites que no se tocan (bala vs Sonic), pero al coincidir los puntos de origen (o estar muy cerca), esto resulta en una colisión:

captura56

 

Colisión por CAJAS DE COLISIÓN

En vez de comprobar un punto vs otro punto, vamos a comprobar un punto vs rango. La forma sencilla de hacer esto es mediante rectángulos:

Sprite: punto (x,y)
Rectángulo: definido por Xmin,Ymin – Xmax,Ymax

¿Es la ‘x’ mayor que la Xmin del rectángulo Y ADEMÁS menor que la Xmax?
¿Es la ‘y’ mayor que la Ymin del rectángulo Y ADEMÁS menor que la Ymax?

En el ejemplo que veremos en el github, tenemos el sprite de la bala (1 tile, 8x8px) y el sprite de Sonic (6×6 tiles), cuyo borde hemos pintado en azul. En la bala tiene sentido coger las coordenadas de su punto de origen, pero en Sonic no. Así que en vez de trabajar con su punto de origen, hemos escogido un rectángulo y sus coordenadas (en rojo):

captura57

Como vemos no hay colisión. Parece que esta forma de trabajar es mucho mejor que la anterior pero… ¿qué sucede si vamos acercando a Sonic?

captura58

Ambos sprites se tocan, visualmente hablando, pero al ocurrir fuera de la caja de colisión, no tenemos colisión alguna. Para ello el sprite de la bala debe estar dentro.

captura59

Ahora sí. La forma y la posición de la caja de colisión debe ser estudiada en detalle para no resultar en una experiencia negativa para el jugador. Normalmente la caja es más pequeña que el sprite, para dejar cierto margen al jugador.

Esta forma de trabajar es perfecta para comprobar la colisión de un sprite pequeño (la típica bala) vs un sprite grande (la típica nave de un matamarcianos). Esta forma NO sería adecuada para dos sprites grandes. Esto lo veremos en la siguiente entrada del blog.

Antes de finalizar esta entrada, un poco de código:

void chequea_colision()
{
[...]

  //actualiza la caja de colision
  CajaColision.x1 = sonic_posx + 16;
  CajaColision.y1 = sonic_posy + 8;
  CajaColision.x2 = sonic_posx + 31;
  CajaColision.y2 = sonic_posy + 31;

//Comprobando colisiones
 if( bala_posx>=CajaColision.x1 && bala_posx<=CajaColision.x2 && bala_posy>=CajaColision.y1 && bala_posy<=CajaColision.y2 )
      //haz cosas
  else
     //haz otras cosas

[...]
}

 

Antes de comprobar colisiones, hay que actualizar la posición de la caja de colisión.

 

 

Un ejemplo sencillo

Puedes encontrar un ejemplo en mi github. Juega con los sprites para ver cómo trabaja cada estrategia de detección de colisiones.

 

Cara y cruz de las CAJAS DE COLISIÓN

Hay un inconveniente que tenemos que tener en cuenta si vamos a usar cajas de colisión y sprites espejados. Supongamos que tenemos un sprite de 6×7 tiles, he aquí una captura con el mallado de 8x8px para distinguir cada tile, la caja de colisión (en rojo) podría ser p.e. de 3×5 tiles tal que así:

Cubre bastante bien el cuerpo del arquero. Pero, ¿qué ocurre si decidimos poner otro arquero en el otro lado de la pantalla con el flag adecuado para que salga espejado horizontalmente?

El lado derecho del arquero espejado NO está incluido en la caja de colisión. Es más, si disparamos «al aire» justo en frente de la cara SÍ contará como impacto.

Por tanto hay que tener en cuenta estas cosas al diseñar una caja de colisión.

 

 

 

 

 

GITHUB

 

El código de esta lección, y de todas las demás, lo podrás encontrar en mi github:

https://github.com/danibusvlc/aventuras-en-megadrive