Voltar para o Blog
Quest Log

Raycast 2D na Unity: Ground Check, Visão e Linha de Tiro

Personagem 2D numa plataforma com duas linhas verdes saindo dos pés e um inimigo com feixe de visão amarelo

Unity raycast 2D com Physics2D.Raycast: ground check com dois raios, inimigo que enxerga o jogador e linha de tiro, e por que o raio acerta o próprio collider.

Ferramenta:UnityC#

Raycast 2D na Unity: Ground Check, Visão e Linha de Tiro

Unity raycast 2D é feito com Physics2D.Raycast: você passa uma origem e uma direção em Vector2 e recebe um RaycastHit2D dizendo se a linha encontrou algum Collider2D, qual e onde. Com essa consulta você resolve três problemas de todo jogo 2D: o personagem está no chão? O inimigo consegue ver o jogador? Onde esse tiro acerta?

Este post monta os três casos com código C# para Unity 6.3 LTS: ground check com dois raios nas bordas do pé, visão de inimigo bloqueada por parede e linha de tiro desenhada com LineRenderer. No caminho, você vai entender a armadilha que pega quase todo mundo no 2D: o raio que acerta o próprio collider do jogador.

Unity raycast 2D: diferença para o 3D

A física 2D da Unity é um sistema separado da 3D, com componentes e consultas próprias. Na prática, quatro diferenças importam:

3D2D
ChamadaPhysics.RaycastPhysics2D.Raycast
Retornobool + out RaycastHitRaycastHit2D direto
Colliders que enxergaBoxCollider, SphereCollider...BoxCollider2D, CircleCollider2D...
Collider onde o raio começaIgnoradoDetectado, com a opção padrão

A última linha é a que mais causa bug, e tem uma seção só para ela no fim.

Physics2D.Raycast e RaycastHit2D

A forma mais usada:

RaycastHit2D hit = Physics2D.Raycast(origem, direcao, distancia, mascara);

origem e direcao são Vector2, distancia é float e mascara é o LayerMask com as layers que o raio deve considerar. A referência do Physics2D.Raycast no Unity 6.3 mostra também as sobrecargas com ContactFilter2D que preenchem uma lista ou array com vários resultados, ordenados do mais perto para o mais longe.

O RaycastHit2D é uma struct, não uma classe. Ele sempre existe; o que diz se houve acerto é o collider:

if (hit.collider != null)
{
    Debug.Log($"Acertou {hit.collider.name} em {hit.point}, normal {hit.normal}");
}

A struct também converte para bool, então if (hit) funciona igual. O que não funciona é if (hit != null): uma struct nunca é null, e esse teste dá sempre verdadeiro.

Campos úteis: hit.point (ponto do impacto no mundo), hit.normal (direção perpendicular à superfície), hit.distance (distância da origem), hit.fraction (fração da distância máxima) e hit.rigidbody.

Ground check com raycast

Um raio só, no centro do pé, falha num caso clássico: o personagem com metade do corpo para fora da plataforma. O centro está no ar, o raio não acha nada e o jogo diz que ele está caindo, mesmo com o pé visualmente apoiado. A correção é usar dois raios, um em cada borda do pé, e considerar chão se qualquer um acertar.

Arquivo GroundCheck2D.cs:

using UnityEngine;

[RequireComponent(typeof(Collider2D))]
public class GroundCheck2D : MonoBehaviour
{
    [SerializeField] private LayerMask layersChao;
    [SerializeField] private float distanciaExtra = 0.08f;
    [SerializeField] private float recuoDaBorda = 0.05f;

    private Collider2D corpo;

    public bool NoChao { get; private set; }

    private void Awake()
    {
        corpo = GetComponent<Collider2D>();
    }

