Voltar para o Blog
Quest Log

OnCollisionEnter na Unity: Detectar Colisão com Tag e Layer

Projétil batendo num robô inimigo com faíscas no ponto de contato e um rótulo de dano flutuando

Unity OnCollisionEnter na prática: objeto Collision, CompareTag vs tag ==, filtro por layer e interface IDamageable com TryGetComponent, em C# do Unity 6.

Ferramenta:UnityC#

OnCollisionEnter na Unity: Detectar Colisão com Tag e Layer

Unity OnCollisionEnter é o método que a Unity chama quando o collider do seu objeto bate em outro collider sólido. Ele recebe um objeto Collision com tudo sobre o impacto: em quem bateu, onde, com que velocidade. A dúvida real vem logo depois: como saber com quem bateu sem espalhar if (tag == "Inimigo") pelo projeto inteiro?

Este post mostra o básico do evento, o que dá para extrair do Collision, a diferença prática entre CompareTag e tag ==, como filtrar por layer e o padrão que dispensa tag para dano: uma interface IDamageable encontrada com TryGetComponent. Código C# para Unity 6.3 LTS, com a versão 2D no fim.

Unity OnCollisionEnter: o básico

O evento dispara no primeiro passo de física em que dois colliders sólidos passam a se tocar. Para ele existir, três condições:

  1. Os dois objetos têm collider, e nenhum dos dois está com Is Trigger marcado (se um for trigger, o evento é OnTriggerEnter).
  2. Pelo menos um dos dois tem Rigidbody não Kinematic. Kinematic contra Kinematic ou contra cenário não gera colisão.
  3. As layers dos dois estão marcadas para interagir na Layer Collision Matrix.

O script vai no objeto que tem o collider ou no objeto do Rigidbody dono do collider. Arquivo Bala.cs, a versão mínima:

using UnityEngine;

public class Bala : MonoBehaviour
{
    private void OnCollisionEnter(Collision collision)
    {
        Debug.Log($"Bala bateu em {collision.gameObject.name}");
        Destroy(gameObject);
    }
}

Se você não precisa de nenhuma informação do impacto, a documentação do OnCollisionEnter recomenda declarar o método sem o parâmetro (private void OnCollisionEnter()), o que evita cálculos desnecessários.

O objeto Collision: gameObject, contacts, relativeVelocity

O Collision é a ficha do impacto. Os campos que mais aparecem em jogo:

CampoO que trazUso típico
gameObjectGameObject do collider atingidoSaber em quem bateu
colliderO Collider atingidoDistinguir partes (cabeça, corpo)
rigidbodyRigidbody do objeto atingido (null se não tiver)Empurrar, achar o dono de colliders filhos
transformTransform do objeto atingidoPosição, hierarquia
relativeVelocityVelocidade relativa entre os doisForça do impacto, dano de queda
impulseImpulso total aplicado para resolver o contatoMedida física do impacto
contactCountNúmero de pontos de contatoLaço sobre os contatos
GetContact(i)Um ponto de contato (posição, normal)Faísca, decalque, direção do golpe

Sobre os contatos: a propriedade collision.contacts cria um array novo a cada acesso, e a página da classe Collision avisa que ela gera lixo de memória. Use contactCount com GetContact, ou GetContacts com um array seu.

Arquivo Impacto.cs, que gera um efeito no ponto exato do choque, alinhado à superfície, só quando a batida é forte:

using UnityEngine;

public class Impacto : MonoBehaviour
{
    [SerializeField] private GameObject prefabFaisca;
    [SerializeField] private float velocidadeMinima = 3f;

    private void OnCollisionEnter(Collision collision)
    {
        if (collision.relativeVelocity.magnitude < velocidadeMinima)
        {
            return;
        }

        if (collision.contactCount == 0)
        {
            return;
        }

        ContactPoint contato = collision.GetContact(0);
        Quaternion rotacao = Quaternion.LookRotation(contato.normal);
        Instantiate(prefabFaisca, contato.point, rotacao);
    }
}

A normal do contato aponta para fora da superfície atingida. É ela que faz a faísca "sair" da parede em vez de aparecer num ângulo qualquer.

CompareTag vs tag ==

As duas linhas abaixo dão o mesmo resultado:

if (collision.gameObject.tag == "Inimigo") { }
if (collision.gameObject.CompareTag("Inimigo")) { }

A diferença está no custo e na segurança. Ler a propriedade tag devolve uma string nova a cada chamada, que vira lixo para o coletor de memória. Num evento de colisão que roda dezenas de vezes por segundo numa cena cheia, isso soma. CompareTag compara sem criar a string.

O ponto fraco das duas é o mesmo: a tag é uma string solta. CompareTag("Inimgo") com erro de digitação dá falso para sempre e o bug parece de física. Guarde os nomes numa classe de constantes (public const string Inimigo = "Inimigo";) ou use o TagHandle, abaixo, que é obtido uma vez só e falha logo no Awake se o nome estiver errado.

