Parte: 3 — Física y matemáticas de juegos aplicadas · Fuente: Geometría de colisiones para juegos — apuntes de aula y práctica con Python ⏱️ Duración estimada: 60 min · Nivel: Intermedio
Aprender a detectar contactos entre formas, el primer paso de toda física. Cubriremos las pruebas más usadas: AABB vs AABB (cajas alineadas), círculo/esfera vs esfera, punto dentro de una forma y el teorema de los ejes separadores (SAT) para polígonos convexos rotados. Programaremos todo en Python puro para ver claramente la geometría, sin motor de por medio.
Al finalizar, el alumno podrá:
| # | Tema | Por qué importa |
|---|---|---|
| 1 | AABB: cajas alineadas | La prueba más barata y frecuente |
| 2 | Círculo/esfera vs esfera | Ideal para personajes y proyectiles |
| 3 | Punto en forma | Clics, selección y triggers |
| 4 | Idea del SAT | Colisión exacta de polígonos convexos |
| 5 | Ejes candidatos y proyección | Núcleo del algoritmo SAT |
| 6 | Solapamiento mínimo | Base para separar y responder |
| 7 | Broadphase (intuición) | Descartar pares lejanos antes del test caro |
Usaremos Python 3.10+ (python.org) con solo la librería estándar; un par de tuplas hacen de vectores. El resultado observable son valores booleanos (colisiona o no) y el solapamiento numérico. Al final indicamos cómo estas pruebas se relacionan con Area2D y las formas de colisión de Godot, que hacen lo mismo internamente. Ten a mano papel para dibujar los rectángulos y verificar a mano.
Paso 1 — AABB vs AABB. Una caja como (x, y, ancho, alto) con esquina superior izquierda en (x, y).
def aabb_vs_aabb(a, b):
ax, ay, aw, ah = a
bx, by, bw, bh = b
return (ax < bx + bw and ax + aw > bx and
ay < by + bh and ay + ah > by)
print(aabb_vs_aabb((0, 0, 4, 4), (3, 3, 4, 4))) # True (se solapan)
print(aabb_vs_aabb((0, 0, 4, 4), (5, 0, 4, 4))) # False (separadas en X)
Paso 2 — Círculo vs círculo. Comparamos distancias al cuadrado.
def circulo_vs_circulo(c1, r1, c2, r2):
dx = c2[0] - c1[0]
dy = c2[1] - c1[1]
dist2 = dx * dx + dy * dy
suma = r1 + r2
return dist2 <= suma * suma
print(circulo_vs_circulo((0, 0), 2, (3, 0), 2)) # True (dist 3 < 4)
print(circulo_vs_circulo((0, 0), 1, (3, 0), 1)) # False (dist 3 > 2)
Paso 3 — SAT para dos rectángulos rotados. Cada rectángulo se da como lista de 4 vértices. Probamos las normales de sus lados como ejes.
import math
def normales(vertices):
ejes = []
n = len(vertices)
for i in range(n):
x1, y1 = vertices[i]
x2, y2 = vertices[(i + 1) % n]
# borde
ex, ey = x2 - x1, y2 - y1
# normal perpendicular, normalizada
long = math.hypot(ex, ey)
ejes.append((-ey / long, ex / long))
return ejes
def proyectar(vertices, eje):
puntos = [vx * eje[0] + vy * eje[1] for vx, vy in vertices]
return min(puntos), max(puntos)
def sat(poli_a, poli_b):
solape_min = float("inf")
for eje in normales(poli_a) + normales(poli_b):
min_a, max_a = proyectar(poli_a, eje)
min_b, max_b = proyectar(poli_b, eje)
if max_a < min_b or max_b < min_a:
return False, 0.0 # eje separador encontrado: NO colisionan
solape = min(max_a, max_b) - max(min_a, min_b)
solape_min = min(solape_min, solape)
return True, solape_min
Paso 4 — Probar SAT con un rectángulo rotado 45°.
def rect(cx, cy, w, h, ang):
hw, hh = w / 2, h / 2
esquinas = [(-hw, -hh), (hw, -hh), (hw, hh), (-hw, hh)]
c, s = math.cos(ang), math.sin(ang)
return [(cx + x * c - y * s, cy + x * s + y * c) for x, y in esquinas]
a = rect(0, 0, 2, 2, 0)
b = rect(2, 0, 2, 2, math.radians(45)) # diamante que roza a "a"
colisiona, overlap = sat(a, b)
print("colisiona:", colisiona, " solapamiento:", round(overlap, 3))
Ejecuta y mueve b a x = 3 para confirmar que deja de colisionar. El SAT detecta correctamente el contacto aunque un rectángulo esté rotado, algo que AABB no puede.
punto_en_aabb(px, py, caja) y punto_en_circulo(px, py, centro, r).circulo_vs_circulo a 3D (esfera vs esfera) agregando la coordenada Z.sat para que también devuelva el eje de menor solapamiento (útil para separar).time cuántos pares por segundo procesa SAT con y sin broadphase para 1000 formas.Implementa una función colision(forma_a, forma_b) que reciba formas etiquetadas ("aabb", "circulo", "poligono") y despache a la prueba correcta, devolviendo (colisiona: bool, solapamiento: float). Incluye el caso poligono-poligono con SAT.
Criterio de aceptación: para dos cuadrados unitarios centrados en (0,0) y (0.5,0) devuelve True con solapamiento 0.5; para los mismos separados a (2,0) devuelve False; y un círculo de radio 1 en (0,0) con otro en (1.5,0) radio 1 devuelve True.
| Síntoma | Causa y arreglo |
|---|---|
| AABB detecta colisión cuando solo se tocan los bordes | Decide si el contacto exacto cuenta; usa < o <= de forma consistente |
| Círculo-círculo lento | Estás usando sqrt; compara dist2 <= (r1+r2)**2 |
| SAT falla con polígonos cóncavos | SAT solo sirve para convexos; descompón el cóncavo en convexos |
| El solapamiento sale negativo o enorme | Olvidaste normalizar los ejes; hazlo antes de proyectar |
| SAT no detecta un rectángulo rotado | Faltan las normales del segundo polígono; incluye ambos conjuntos de ejes |
¿Por qué AABB si SAT es más general? AABB es órdenes de magnitud más barato. Se usa como broadphase para descartar la mayoría de pares antes del test exacto.
¿SAT sirve en 3D? Sí, pero además de las normales de las caras hay que probar los productos cruzados de aristas. La idea (buscar un eje separador) es la misma.
¿Qué pasa con formas cóncavas? SAT no las cubre. Se descomponen en piezas convexas o se usan mallas de colisión, como hace Godot con los CollisionPolygon.
¿Esto lo hace Godot por mí? Sí; Area2D, CharacterBody2D y las formas de colisión implementan estas pruebas. Programarlas a mano te da la intuición para depurar y para casos personalizados.
Clase 070 - Integración numérica en la práctica (Euler y Verlet)