    private void FixedUpdate()
    {
        Bounds b = corpo.bounds;
        float y = b.min.y + 0.02f; // um pouco acima da base
        Vector2 esquerda = new Vector2(b.min.x + recuoDaBorda, y);
        Vector2 direita = new Vector2(b.max.x - recuoDaBorda, y);
        float distancia = 0.02f + distanciaExtra;

        bool acertouEsquerda = Physics2D.Raycast(esquerda, Vector2.down, distancia, layersChao);
        bool acertouDireita = Physics2D.Raycast(direita, Vector2.down, distancia, layersChao);
        NoChao = acertouEsquerda || acertouDireita;

        Debug.DrawRay(esquerda, Vector2.down * distancia, acertouEsquerda ? Color.green : Color.red);
        Debug.DrawRay(direita, Vector2.down * distancia, acertouDireita ? Color.green : Color.red);
    }
}

Três decisões nesse script:

  • Os raios saem dos bounds do collider, não de números fixos. Se você trocar o tamanho do personagem, o ground check acompanha.
  • O recuoDaBorda puxa os raios um pouco para dentro. Sem isso, encostar numa parede faz o raio da borda descer rente a ela e acusar "chão" no meio do pulo.
  • A máscara contém só o chão. O jogador fica fora, o que já resolve o problema do raio acertar o próprio collider.

O raycast resolve o "estou no chão?". O que o pulo faz com essa informação (coyote time, buffer de pulo, pulo variável) é outro assunto: o post de ground check e pulo 2D na Unity monta o pulo completo, e o de coyote time explica por que o jogador sente o controle "justo" quando você dá alguns milissegundos de tolerância depois da borda.

Inimigo que enxerga o jogador

A visão do inimigo é um raycast do olho dele até o jogador, com uma regra que muda tudo: a máscara precisa incluir as paredes e o jogador. Assim o raio para no primeiro obstáculo. Se o primeiro obstáculo é o jogador, ele está visível; se é parede, está escondido. Com só o jogador na máscara, o raio atravessa paredes e o inimigo ganha visão de raio X.

Antes do raycast, dois testes baratos filtram a maioria dos casos: distância e cone de visão.

Arquivo VisaoInimigo2D.cs:

using UnityEngine;

public class VisaoInimigo2D : MonoBehaviour
{
    [SerializeField] private Transform jogador;
    [SerializeField] private Transform olho;
    [SerializeField] private float alcance = 8f;
    [SerializeField] private float anguloVisao = 50f; // graus para cada lado
    [SerializeField] private LayerMask layersVisao; // Jogador + Cenario

    private int direcaoOlhar = 1; // 1 direita, -1 esquerda

    public bool VendoJogador { get; private set; }

    public void DefinirDirecao(int direcao)
    {
        direcaoOlhar = direcao >= 0 ? 1 : -1;
    }

    private void FixedUpdate()
    {
        VendoJogador = EnxergaJogador();
    }

    private bool EnxergaJogador()
    {
        Vector2 origem = olho.position;
        Vector2 paraJogador = (Vector2)jogador.position - origem;

        if (paraJogador.sqrMagnitude > alcance * alcance)
        {
            return false;
        }

        Vector2 frente = new Vector2(direcaoOlhar, 0f);
        if (Vector2.Angle(frente, paraJogador) > anguloVisao)
        {
            return false;
        }

        RaycastHit2D hit = Physics2D.Raycast(origem, paraJogador.normalized, alcance, layersVisao);
        bool viu = hit.collider != null && hit.transform == jogador;
        Vector2 fimLinha = hit.collider != null ? hit.point : origem + paraJogador;
        Debug.DrawLine(origem, fimLinha, viu ? Color.yellow : Color.gray);

        return viu;
    }
}

O olho é um objeto filho posicionado na altura dos olhos e fora do collider do inimigo. Se ele ficasse dentro, o raio acertaria o próprio inimigo primeiro (mais sobre isso adiante). A comparação hit.transform == jogador assume que o collider está no mesmo objeto do Transform do jogador; se o collider estiver num filho, compare com hit.transform.root ou use hit.rigidbody.transform.

Se a lógica de inimigo com raycast no Godot te interessa, o post de RayCast2D no Godot monta o mesmo cone de visão com produto escalar, e a matemática é idêntica.

