Voltar para o Blog
Quest Log

Vector3 na Unity: Distance, MoveTowards, Normalize e Dot

Inimigo em patrulha entre dois pontos com setas de direção e um cone de visão apontado para o jogador

Unity Vector3 aplicado a mecânicas: Distance para alcance de ataque, MoveTowards na patrulha, normalized, Dot para visão e sqrMagnitude, com C# do Unity 6.

Ferramenta:UnityC#

Vector3 na Unity: Distance, MoveTowards, Normalize e Dot

Unity Vector3 é a struct que guarda três números (x, y, z) e representa tanto uma posição quanto uma direção ou um deslocamento. A maior parte das mecânicas de jogo se resolve com cinco operações dela: Distance para alcance, MoveTowards para ir até um ponto, normalized para direção, Dot para saber se algo está na frente e sqrMagnitude para comparar distâncias sem gastar CPU.

Este post passa por cada uma com um caso de jogo e código C# do Unity 6.3 LTS: um inimigo que patrulha, enxerga o jogador num cone e ataca quando está perto. A matemática por trás (soma, subtração, comprimento) está explicada sem susto em matemática para jogos: vetores; aqui o foco é a API da Unity.

Vector3 na Unity: o que representa

O mesmo tipo serve para três coisas diferentes, e saber qual delas você tem em mãos evita metade dos bugs:

  • Posição: um ponto no mundo. transform.position é um Vector3 de posição.
  • Direção: para onde algo aponta, com comprimento 1. transform.forward, transform.right, Vector3.up.
  • Deslocamento: a diferença entre duas posições, com direção e distância juntas. alvo.position - transform.position.

A regra que liga as três: posição menos posição dá deslocamento, e deslocamento com comprimento ajustado para 1 vira direção.

A Unity já traz os vetores mais usados como atalhos estáticos: Vector3.zero (0, 0, 0), Vector3.one (1, 1, 1), Vector3.up, Vector3.right e Vector3.forward, que são as direções do mundo. Não confunda com transform.up, transform.right e transform.forward, que são as direções do objeto e giram junto com ele. Um inimigo girado 180 graus no eixo Y para olhar para a esquerda tem transform.right apontando para a esquerda, enquanto Vector3.right continua apontando para a direita do mundo.

Em jogo 2D, você usa Vector2 ou Vector3 com z fixo. Os métodos são os mesmos, e a Unity converte entre os dois automaticamente (o z é descartado ou vira 0).

Um ponto que pega todo mundo: Vector3 é struct, um tipo de valor. Isso explica o erro de compilação mais comum com vetores:

// Não compila: transform.position devolve uma cópia.
// transform.position.x = 5f;

// Certo: copia, altera, devolve.
Vector3 p = transform.position;
p.x = 5f;
transform.position = p;

transform.position é uma propriedade que entrega uma cópia do vetor. Alterar o x dessa cópia não mudaria nada no objeto, então o compilador recusa. A saída é a de três linhas acima.

Direção e normalized

Para fazer um projétil ou um inimigo ir em direção a algo, você precisa da direção. Subtraia as posições e normalize:

Vector3 offset = target.position - transform.position;
Vector3 direction = offset.normalized;

normalized devolve um vetor com o mesmo sentido e comprimento 1. Isso importa porque o offset cru carrega a distância junto: se o alvo está a 10 unidades, offset * speed faria o inimigo andar dez vezes mais rápido do que se o alvo estivesse a 1 unidade. Com a direção normalizada, direction * speed dá sempre a mesma velocidade.

Existe também Vector3.Normalize(v) (estático) e v.Normalize() (que altera a própria variável). normalized é o mais usado porque não mexe no original.

Cuidado com o vetor zero: se o inimigo está exatamente em cima do alvo, offset é (0, 0, 0) e o normalized devolve zero. Não dá erro, mas a direção some. Em código de mira, trate o caso de distância zero antes.

Distance e sqrMagnitude

O inimigo ataca quando o jogador está dentro do alcance. Arquivo AttackRange.cs:

using UnityEngine;

public class AttackRange : MonoBehaviour
{
    [SerializeField] private Transform player;
    [SerializeField] private float attackRange = 1.5f;

