A detecção de colisão usando caixas delimitadoras orientadas (OBB) em espaço 2D é eficiente para objetos rotacionados. O método emprega o Teorema dos Iexos Separadores (SAT), onde se verifica se existe um eixo de separação entre dois polígonos convexos. Caso as projeções dos polígonos nesse eixo não se sobreponham, não há colisão. Os eixos candidatos são as normais das arestas dos polígonos.
Inicialmente, implementa-se uma versão para caixas retangulares. Cada caixa é definida por um transform, tamanho e cor para visualização. Os vértices são calculados a partir da matriz de transfromação local para global.
using UnityEngine;
public class CaixaOrientada2D : MonoBehaviour
{
public Vector2 dimensoes;
public Color corVisualizacao = Color.white;
private Vector2 Vertice0 => transform.localToWorldMatrix.MultiplyPoint3x4(new Vector2(-dimensoes.x * 0.5f, -dimensoes.y * 0.5f));
private Vector2 Vertice1 => transform.localToWorldMatrix.MultiplyPoint3x4(new Vector2(dimensoes.x * 0.5f, -dimensoes.y * 0.5f));
private Vector2 Vertice2 => transform.localToWorldMatrix.MultiplyPoint3x4(new Vector2(dimensoes.x * 0.5f, dimensoes.y * 0.5f));
private Vector2 Vertice3 => transform.localToWorldMatrix.MultiplyPoint3x4(new Vector2(-dimensoes.x * 0.5f, dimensoes.y * 0.5f));
public bool VerificaColisao(CaixaOrientada2D outra)
{
Vector2 eixo1 = (Vertice1 - Vertice0).normalized;
Vector2 eixo2 = (Vertice3 - Vertice0).normalized;
Vector2 eixo3 = (outra.Vertice1 - outra.Vertice0).normalized;
Vector2 eixo4 = (outra.Vertice3 - outra.Vertice0).normalized;
if (TestarEixo(this, outra, eixo1)) return false;
if (TestarEixo(this, outra, eixo2)) return false;
if (TestarEixo(this, outra, eixo3)) return false;
if (TestarEixo(this, outra, eixo4)) return false;
return true;
}
private bool TestarEixo(CaixaOrientada2D caixaA, CaixaOrientada2D caixaB, Vector2 eixo)
{
float projA0 = Vector2.Dot(caixaA.Vertice0, eixo);
float projA1 = Vector2.Dot(caixaA.Vertice1, eixo);
float projA2 = Vector2.Dot(caixaA.Vertice2, eixo);
float projA3 = Vector2.Dot(caixaA.Vertice3, eixo);
float projB0 = Vector2.Dot(caixaB.Vertice0, eixo);
float projB1 = Vector2.Dot(caixaB.Vertice1, eixo);
float projB2 = Vector2.Dot(caixaB.Vertice2, eixo);
float projB3 = Vector2.Dot(caixaB.Vertice3, eixo);
float minA = Mathf.Min(projA0, projA1, projA2, projA3);
float maxA = Mathf.Max(projA0, projA1, projA2, projA3);
float minB = Mathf.Min(projB0, projB1, projB2, projB3);
float maxB = Mathf.Max(projB0, projB1, projB2, projB3);
if (minB > maxA || maxB < minA) return true;
return false;
}
private void OnDrawGizmos()
{
Gizmos.matrix = transform.localToWorldMatrix;
Gizmos.color = corVisualizacao;
Gizmos.DrawWireCube(Vector3.zero, new Vector3(dimensoes.x, dimensoes.y, 1f));
}
}
Para testar a detecção entre duas caixas, utiliza-se um script simples que altera a cor com base na colisão.
using UnityEngine;
public class TestadorColisao : MonoBehaviour
{
public CaixaOrientada2D objetoA;
public CaixaOrientada2D objetoB;
private void Update()
{
bool colisaoDetectada = objetoA.VerificaColisao(objetoB);
if (colisaoDetectada)
{
objetoA.corVisualizacao = Color.red;
objetoB.corVisualizacao = Color.red;
}
else
{
objetoA.corVisualizacao = Color.white;
objetoB.corVisualizacao = Color.white;
}
}
}
Para polígonos convexos genéricos, a implementação estende-se ao considerar todos os eixos das arestas. As normais são obtidas via produto vetorial. A lógica de projeção é similar, mas calcula-se o mínimo e máximo das projeções de todos os vértices.
using UnityEngine;
public class PoligonoOrientado2D : MonoBehaviour
{
public Vector2[] vertices = new Vector2[0];
public Color corVisualizacao = Color.white;
public bool VerificaColisao(PoligonoOrientado2D outro)
{
for (int i = 0; i < vertices.Length; i++)
{
Vector2 pontoAtual = transform.localToWorldMatrix.MultiplyPoint3x4(vertices[i]);
Vector2 proximoPonto = transform.localToWorldMatrix.MultiplyPoint3x4(vertices[(i + 1) % vertices.Length]);
Vector2 aresta = proximoPonto - pontoAtual;
Vector2 eixo = new Vector2(-aresta.y, aresta.x).normalized;
if (VerificarSeparacao(this, outro, eixo))
return false;
}
return true;
}
private bool VerificarSeparacao(PoligonoOrientado2D polA, PoligonoOrientado2D polB, Vector2 eixo)
{
ObterProjeçãoMinMax(polA.transform.localToWorldMatrix, polA.vertices, eixo, out float minA, out float maxA);
ObterProjeçãoMinMax(polB.transform.localToWorldMatrix, polB.vertices, eixo, out float minB, out float maxB);
if (minB > maxA || maxB < minA)
return true;
return false;
}
private void ObterProjeçãoMinMax(Matrix4x4 matriz, Vector2[] pontos, Vector2 eixo, out float min, out float max)
{
min = float.MaxValue;
max = float.MinValue;
foreach (Vector2 ponto in pontos)
{
Vector3 pontoGlobal = matriz.MultiplyPoint3x4(ponto);
float projecao = Vector2.Dot(pontoGlobal, eixo);
if (projecao < min) min = projecao;
if (projecao > max) max = projecao;
}
}
private void OnDrawGizmos()
{
Gizmos.color = corVisualizacao;
for (int i = 0; i < vertices.Length; i++)
{
Vector3 inicio = transform.localToWorldMatrix.MultiplyPoint3x4(vertices[i]);
Vector3 fim = transform.localToWorldMatrix.MultiplyPoint3x4(vertices[(i + 1) % vertices.Length]);
Gizmos.DrawLine(inicio, fim);
}
}
}