No Unity 6.3 existe também a sobrecarga com TagHandle, que a documentação descreve como potencialmente mais rápida quando o mesmo handle é reutilizado em muitas chamadas:

using UnityEngine;

public class DetectorInimigo : MonoBehaviour
{
    private TagHandle tagInimigo;

    private void Awake()
    {
        tagInimigo = TagHandle.GetExistingTag("Inimigo");
    }

    private void OnCollisionEnter(Collision collision)
    {
        if (collision.gameObject.CompareTag(tagInimigo))
        {
            Debug.Log("Bateu num inimigo");
        }
    }
}

Tag resolve bem perguntas de identidade ("isso é o jogador?"). Ela começa a atrapalhar quando você usa para perguntas de capacidade ("isso pode tomar dano?"): aí cada tipo novo de objeto destrutível exige mexer em todos os scripts que comparam tags. É onde entra a interface, duas seções abaixo.

Filtrar por layer

Layer é melhor que tag quando o filtro também deveria valer para a física. Se balas do jogador nunca devem bater em outras balas do jogador, não filtre no código: desmarque o par na Layer Collision Matrix (Edit > Project Settings > Physics) e o evento nem existe.

Quando você precisa filtrar no código (o objeto colide fisicamente com várias layers, mas só reage a algumas), use um LayerMask no Inspector e teste o bit da layer do outro objeto. Arquivo ReagePorLayer.cs:

using UnityEngine;

public class ReagePorLayer : MonoBehaviour
{
    [SerializeField] private LayerMask layersQueReagem;

    private void OnCollisionEnter(Collision collision)
    {
        int layerOutro = collision.gameObject.layer;
        bool reage = (layersQueReagem.value & (1 << layerOutro)) != 0;

        if (!reage)
        {
            return;
        }

        Debug.Log($"Reagiu a {collision.gameObject.name}");
    }
}

O gameObject.layer é um índice (0 a 31), e o LayerMask é uma máscara de bits. O 1 << layerOutro transforma o índice em máscara para comparar. Misturar os dois é o erro mais comum com layers.

Rigidbody e Collider têm também Include Layers e Exclude Layers (Layer Overrides no Inspector), que sobrescrevem a matriz só para aquele objeto. Útil para casos pontuais sem criar layer nova.

Filtrar por tag e por layer resolve um objeto. O problema aparece quando o jogo cresce: o mesmo sistema de dano precisa conversar com animação de golpe, invencibilidade, som, partícula, barra de vida e inimigos com vários colliders, e um projeto solo costuma virar um emaranhado de if e referências cruzadas. O curso de Unity constrói esse sistema dentro de um jogo completo, com uma organização de scripts que aguenta o projeto crescer. A próxima seção mostra a peça central dessa organização.

Curso de Unity
Quer fazer seu primeiro jogo na Unity?

No curso de Unity 6 da CursoGame.Dev você vai do zero ao primeiro jogo, com um desafio prático em cada level e suporte individual no WhatsApp quando travar.

Conhecer o curso de Unity
Suporte no WhatsAppAcesso vitalícioGarantia de 7 dias

Interface IDamageable em vez de tag

Dano é o caso em que tag mais atrapalha. Inimigo, caixa destrutível, barril explosivo e o próprio jogador tomam dano, cada um de um jeito. Com tags, a bala vira uma lista de if que cresce a cada objeto novo.

A alternativa é a bala perguntar "você toma dano?" em vez de "você é inimigo?". Isso é uma interface. Arquivo IDamageable.cs (o using UnityEngine; é necessário por causa do Vector3):

using UnityEngine;

public interface IDamageable
{
    void ReceberDano(int quantidade, Vector3 pontoImpacto);
}

Qualquer componente implementa. Arquivo CaixaDestrutivel.cs:

using UnityEngine;

public class CaixaDestrutivel : MonoBehaviour, IDamageable
{
    [SerializeField] private int vida = 3;

    public void ReceberDano(int quantidade, Vector3 pontoImpacto)
    {
        vida -= quantidade;
        if (vida <= 0)
        {
            Destroy(gameObject);
        }
    }
}

E a bala não conhece nenhum tipo concreto. Arquivo BalaComDano.cs:

using UnityEngine;

public class BalaComDano : MonoBehaviour
{
    [SerializeField] private int dano = 1;

    private void OnCollisionEnter(Collision collision)
    {
        IDamageable alvo = EncontrarAlvo(collision);
        if (alvo != null)
        {
            Vector3 ponto = collision.contactCount > 0
                ? collision.GetContact(0).point
                : transform.position;
            alvo.ReceberDano(dano, ponto);
        }

        Destroy(gameObject);
    }

    private IDamageable EncontrarAlvo(Collision collision)
    {
        // 1) No próprio objeto do collider atingido.
        if (collision.gameObject.TryGetComponent(out IDamageable alvo))
        {
            return alvo;
        }

        // 2) No dono do Rigidbody (colliders filhos de um personagem).
        if (collision.rigidbody != null &&
            collision.rigidbody.TryGetComponent(out alvo))
        {
            return alvo;
        }

        return null;
    }
}

