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í:
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.
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:
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):
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?
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.
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