    public bool PlayerInRange()
    {
        float distance = Vector3.Distance(transform.position, player.position);
        return distance <= attackRange;
    }

    public bool PlayerInRangeFast()
    {
        Vector3 offset = player.position - transform.position;
        return offset.sqrMagnitude <= attackRange * attackRange;
    }
}

Vector3.Distance(a, b) é o mesmo que (a - b).magnitude: o comprimento do deslocamento. Para calcular comprimento, a conta usa uma raiz quadrada.

sqrMagnitude é o comprimento ao quadrado, sem a raiz. Se você só quer comparar com um alcance, comparar os quadrados dá exatamente o mesmo resultado: se a distância é menor que 1,5, a distância ao quadrado é menor que 2,25. É mais barato, e a diferença aparece quando são centenas de inimigos checando alcance todo frame.

Regra de uso: precisa do número da distância (para mostrar na tela, para calcular dano por distância), use Distance. Só precisa saber se está perto, use sqrMagnitude contra o alcance ao quadrado.

MoveTowards para ir até um ponto

Patrulha entre pontos é o uso perfeito de MoveTowards. Arquivo Patrol.cs:

using UnityEngine;

public class Patrol : MonoBehaviour
{
    [SerializeField] private Transform[] waypoints;
    [SerializeField] private float speed = 2.5f;

    private int currentIndex;

    private void Update()
    {
        if (waypoints.Length == 0)
        {
            return;
        }

        Vector3 target = waypoints[currentIndex].position;
        transform.position = Vector3.MoveTowards(transform.position, target, speed * Time.deltaTime);

        if (transform.position == target)
        {
            currentIndex = (currentIndex + 1) % waypoints.Length;
        }
    }
}

MoveTowards(atual, alvo, passoMaximo) devolve uma posição que andou no máximo passoMaximo em direção ao alvo. A documentação garante que ele não ultrapassa o alvo: quando falta menos que o passo, ele devolve o alvo exato. Por isso a comparação transform.position == target funciona aqui (o == de Vector3 da Unity já tolera diferenças minúsculas de ponto flutuante).

O passo é speed * Time.deltaTime porque speed está em unidades por segundo. O % waypoints.Length faz o índice voltar a 0 depois do último ponto, em loop.

Compare com o Lerp: Vector3.Lerp(atual, alvo, 0.1f) a cada frame anda 10% do que falta, desacelera perto do fim e não chega com velocidade constante. Para patrulha, velocidade constante e chegada garantida são o que você quer, e é isso que o MoveTowards entrega. A diferença completa está em Lerp na Unity.

Se o inimigo usa Rigidbody2D, troque a atribuição direta por body.MovePosition(Vector2.MoveTowards(body.position, target, speed * Time.fixedDeltaTime)) dentro do FixedUpdate.

Patrulha, alcance e visão separados são três scripts simples. Juntar os três num inimigo com estados (patrulhando, perseguindo, atacando, voltando), animação para cada estado e um jogador que reage é o ponto em que muita gente que aprende sozinha fica com um inimigo que funciona pela metade.

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

Dot: o inimigo está na minha frente?

Produto escalar (Vector3.Dot) entre duas direções normalizadas devolve um número entre -1 e 1:

  • 1: apontam para o mesmo lado;
  • 0: são perpendiculares;
  • -1: apontam para lados opostos.

Em jogo, isso responde "o alvo está na frente de mim?" sem calcular ângulo. Arquivo VisionCone.cs:

using UnityEngine;

public class VisionCone : MonoBehaviour
{
    [SerializeField] private Transform player;
    [SerializeField] private float viewDistance = 8f;
    [SerializeField, Range(0f, 180f)] private float viewAngle = 60f;

    public bool CanSeePlayer()
    {
        Vector3 offset = player.position - transform.position;

        if (offset.sqrMagnitude > viewDistance * viewDistance)
        {
            return false;
        }

        Vector3 toPlayer = offset.normalized;
        float dot = Vector3.Dot(transform.right, toPlayer);
        float threshold = Mathf.Cos(viewAngle * 0.5f * Mathf.Deg2Rad);

        return dot >= threshold;
    }
}