TryGetComponent aceita interface como tipo e, ao contrário de GetComponent, não aloca nada no editor quando o componente não existe. O segundo passo da busca resolve o caso de personagens com vários colliders filhos (cabeça, tronco, pernas): o collider atingido está num filho, mas o componente de vida está no objeto do Rigidbody.

Com esse padrão, adicionar um barril explosivo é criar um script que implementa IDamageable. A bala não muda. É o mesmo princípio de "programar contra uma capacidade" que aparece no post de padrões de projeto para jogos.

OnCollisionEnter2D

Em 2D o método muda de nome e de parâmetro: OnCollisionEnter2D(Collision2D collision). Ele só dispara entre Collider2D (BoxCollider2D, CircleCollider2D) com Rigidbody2D. A física 2D não conversa com a 3D.

O Collision2D tem gameObject, collider (o do outro objeto), otherCollider (o seu), rigidbody, relativeVelocity, contactCount e GetContact. Arquivo BalaComDano2D.cs, a mesma bala em 2D usando a mesma interface:

using UnityEngine;

public class BalaComDano2D : MonoBehaviour
{
    [SerializeField] private int dano = 1;

    private void OnCollisionEnter2D(Collision2D collision)
    {
        if (collision.gameObject.TryGetComponent(out IDamageable alvo))
        {
            Vector3 ponto = collision.contactCount > 0
                ? (Vector3)collision.GetContact(0).point
                : transform.position;
            alvo.ReceberDano(dano, ponto);
        }

        Destroy(gameObject);
    }
}

A interface é a mesma do 3D: IDamageable não depende de física, então o sistema de dano serve para os dois mundos.

Erros comuns

Método com parâmetro errado. OnCollisionEnter(Collider other) compila e nunca é chamado. O tipo é Collision. Em 2D, Collision2D.

Usar collision.contacts no laço. Funciona, mas aloca um array a cada acesso. Troque por contactCount e GetContact.

Script de vida no pai, collider no filho. collision.gameObject.TryGetComponent falha porque o componente não está ali. Busque pelo collision.rigidbody ou com GetComponentInParent<IDamageable>().

Dano duplicado. Uma bala com dois colliders, ou um alvo com vários, pode gerar mais de um OnCollisionEnter no mesmo passo. Destrua a bala ou desligue o collider dela no primeiro impacto.

Tag escrita errado. A comparação dá falso em silêncio e o bug parece de física. Use CompareTag com uma classe de constantes ou o TagHandle.

Esperar evento com Kinematic. Kinematic contra parede não gera colisão. Se o projétil é Kinematic, use trigger ou raycast.

Destruir o objeto e continuar usando ele. Destroy(gameObject) só remove o objeto no fim do frame, então o código depois dele ainda roda. Se a bala chama Destroy e logo depois tenta aplicar dano de novo num segundo contato, o dano sai duplicado. Desligue o collider (GetComponent<Collider>().enabled = false;) antes do Destroy quando houver risco de dois eventos seguidos.

Próximo passo

Implemente IDamageable em dois objetos diferentes (uma caixa e um inimigo com collider filho) e use a mesma BalaComDano nos dois. Depois mova a caixa para uma layer "Destrutivel" e use o ReagePorLayer para um sensor que só reage a ela. Com esses dois padrões, quase nenhum if de tag sobra no seu projeto.

O passo seguinte natural é transformar ReceberDano num sistema de vida completo, com invencibilidade, morte e barra de vida: está no post de sistema de vida e dano na Unity. Se você também mexe com Godot, o guia de física de jogos no Godot mostra a detecção equivalente por sinais.

Perguntas frequentes

Qual a diferença entre CompareTag e tag == na Unity?

Os dois comparam a tag do objeto, mas ler gameObject.tag cria uma string nova a cada chamada, e CompareTag compara sem isso. No Unity 6 há ainda a sobrecarga com TagHandle, mais rápida quando você reutiliza o handle.

Como pegar o objeto com que colidi no OnCollisionEnter?

Use collision.gameObject para o GameObject do collider atingido, ou collision.rigidbody para o Rigidbody dono dele. Em objetos com vários colliders filhos, o collider pode estar num filho e o script de vida no pai: nesse caso procure o componente a partir de collision.rigidbody ou com GetComponentInParent.

OnCollisionEnter2D funciona igual ao OnCollisionEnter?

A lógica é a mesma, mas o método recebe Collision2D e só dispara entre Collider2D com pelo menos um Rigidbody2D Dynamic envolvido. Collision2D tem gameObject, collider, otherCollider, relativeVelocity, contactCount e GetContact.

Por que OnCollisionEnter não é chamado?

Colisão só gera evento se pelo menos um dos dois objetos tiver Rigidbody não Kinematic, e nenhum dos colliders pode estar marcado como Is Trigger. Confira também a Layer Collision Matrix e se você não misturou física 2D com 3D.