Ground check, visão e tiro funcionam bem isolados numa cena de teste. No jogo, eles precisam conversar com a máquina de estados do inimigo, as animações, o dano e o level design, e é nesse encaixe que a maior parte dos projetos solo trava. O curso de Unity constrói esse conjunto dentro de um jogo 2D completo, do primeiro raio ao inimigo que patrulha, persegue e ataca.

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

Linha de tiro e LineRenderer

Para tiro instantâneo em 2D (laser, arma de raio, mira de sniper), o raycast decide onde o tiro acerta e um LineRenderer desenha a linha por um instante.

Arquivo TiroLaser2D.cs:

using System.Collections;
using UnityEngine;
using UnityEngine.InputSystem;

public class TiroLaser2D : MonoBehaviour
{
    [SerializeField] private Transform canoArma;
    [SerializeField] private LineRenderer linha;
    [SerializeField] private float alcance = 15f;
    [SerializeField] private float duracaoLinha = 0.05f;
    [SerializeField] private LayerMask layersAtingiveis; // Inimigo + Cenario

    private InputAction acaoAtirar;

    private void Awake()
    {
        acaoAtirar = InputSystem.actions.FindAction("Attack");
        linha.positionCount = 2;
        linha.enabled = false;
    }

    private void Update()
    {
        if (acaoAtirar.WasPressedThisFrame())
        {
            Atirar();
        }
    }

    private void Atirar()
    {
        Vector2 origem = canoArma.position;
        Vector2 direcao = canoArma.right; // o cano aponta para o eixo X local

        RaycastHit2D hit = Physics2D.Raycast(origem, direcao, alcance, layersAtingiveis);
        Vector2 fim = hit ? hit.point : origem + direcao * alcance;

        if (hit)
        {
            Debug.Log($"Laser acertou {hit.collider.name}");
        }

        StartCoroutine(MostrarLinha(origem, fim));
    }

    private IEnumerator MostrarLinha(Vector2 inicio, Vector2 fim)
    {
        linha.SetPosition(0, inicio);
        linha.SetPosition(1, fim);
        linha.enabled = true;
        yield return new WaitForSeconds(duracaoLinha);
        linha.enabled = false;
    }
}

O canoArma é um objeto filho na ponta da arma. Usar canoArma.right como direção faz o tiro seguir a rotação da arma: gire a arma para mirar no mouse e o raio acompanha. Configure o LineRenderer com largura pequena (0,05) e um material Sprites-Default para ele aparecer no 2D.

BoxCast e CircleCast

Physics2D.BoxCast e Physics2D.CircleCast arrastam uma caixa ou um círculo ao longo da direção, em vez de uma linha. Retornam o mesmo RaycastHit2D.

  • BoxCast para ground check: uma caixa com a largura do pé, arrastada um pouco para baixo, cobre toda a sola com uma chamada. Mais robusta em chão irregular e degraus pequenos do que dois raios.
RaycastHit2D chao = Physics2D.BoxCast(
    corpo.bounds.center, new Vector2(corpo.bounds.size.x * 0.9f, 0.05f),
    0f, Vector2.down, corpo.bounds.extents.y + 0.05f, layersChao);
  • CircleCast para projétil com espessura: uma bola de fogo grande deveria acertar quem passa de raspão. Um raio fino no centro não acerta.

A desvantagem é depurar: Debug.DrawRay mostra só uma linha, e o formato arrastado você precisa imaginar ou desenhar com Gizmos.

Quando você precisa de todos os acertos na linha (laser que atravessa vários inimigos), use a sobrecarga com ContactFilter2D e uma lista que você reaproveita. Ela não cria array novo a cada chamada e devolve os resultados do mais perto para o mais longe:

private readonly List<RaycastHit2D> acertos = new List<RaycastHit2D>();

