Dois Corpos Não Ocupam O Mesmo Espaço
Colisão e ocupação de espaço: o que acontece quando a matemática diz uma coisa e o motor diz outra
O princípio de que dois corpos não ocupam o mesmo espaço parece óbvio até você tentar implementá-lo em código. Aí vira um pesadelo de bordas, tolerâncias e situações em que o objeto simplesmente atravessa paredes. Vou explicar como isso funciona na prática, porque a teoriazinha de livro não ajuda muito quando o jogador está colado na geometria e o personagem fica vibrando. No núcleo, a ideia é simples: cada objeto tem uma forma geométrica associada, seja uma esfera, um AABB, um OBB ou uma malha convexa. A cada frame, o motor verifica se há interseção entre essas formas. Se houver, você aplica uma resposta de colisão, que basicamente empurra um dos corpos para fora da sobreposição. O problema é que a verificação não é mágica. Existem vários níveis de implementação:
Fase de detecção (broad phase) — Aqui você descarta rapidamente pares que estão longe o suficiente para nem precisarem ser checados. Técnicas comuns incluem spatial partitioning, sweep and prune ou bounding volume hierarchies. Sem isso, sua simulação vai checar colisões em O(n²), o que significa que 100 objetos viram 4.950 pares por frame. A maioria dos motores modernos usa AABBs unidos em uma BVH ou Quadtree para reduzir drasticamente esse número. Fase precisa (narrow phase) — Quando dois volumes gerais se sobrepõem, você entra na geometria real. Para esferas, é uma conta de distância. Para AABBs, é testar interseção nos três eixos. Para malhas complexas, aí o trabalho aperta. O GJK (Gilbert-Johnson-Keerthi) com EPA é o padrão da indústria paraconvex shapes, e ray-casting contra triângulos é o que você usa para personagens e projéteis. Resolução — Depois de detectar a colisão, você precisa empurrar os corpos para fora. Isso se chama correção posicional. Se só aplicar força de rebote, objetos macios vão penetrar e ficar instáveis. O método padrão é o impulse-based resolution com constraint solver, que resolve normal e atrito de forma iterativa. O número de iterações importa. Três iterações costuma dar estabilidade razoável. Dez iterações dá estabilidade boa mas custa mais.
Um detalhe que quase ninguém menciona: a penetração não é sempre ruim. Deixar uma penetração controlada de 1 a 2 mm no AABB do broad phase evita que o narrow phase dispare false positives por erro de ponto flutuante. Isso se chama speculum tolerance ou penetration tolerance, e ajustar esse valor é uma das primeiras coisas que eu faria se estivesse debugando jitter em colisões. Aqui vai um problema real que eu enfrentei e que mostra como o conceito simples se complica: estávamos construindo um sistema de navmesh para um jogo de ficção científica onde os personagens podiam ter alturas variáveis e colidir com objetos dinâmicos. O problema era que o motor de física (PhysX 4) tratava cada actor com seu próprio shape, e quando dois personagens de alturas diferentes paravam lado a lado, o solver criava constraints conflitantes. Um ficava empurrando o outro pra cima enquanto o outro empurrava pra baixo. Resultado: os dois ficavam vibrando na vertical, uma oscilação de cerca de 4 cm em amplitude e 60 Hz de frequência. A solução foi configurar o flag PxArticulationReductionMode para reduzir o número de graus de liberdade nas constraints de colisão entre personagens, e depois ajustar o slop parameter do solver de 0.01 para 0.001, o que diminuiu a penetração permitida sem quebrar a estabilidade. Também implementamos um simples offset vertical baseado na diferença de altura entre os dois personagens colidindo, o que eliminou completamente o jitter na maioria dos casos. Ajustes finos no warmstarting do solver ajudaram o resto.
O ponto é que o principio de dois corpos não ocupam o mesmo espaço não é uma afirmação absoluta. É uma aproximação que você constrói com tolerâncias, iterações e heurísticas. E essas aproximações falham de maneiras previsíveis se você souber onde olhar. Outra coisa que os tutoriais não ensinam: colisão não é binária. O conceito de contato persistente existe por um motivo. Quando dois objetos já estão em contato no frame anterior, o solver pode reutilizar informações do frame anterior (warm starting) em vez de recalcular tudo do zero. Isso economiza tempo de simulação significativamente, mas introduz um problema de consistência: se o objeto se move muito rápido entre frames, o warm start pode levar a uma posição inicial errada e o solver demora para convergir. Em práticas, eu uso warm starting só para velocidades moderadas e desligo para projetos rápidos. Vou mencionar também uma armadilha comum: usar colliders muito grandes por preguiça. Parece inofensivo, mas collider oversized cria restrições fictícias no solver. Dois objetos podem parecer não estar colidindo pelo visual, mas seus colliders grandes se sobrepõem, gerando forças de repulsão desnecessárias que empurram outros objetos próximos. Eu vi isso acontecer com personagens que tinham capsule colliders com raio 0.6 em vez de 0.4, o que fazia com que encostassem em móveis antes de encostar na parede visualmente.
Se você quer implementar algo do zero, comece com AABBs. É a forma mais simples de representar dois corpos não ocupam o same space e serve como baseline para tudo. Depois migre para spheres, depois para convex hulls. Nunca pule etapas. Tentar implmentar GJK antes de dominar AABB intersection é como querer correr antes de aprender a andar. Há limitações sérias que valem a pena saber. O método impulse-based falha catastrophicamente com stacks altos — piles de mais de 10 objetos geralmente levam a colapsos, penetrções e instabilidade numérica. O problema é que constraints empilhadas criam dependências em cascata que o solver não resolve dentro do limite de iterações típico. A alternativa aqui é usar posicionamento direto em vez de forças, ou ferramentas especializadas como o solver de constraint do Bullet ou do Godot Physics, que têm abordagens diferentes para esse problema. Outro ponto: objetos com forma não-convexa. O PhysX e o Box2D lidam com non-convex shapes decompondo-as em convex parts. Isso é computacionalmente caro e gera artefatos visuais nas junções entre as parts. Se seu objeto é essencialmente não-convexo, considere usar um collider composto ou, em último caso, uma representation simplificada. O importante é reconhecer que convex decomposition é uma aproximação, não uma solução perfeita.
Em resumo, o conceito básico é straightforward. A implementação prática envolve trade-offs entre precisão, performance e estabilidade. O segredo é entender onde cada approximation quebra e ter ferramentas para contornar essas falhas. Se você está começando, foque em dominar AABBs e spheres antes de se aprofundar em geometria complexa. O resto vem com experiência e muitos debugging sessions.
