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.
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:
- Esquerda ou direita? Em 3D,
Vector3.Cross(transform.forward, direcaoDoAlvo).ypositivo significa que o alvo está à direita; negativo, à esquerda. Útil para uma torreta decidir para que lado girar. - Normal de uma superfície: com dois lados de um triângulo, o
Crossdá 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;



