El dilema de la colisión por pixel
En juegos de plataformas de precisión o matamarcianos (shmups) con estética retro, las cajas delimitadoras tipo AABB (Axis-Aligned Bounding Boxes) a menudo se quedan cortas. Los jugadores perdonan que una bala roce un pixel transparente, pero no toleran una colisión fantasma en el aire.
Sin embargo, verificar pixel por pixel en cada fotograma puede arruinar el rendimiento si no se estructura adecuadamente. Aquí os comparto la solución en dos fases que implementé para mi proyecto actual.
Fase 1: Descarte temprano mediante AABB
Antes de analizar ningún pixel, comprobamos la intersección básica de las cajas. Si las cajas no se solapan, se descarta inmediatamente cualquier cálculo posterior:
// Verificación preliminar de intersección AABB
inline bool check_bounding_overlap(const Rect& a, const Rect& b, Rect& overlap) {
int left = std::max(a.x, b.x);
int top = std::max(a.y, b.y);
int right = std::min(a.x + a.w, b.x + b.w);
int bottom = std::min(a.y + a.h, b.y + b.h);
if (right <= left || bottom <= top) {
return false; // Sin contacto
}
overlap = { left, top, right - left, bottom - top };
return true;
}
Fase 2: Máscaras de bits empaquetadas (Bitmasks)
En lugar de guardar arreglos de bytes para la transparencia, comprimimos la información alfa de cada sprite en enteros de 64 bits (uint64_t). Cada bit representa si un pixel es sólido (1) o transparente (0).
Al restringir la comprobación únicamente al rectángulo de solapamiento (overlap), el test se reduce a desplazar y aplicar una operación lógica AND a nivel de bits:
bool check_bitmask_collision(const Sprite& sprA, const Sprite& sprB, const Rect& overlap) {
for (int y = 0; y < overlap.h; ++y) {
uint64_t rowA = sprA.get_mask_row(overlap.y + y - sprA.y, overlap.x - sprA.x, overlap.w);
uint64_t rowB = sprB.get_mask_row(overlap.y + y - sprB.y, overlap.x - sprB.x, overlap.w);
if ((rowA & rowB) != 0) {
return true; // ¡Impacto detectado a nivel de pixel!
}
}
return false;
}
Resultados
Con esta optimización, el cálculo de colisiones para más de 120 proyectiles y enemigos simultáneos tarda menos de 0.18 milisegundos por frame en un procesador modesto, manteniendo 60 FPS estables sin titubeos.