Jugable
Apuntarme
Fundamentos

Detección de colisiones en 2D: AABB, círculos y por qué te atraviesan las paredes

Cómo funciona realmente una colisión 2D: rectángulos alineados, círculos, y el problema del tunneling que hace que las balas rápidas atraviesen los muros.

Detectar que dos cosas chocan es fácil. Lo difícil es qué haces después, y por qué a veces no lo detectas en absoluto.

AABB: rectángulos alineados

El 90% de los juegos 2D usa esto. Axis-Aligned Bounding Box: un rectángulo que no rota.

function chocan(a, b) {
  return a.x < b.x + b.w &&
         a.x + a.w > b.x &&
         a.y < b.y + b.h &&
         a.y + a.h > b.y
}

Cuatro comparaciones. Si alguna falla, hay un hueco entre ellos y no chocan.

Es barato y es el motivo de que Arcade Physics en Phaser sea tan rápido: sus cuerpos son AABB. También es su límite — un cuerpo Arcade no rota. Si necesitas colisiones rotadas, necesitas Matter.js.

Círculos

Más barato todavía, y sin el problema de las esquinas:

function chocanCirculos(a, b) {
  const dx = a.x - b.x
  const dy = a.y - b.y
  const r  = a.radio + b.radio
  return dx * dx + dy * dy < r * r
}

Nota que comparamos distancias al cuadrado. No hace falta la raíz cuadrada para saber cuál es mayor, y Math.sqrt es de las operaciones más caras que hay. Es un truco que vas a usar constantemente.

Separar, no sólo detectar

Detectar que se solapan no arregla nada: el jugador sigue dentro de la pared. Tienes que empujarlo fuera, y por el eje de menor penetración:

const solapeX = Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x)
const solapeY = Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y)

if (solapeX < solapeY) {
  a.x += (a.x < b.x ? -solapeX : solapeX)   // separar en X
} else {
  a.y += (a.y < b.y ? -solapeY : solapeY)   // separar en Y
}

Si separas siempre por el mismo eje, el jugador se queda pegado a las paredes o salta solo al caminar sobre suelos de tiles. Es un bug clásico.

Tunneling

Aquí está el bug que te va a costar una tarde.

Una bala que se mueve a 2000 px/s recorre 33 píxeles entre frames a 60 fps. Si la pared tiene 16 píxeles de grosor, en un frame la bala está delante y en el siguiente está detrás. Nunca hubo un frame en el que se solaparan. No hay colisión que detectar.

Tres soluciones, de peor a mejor:

  1. Paredes más gruesas que la distancia recorrida por frame. Funciona, es feo, se rompe cuando baja el framerate.
  2. Substeps: divide el movimiento del frame en varios pasos pequeños y comprueba en cada uno. Es lo que hace body.setMaxVelocity implícitamente.
  3. Raycast / swept collision: en vez de mover y comprobar, lanzas un rayo desde la posición anterior a la nueva y buscas el primer impacto. Es lo correcto para balas.

Phaser tiene un flag para esto en Arcade: body.setCollideWorldBounds no lo resuelve, pero sí ayuda physics.world.OVERLAP_BIAS para casos límite.

Qué usar en la práctica

Empieza con Arcade y AABB. Es suficiente para plataformas, top-down, arcade y puzzles — que es casi todo lo que vas a hacer al principio. Pasa a Matter sólo cuando necesites rotación real o formas cóncavas, y asume que Matter es bastante más lento.


Esto lo vemos a fondo en la sesión 4 del curso presencial de Phaser.