private void LaserPerfurante(Vector2 origem, Vector2 direcao)
{
    ContactFilter2D filtro = new ContactFilter2D();
    filtro.SetLayerMask(layersAtingiveis);
    filtro.useTriggers = false;

    int quantidade = Physics2D.Raycast(origem, direcao, filtro, acertos, 15f);
    for (int i = 0; i < quantidade; i++)
    {
        Debug.Log($"Laser atravessou {acertos[i].collider.name}");
    }
}

O arquivo precisa de using System.Collections.Generic; para a List. O useTriggers = false faz esse raio ignorar triggers sem mexer na opção global do projeto.

Erros comuns: acertar o próprio collider

O raio acerta o próprio jogador. É o erro mais comum do raycast 2D. Em Project Settings > Physics 2D, a opção Queries Start In Colliders vem ligada, e com ela o raycast detecta o collider dentro do qual começa. A documentação descreve esse caso: o acerto vem com fraction zero e a normal invertida em relação ao raio. Como o raio sai do centro do jogador, o primeiro acerto é sempre ele. Três correções, da mais limpa para a mais drástica:

  1. Deixe a layer do jogador fora da máscara do raycast (como no GroundCheck2D).
  2. Comece o raio fora do collider, num objeto filho posicionado além da borda (como o olho e o canoArma).
  3. Desligue Queries Start In Colliders no projeto. Afeta todas as consultas, então só faça se você entende o impacto.

Comparar RaycastHit2D com null. Struct nunca é null. Use hit.collider != null ou if (hit).

Usar Physics.Raycast num jogo 2D. A física 3D não enxerga Collider2D. O raio nunca acerta nada.

Trigger parando o raio. A opção Queries Hit Triggers (também em Physics 2D) vem ligada: zonas trigger invisíveis bloqueiam visão e tiro. Desligue no projeto ou deixe essas zonas numa layer fora da máscara.

Distância em pixels. A distância é em unidades do mundo, não em pixels. Um sprite de 32 px com Pixels Per Unit 32 tem 1 unidade. Uma distância de 32 no raycast é um raio de 32 personagens.

Próximo passo

Monte uma cena com uma plataforma em L, o GroundCheck2D no jogador e o VisaoInimigo2D num inimigo do outro lado de uma parede. Ande com o jogador até a borda da plataforma e veja os dois raios mudando de cor. Depois coloque a layer do jogador na máscara do ground check e veja o bug do próprio collider aparecer: o jogador "está no chão" em pleno ar.

Com os dois raios funcionando, troque o ground check pelo BoxCast e compare numa rampa e num degrau pequeno: anote qual dos dois dá menos falso "no ar". Por fim, ligue o TiroLaser2D numa arma que gira para o mouse e coloque um trigger decorativo no caminho, para ver a opção Queries Hit Triggers em ação antes que ela vire bug no seu jogo.

Perguntas frequentes

Qual a diferença entre Physics.Raycast e Physics2D.Raycast?

Physics.Raycast é da física 3D, retorna bool e preenche um RaycastHit por out. Physics2D.Raycast é da física 2D, recebe Vector2 e retorna direto um RaycastHit2D. Cada um só enxerga os colliders do próprio sistema: um raycast 2D nunca acerta um BoxCollider 3D.

Por que o Physics2D.Raycast acerta o próprio jogador?

Com a opção Queries Start In Colliders ligada (o padrão em Project Settings > Physics 2D), o raycast 2D detecta o collider em que começa. Como o raio sai de dentro do jogador, o primeiro acerto é ele mesmo. Tire a layer do jogador da máscara do raycast ou desligue essa opção.

Como saber se o RaycastHit2D acertou algo?

RaycastHit2D é uma struct, então nunca é null. Confira hit.collider != null, ou use if (hit), que a struct converte para bool. Comparar o hit com null não funciona.

Raycast ou BoxCast para ground check em 2D?

Dois raycasts nas bordas do pé resolvem a maioria dos jogos de plataforma e mostram exatamente onde testam. BoxCast cobre toda a largura do pé com uma chamada só e é mais robusto em chão irregular. Os dois funcionam; escolha o que você consegue depurar melhor.