Num jogo 2D de visão lateral, a "frente" do inimigo costuma ser transform.right; em 3D, transform.forward. O cone de 60 graus significa 30 para cada lado, e o cosseno de 30 graus é cerca de 0,866. Se o Dot for maior ou igual a isso, o jogador está dentro do cone.

A ordem das checagens é proposital: primeiro a distância com sqrMagnitude (barata), e só se estiver perto calcula a direção e o Dot. Para garantir que nenhuma parede bloqueia a visão, o passo seguinte seria um Physics2D.Raycast do inimigo até o jogador.

Outro uso do Dot: ataque pelas costas. Dot(alvo.forward, direcaoDoAtacante) maior que zero significa que o atacante está atrás do alvo, olhando na mesma direção que ele.

Cross e quando você vai precisar

Produto vetorial (Vector3.Cross(a, b)) devolve um vetor perpendicular aos dois. Ele aparece menos, mas resolve dois problemas específicos:

  1. Esquerda ou direita? Em 3D, Vector3.Cross(transform.forward, direcaoDoAlvo).y positivo significa que o alvo está à direita; negativo, à esquerda. Útil para uma torreta decidir para que lado girar.
  2. Normal de uma superfície: com dois lados de um triângulo, o Cross dá a direção perpendicular a ele. Aparece em geração de malha e em alinhar um personagem ao chão inclinado.

Em 2D, a mesma pergunta de esquerda e direita se responde com o z do Cross, ou com Vector2.SignedAngle. Se você está começando, guarde só que ele existe; o dia em que precisar, vai ser para uma dessas duas coisas.

Erros comuns com Vector3

Usar o deslocamento cru como direção. (alvo - eu) * speed sem normalized: o objeto corre mais quando está longe e quase para quando está perto.

Comparar distância com ==. Vector3.Distance(a, b) == 0f raramente é verdade com float. Compare com um valor pequeno ou use o MoveTowards, que chega no alvo exato.

Esquecer o Time.deltaTime no passo do MoveTowards. Sem ele, o passo é por frame, e o inimigo anda mais rápido em PC com mais FPS. O conceito está em delta time e movimento por frame.

Normalizar um vetor zero. Mira que trava quando o jogador fica exatamente em cima do inimigo. Cheque sqrMagnitude > 0.0001f antes.

Dot com vetores não normalizados. O resultado sai multiplicado pelos comprimentos, e a comparação com o cosseno deixa de fazer sentido.

Misturar forward e right entre 2D e 3D. Em sprite 2D, forward aponta para dentro da tela (eixo Z). A frente do personagem é right ou up, conforme a arte.

Próximo passo

Junte os três scripts num inimigo só: Patrol ligado por padrão, VisionCone checando a cada frame e, quando enxergar, troca para perseguir com MoveTowards até o AttackRange dizer que está perto. Desenhe o cone no editor com Debug.DrawRay para ver o que o inimigo enxerga. Depois, leia Lerp na Unity para suavizar a câmera e a UI, que é onde a interpolação brilha.

Perguntas frequentes

Como calcular a distância entre dois objetos na Unity?

Use Vector3.Distance(a.position, b.position), que devolve a distância em unidades de mundo. Se você só precisa comparar com um alcance, compare (a - b).sqrMagnitude com alcance * alcance, que dá o mesmo resultado sem calcular raiz quadrada.

Vector3.MoveTowards passa do alvo?

Não. A documentação garante que MoveTowards não ultrapassa o alvo: quando a distância restante é menor que o passo, ele devolve exatamente a posição do alvo. Isso permite checar a chegada com igualdade ou com uma distância bem pequena.

Para que serve Vector3.Dot em jogo?

Dot entre duas direções normalizadas devolve 1 quando elas apontam para o mesmo lado, 0 quando são perpendiculares e -1 quando são opostas. É o jeito barato de saber se o alvo está na frente, atrás ou do lado de um personagem, e de montar um cone de visão.

Por que não consigo mudar transform.position.x direto?

Porque Vector3 é uma struct e transform.position devolve uma cópia. Mudar o x da cópia não teria efeito, então o compilador bloqueia. Copie para uma variável, altere o x e atribua de volta: Vector3 p = transform.position; p.x = 5f; transform.position = p;