Objetivo
Tal como ya os comentamos en la última entrega de la serie de artículos sobre la programación de un juego tipo Pong, ahora toca mejorar las colisiones aplicando una técnica utilizada en el mundo de los videojuegos denominada cajas de colisiones.
Primero os presentaremos un pequeño ejemplo que hemos adaptado del libro Modern MSX BASIC Game Development, de Raúl Portales, de su capítulo 5 “Detección de colisiones” de BASIC a C. Aquí os presentaremos la técnica y la compararemos con las colisiones por hardware. Podéis encontrar más información sobre el libro en el enlace [1] al final del artículo.
Para finalizar, os explicaremos cómo hemos modificado el código del último artículo sobre Pong (la pala de la CPU) para sustituir la gestión de las colisiones por colisiones por cajas.
Introducción
Captura de pantalla del ejemplo de colisiones que hemos preparado
El ejemplo que os proponemos para entender las colisiones es el de más arriba. Consta de dos sprites en forma de C. Esta forma se ha elegido para ejemplificar las colisiones de caja en comparación con las de sprite, ya que con las de sprite podemos encajar una figura dentro de otra sin que estas colisionen.
También hemos creado un par de semáforos en la parte inferior de la pantalla para hacer gráfica la colisión cuando esta se produce, tanto en la modalidad de cajas como en la de sprite, software o hardware, respectivamente.
Un poco de teoría. ¿Qué son las cajas de colisiones?
La idea es imaginar una caja alrededor de los sprites cuyo solapamiento queremos controlar. Cuando las cajas se solapan, es cuando ocurre la colisión de los sprites.
Figura con cajas de colisión alrededor de sprites
Para crear las cajas imaginarias utilizamos las coordenadas (X1,Y1) y (X1 + W1,Y1 + H1) en el caso de la primera caja que engloba a Joe, el protagonista del juego Bricks. Aquí, W1 es el ancho y H1 es la altura de la caja.
Si no conoces Bricks, puedes encontrarlo en el enlace [2] al final del artículo.
De igual forma, haremos lo mismo para la caja imaginaria del personaje Sam, también del juego Bricks, utilizando las coordenadas (X2,Y2) y (X2+W2,Y2+H2). Aquí, de igual manera, W2 es el ancho y H2 es la altura de la caja.
Ahora que ya tenemos las cajas definidas, debemos ver cómo comprobar si estas se tocan o se superponen. Para hacerlo, utilizamos la siguiente comprobación.
Primero, verificaremos que las coordenadas en el eje X de la primera caja no sobrepasen las de la segunda caja.
X1 + W1 >= X2 AND X1 <= X2 + W2
Luego, verificaremos que las coordenadas Y de la primera caja no sobrepasen las de la segunda caja.
Y1 + H1 >= Y2 AND Y1 <= Y2 + H2
Si se cumplen las cuatro condiciones anteriores, entonces podemos afirmar que los sprites están colisionando.
X1 + W1 >= X2 AND X1 <= X2 + W2 AND Y1 + H1 >= Y2 AND Y1 <= Y2 + H2
Pongámoslo en práctica con un ejemplo
Pondremos la teoría en práctica con un ejemplo de colisiones. En este ejemplo, tal como hemos comentado, compararemos las colisiones por cajas con las de hardware.
Para hacerlo, primero crearemos un par de objetos que podremos mover por la pantalla, uno con los cursores y el otro con las teclas A, D, W y S, con el fin de acercarlos y provocar las colisiones.
Captura de pantalla de las figuras
Los objetos están abiertos por un extremo intencionalmente para comprobar la diferencia que existe entre las dos aproximaciones para el control de las colisiones presentadas.
En concreto, veremos cómo en las colisiones por hardware podemos introducir un objeto dentro de otro por los extremos abiertos, ya que solo colisionan a nivel de píxel. En cambio, al introducir un objeto dentro de otro por los extremos abiertos, colisionarán a nivel de cajas, ya que la comprobación presentada en la sección anterior se cumplirá.
Os dejo las definiciones de los objetos:
/* --------------------------------------------------------- */
/* SPRITE square with right opening */
/* ========================================================= */
static const unsigned char left_object_pattern_1[] = {
0b00000000,
0b00000000,
0b00111111,
0b00111111,
0b00110000,
0b00110000,
0b00110000,
0b00110000
};
static const unsigned char left_object_pattern_2[] = {
0b00110000,
0b00110000,
0b00110000,
0b00110000,
0b00111111,
0b00111111,
0b00000000,
0b00000000
};
static const unsigned char left_object_pattern_3[] = {
0b00000000,
0b00000000,
0b11111100,
0b11111100,
0b00000000,
0b00000000,
0b00000000,
0b00000000
};
static const unsigned char left_object_pattern_4[] = {
0b00000000,
0b00000000,
0b00000000,
0b00000000,
0b11111100,
0b11111100,
0b00000000,
0b00000000
};
/* --------------------------------------------------------- */
/* SPRITE square with left opening. */
/* ========================================================= */
static const unsigned char right_object_pattern_1[] = {
0b00000000,
0b00000000,
0b00111111,
0b00111111,
0b00000000,
0b00000000,
0b00000000,
0b00000000
};
static const unsigned char right_object_pattern_2[] = {
0b00000000,
0b00000000,
0b00000000,
0b00000000,
0b00111111,
0b00111111,
0b00000000,
0b00000000
};
static const unsigned char right_object_pattern_3[] = {
0b00000000,
0b00000000,
0b11111100,
0b11111100,
0b00001100,
0b00001100,
0b00001100,
0b00001100
};
static const unsigned char right_object_pattern_4[] = {
0b00001100,
0b00001100,
0b00001100,
0b00001100,
0b11111100,
0b11111100,
0b00000000,
0b00000000
};
Para hacer el ejemplo más visual, hemos decidido incorporar unos indicadores al estilo de semáforos. En concreto, un semáforo que se pondrá en rojo cuando haya una colisión por hardware y otro que se pondrá en rojo cuando haya una colisión por software.
Captura de pantalla de los semáforos.
Os dejo las definiciones de los semáforos:
/* --------------------------------------------------------- */
/* SPRITE traffic light */
/* ========================================================= */
static const unsigned char semaphore_pattern_1[] = {
0b00000000,
0b00000111,
0b00001111,
0b00011111,
0b00111111,
0b01111111,
0b01111111,
0b01111111
};
static const unsigned char semaphore_pattern_2[] = {
0b01111111,
0b01111111,
0b00111111,
0b00011111,
0b00001111,
0b00000111,
0b00000000,
0b00000000
};
static const unsigned char semaphore_pattern_3[] = {
0b00000000,
0b11000000,
0b11100000,
0b11110000,
0b11111000,
0b11111100,
0b11111100,
0b11111100
};
static const unsigned char semaphore_pattern_4[] = {
0b11111100,
0b11111100,
0b11111000,
0b11110000,
0b11100000,
0b11000000,
0b00000000,
0b00000000
};
Los dos objetos se pueden mover hacia arriba, abajo, hacia la izquierda y hacia la derecha para hacerlos colisionar.
Captura de pantalla de los objetos.
El objeto azul lo podréis mover con los cursores, y el gris con las teclas W (arriba), S (abajo), A (izquierda) y D (derecha).
Os dejo el fragmento de código relacionado con el movimiento de los objetos:
// Up
if(stick==1)
{
ylefobj=(ylefobj-1);
PutSprite (1,0,xlefobj,ylefobj,1);
}
// w
if(key==119)
{
yrigobj=(yrigobj-1);
PutSprite (2,4,xrigobj,yrigobj,1);
}
// Right
if(stick==3)
{
xlefobj=(xlefobj+1);
PutSprite (1,0,xlefobj,ylefobj,1);
}
// d
if(key==100)
{
xrigobj=(xrigobj+1);
PutSprite (2,4,xrigobj,yrigobj,1);
}
// Down
if(stick==5)
{
ylefobj=(ylefobj+1);
PutSprite (1,0,xlefobj,ylefobj,1);
}
// s
if(key==115)
{
yrigobj=(yrigobj+1);
PutSprite (2,4,xrigobj,yrigobj,1);
}
// Left
if(stick==7)
{
xlefobj=(xlefobj-1);
PutSprite (1,0,xlefobj,ylefobj,1);
}
// a
if(key==97)
{
xrigobj=(xrigobj-1);
PutSprite (2,4,xrigobj,yrigobj,1);
}
Pero vamos a lo que nos interesa. ¿Cómo hemos gestionado el tema de las colisiones en este ejemplo?
Tal como os explicamos al comienzo, haremos uso de los dos tipos de colisiones: las de hardware y las de software.
Os dejo el fragmento de código encargado de gestionar las colisiones de hardware, que hará uso de la función de la librería vdp_sprites.h de FUSION-C, SptireCollision():
// Hardware collision
if (SpriteCollision()) {
PutText(230,10,"1",0);
SC5SpriteColors(3,LineColorsLayer13);
} else {
PutText(230,10," ",0);
SC5SpriteColors(3,LineColorsLayer12);
}
A continuación, os dejo el fragmento de código encargado de gestionar las colisiones por software. Aquí se ha simplificado el cálculo presentado en la introducción teórica.
// Software collision
if ((abs(xlefobj - xrigobj) < 32) && (abs(ylefobj - yrigobj) < 32) ) {
PutText(230,10,"2",0);
SC5SpriteColors(4,LineColorsLayer13);
} else {
PutText(230,10," ",0);
SC5SpriteColors(4,LineColorsLayer12);
}
A continuación, os explicamos el porqué de la simplificación del cálculo en este caso particular.
El cálculo inicial era el siguiente:
IF X1 + W1 >= X2 AND X1 <= X2 + W2 AND Y1 + H1 >= Y2 AND Y1 <= Y2 + H2
Figura donde se muestran las cajas de colisión de los objetos.
Al ser los objetos del mismo tamaño, podemos simplificar el cálculo cambiando H1, H2 por H y W1, W2 por W.
De la misma manera, podemos comprobar el solapamiento vertical restando las coordenadas X de los dos objetos en valor absoluto, y lo mismo con las coordenadas Y, quedando de la siguiente manera:
IF ABS(X1-X2)<W AND ABS(Y1-Y2)<H
Figura donde se muestran las cajas de colisión en colisión
Modifiquemos las colisiones en el Pong del artículo anterior
Recuperemos ahora el último artículo sobre Pong para modificar la gestión de colisiones que hicimos y para incorporar la técnica de colisiones por cajas.
El fragmento de código más abajo es del artículo original. En ese caso, controlábamos las colisiones de la pelota con las palas de los jugadores a través de la función SpriteCollision() de la librería vdp_sprites.h de FUSION-C. La función nos devuelve un 1 cuando detecta colisión entre sprites.
Como comentamos, aunque era una ventaja para los MSX de la época que el VDP tuviera detección de colisiones a nivel de píxel, presenta algunos problemas potenciales. Una limitación importante es la de no saber cuáles son los sprites que han colisionado. Con la técnica de colisiones por cajas, estos problemas potenciales y la limitación mencionada desaparecen.
void DrawSprite(void)
{
// Collision Detection
// Collision with CPU Pad or Player Pad Sptite
if (SpriteCollision()) {
if(TheBall.x>235 && DirX>0) {
DirX*=-1;
}
if (TheBall.x<30 && DirX<0) {
DirX*=-1;
}
}
…
}
En el fragmento de código más abajo, podemos ver la función anterior donde hemos cambiado la gestión de las colisiones por la técnica de cajas de colisiones. A diferencia del ejemplo introductorio, aquí no hemos podido simplificar el cálculo, ya que la pelota y las palas de los jugadores, junto con sus respectivas cajas de colisiones, tienen diferentes dimensiones.
Podemos observar que hemos tenido que diferenciar entre el caso en que la pelota colisione con la pala del jugador (x <= 128) o con la pala de la CPU (x > 128), según la ubicación de las cajas de colisiones de los sprites, ya que tienen tamaños diferentes.
void DrawSprite(void)
{
// Collision Detection
// Collision with CPU Pad or Player Pad Sptite
// If the ball's x < 128 we will look at the collision with the left paddle
if ( (TheBall.x <= 128) && (PlyPad.x + 6 >= TheBall.x) && (PlyPad.x <= TheBall.x + 6) && (PlyPad.y + 16 >= TheBall.y) && (PlyPad.y <= TheBall.y + 6) ) {
DirX*=-1;
}
// If the ball's x > 128 we will look at the collision with the right paddle
if ( (TheBall.x > 128) && (TheBall.x + 6 >= CpuPad.x) && (TheBall.x <= CpuPad.x + 16) && (TheBall.y + 6 >= CpuPad.y) && (TheBall.y <= CpuPad.y + 16) ) {
DirX*=-1;
}
…
}
Conclusiones
Después de la lectura de este artículo, ya tienes las nociones básicas para probar la técnica de las cajas de colisiones en tus proyectos. Te proponemos que, para practicar, utilices unos sprites definidos por ti e incorpores a ellos el ejemplo introductorio. Es posible que tengas que modificar el cálculo, ya que los que te hemos propuesto tienen el mismo tamaño.
Cuando lo tengas, podrás aplicarlo a tu proyecto y considerarlo siempre como una nueva técnica a aplicar.
Esperamos que te haya sido de ayuda. Nos vemos en el próximo artículo.
Haz clic aquí para ver el ejemplo en funcionamiento.
Haz clic aquí para probar la versión retocada del Pong.
Haz clic aquí para acceder al código del ejemplo.
Haz clic aquí para acceder al código de la versión retocada del Pong.
Comments