Campo de Visão do Inimigo na Unity: Ver, Ouvir e Perder
Um Unity field of view enemy precisa de três checagens, nesta ordem: o jogador está perto o bastante (raio de visão), está na frente (ângulo dentro do cone, com Vector3.Angle) e não tem parede no meio (um Physics.Raycast com a LayerMask das paredes). Aqui você monta um inimigo de stealth que vê com esse cone, ouve barulho por raio e intensidade, e perde o jogador com tempo de memória, indo até a última posição vista. Tem também o cone desenhado no editor e a versão 2D com Physics2D.
Perder o jogador é a parte que transforma detecção em jogo. Um guarda que esquece instantaneamente é bobo; um que nunca esquece é injusto. A memória com tempo é o que cria a tensão de se esconder.
Campo de visão do inimigo na Unity: as três checagens
A ordem das checagens é uma decisão de desempenho:
- Distância: comparar distância ao quadrado é quase de graça. A maior parte do tempo, o jogador está longe e o teste termina aqui.
- Ângulo: uma conta de vetor. Elimina o jogador que está perto, mas atrás.
- Linha de visão: o Raycast é a checagem mais cara, porque consulta a física. Só roda quando as duas primeiras passaram.
Os vetores por trás (direção, normalização, ângulo entre dois vetores) estão explicados com desenho em matemática para jogos: vetores. E o design do stealth em si (estados de suspeita, esconderijos, como o jogador lê o guarda) é tema do post sobre como fazer jogo stealth. Aqui o foco é a implementação na Unity.
Distância e ângulo com Vector3.Angle
Vector3.Angle(a, b) devolve o ângulo em graus entre dois vetores, sempre entre 0 e 180. Se o inimigo tem um cone de 90 graus, o jogador está dentro quando o ângulo entre transform.forward e a direção até o jogador é menor que 45, a metade do cone. Esquecer a metade é o erro mais comum: o cone fica com o dobro do tamanho.
Prepare as layers antes do código: crie Player e Obstacle em Edit > Project Settings > Tags and Layers, coloque o jogador em Player e paredes, caixas e portas em Obstacle.
Crie EnemyVision.cs:
using UnityEngine;
public class EnemyVision : MonoBehaviour
{
[SerializeField] private Transform eye;
[SerializeField] private float viewRadius = 12f;
[SerializeField, Range(0f, 360f)] private float viewAngle = 90f;
[SerializeField] private LayerMask targetMask;
[SerializeField] private LayerMask obstacleMask;
[SerializeField] private float targetHeightOffset = 1.2f;
private readonly Collider[] overlapBuffer = new Collider[8];
public float ViewRadius => viewRadius;
public float ViewAngle => viewAngle;
public bool TrySeeTarget(out Transform seenTarget)
{
seenTarget = null;
Vector3 eyePosition = eye != null ? eye.position : transform.position;
int count = Physics.OverlapSphereNonAlloc(eyePosition, viewRadius, overlapBuffer, targetMask);
for (int i = 0; i < count; i++)
{
Transform candidate = overlapBuffer[i].transform;
Vector3 targetPoint = candidate.position + Vector3.up * targetHeightOffset;
Vector3 toTarget = targetPoint - eyePosition;
Vector3 flatToTarget = new Vector3(toTarget.x, 0f, toTarget.z);
Vector3 flatForward = new Vector3(transform.forward.x, 0f, transform.forward.z);
if (Vector3.Angle(flatForward, flatToTarget) > viewAngle * 0.5f)
{
continue;
}
float distance = toTarget.magnitude;
if (Physics.Raycast(eyePosition, toTarget / distance, distance, obstacleMask, QueryTriggerInteraction.Ignore))
{
continue;
}
seenTarget = candidate;
return true;
}
return false;
}
}
Monte o inimigo com um filho vazio Olho na altura da cabeça (cerca de 1,6 m num humanoide) e arraste para o campo eye. Em Target Mask, só Player; em Obstacle Mask, só Obstacle.
Detalhes do código:
OverlapSphereNonAlloccom buffer acha quem está no raio sem criar array novo a cada chamada. A documentação descreve como a versão deOverlapSphereque não gera lixo para o coletor, e avisa que o buffer não cresce: com 8 posições, ele devolve no máximo 8. Para um jogador, sobra. Se o jogo tem aliados e iscas na mesma layer, aumente.- Ângulo no plano horizontal. Zerar o Y antes do
Vector3.Angleevita que o jogador num andar de cima ou de baixo fique "fora do cone" só pela inclinação. Se o seu jogo tem visão vertical (guarda numa torre), use o vetor completo e um segundo ângulo vertical. targetHeightOffsetmira o raio no peito do jogador, não no pé. Raio no pé bate em degrau, mureta e no próprio chão.
Linha de visão com Raycast e LayerMask
O Raycast vai do olho até o ponto do jogador com a distância exata entre os dois. Se bater em qualquer coisa da obstacleMask antes de chegar, a visão está bloqueada.
Três cuidados:
- A máscara contém só obstáculos. Se a máscara incluísse o jogador, o raio bateria no jogador e você teria que comparar o que atingiu. Com a máscara só de paredes, a lógica fica binária: bateu, não vê.
QueryTriggerInteraction.Ignore. Zonas de trigger (área de diálogo, checkpoint) não bloqueiam visão.- Vidro, grade e folhagem. Coloque numa layer fora da
obstacleMaskse o guarda deve ver através. Folhagem densa que esconde é escolha de design: pode bloquear visão e não bloquear tiro.
Para ir além (um raio para a cabeça e outro para o tronco, visibilidade parcial), lance dois ou três raios para pontos diferentes do jogador e conte quantos passaram. Visibilidade de 1 em 3 pode significar "suspeito" em vez de "viu".
Desenhar o cone com Gizmos/Handles
Um cone que você não vê é um cone que você não ajusta. Handles.DrawSolidArc desenha um setor preenchido, mas Handles é do editor, então o código fica protegido com #if UNITY_EDITOR. Acrescente dentro da classe EnemyVision:
#if UNITY_EDITOR
private void OnDrawGizmosSelected()
{
Vector3 origin = eye != null ? eye.position : transform.position;
Vector3 flatForward = new Vector3(transform.forward.x, 0f, transform.forward.z).normalized;
Vector3 startDirection = Quaternion.AngleAxis(-viewAngle * 0.5f, Vector3.up) * flatForward;
UnityEditor.Handles.color = new Color(1f, 0.85f, 0.2f, 0.15f);
UnityEditor.Handles.DrawSolidArc(origin, Vector3.up, startDirection, viewAngle, viewRadius);
UnityEditor.Handles.color = new Color(1f, 0.85f, 0.2f, 0.8f);
UnityEditor.Handles.DrawWireArc(origin, Vector3.up, startDirection, viewAngle, viewRadius);
}
#endif
O arco começa em metade do ângulo para a esquerda da frente (Quaternion.AngleAxis com ângulo negativo em volta do Y) e varre o ângulo inteiro. Selecione o inimigo e o cone aparece no chão da Scene view. Ajuste viewAngle e viewRadius olhando para a fase: o cone deve cobrir o que o guarda vigia e deixar a sombra por onde o jogador passa.
Montar o cone é satisfatório. Fazer o stealth funcionar como jogo é outra coisa: guardas com rotas que se cruzam, estados de suspeita com feedback claro, animação de olhar em volta, som, UI de alerta e uma fase desenhada em volta disso. É nesse encaixe que muita gente trava sozinha, porque cada sistema parece pronto isolado e quebra quando encontra os outros.
Audição: ruído com raio e intensidade
Visão diz onde o jogador está. Audição diz onde algo aconteceu. O guarda que ouve vai até o ponto do som, e o jogador ganha uma ferramenta: jogar uma garrafa para distrair.
O barulho é um evento: quem faz barulho avisa os inimigos próximos. Crie NoiseEmitter.cs:
using UnityEngine;
public static class NoiseEmitter
{
private static readonly Collider[] Buffer = new Collider[32];
public static void Emit(Vector3 position, float loudness, LayerMask listenerMask)
{
int count = Physics.OverlapSphereNonAlloc(position, loudness, Buffer, listenerMask, QueryTriggerInteraction.Collide);
for (int i = 0; i < count; i++)
{
EnemyAwareness listener = Buffer[i].GetComponentInParent<EnemyAwareness>();
if (listener != null)
{
float distance = Vector3.Distance(position, listener.transform.position);
float intensity = 1f - Mathf.Clamp01(distance / loudness);
listener.HearNoise(position, intensity);
}
}
}
}
O loudness é o raio em metros. Andar agachado não chama nada; andar normal, 3 m; correr, 8 m; cair de altura ou quebrar vidro, 15 m. A intensity vai de 1 (colado) a 0 (no limite), e o inimigo pode reagir diferente a barulho fraco e forte. No script de movimento do jogador, chame NoiseEmitter.Emit(transform.position, 8f, enemyMask) a cada passo de corrida (por evento de animação, por exemplo), não a cada frame.
Perder o jogador: memória e última posição vista
Agora o cérebro que junta visão e audição. Crie EnemyAwareness.cs:
using UnityEngine;
[RequireComponent(typeof(EnemyVision))]
public class EnemyAwareness : MonoBehaviour
{
public enum AwarenessState { Unaware, Suspicious, Alert }
[SerializeField] private float memoryDuration = 4f;
[SerializeField] private float suspicionToAlert = 1f;
[SerializeField] private float suspicionGainPerSecond = 2f;
[SerializeField] private float noiseSuspicionThreshold = 0.5f;
private EnemyVision vision;
private float suspicion;
private float timeSinceSeen = float.MaxValue;
public AwarenessState State { get; private set; } = AwarenessState.Unaware;
public Vector3 LastKnownPosition { get; private set; }
private void Awake()
{
vision = GetComponent<EnemyVision>();
}
private void Update()
{
if (vision.TrySeeTarget(out Transform target))
{
LastKnownPosition = target.position;
timeSinceSeen = 0f;
suspicion = Mathf.Min(suspicion + suspicionGainPerSecond * Time.deltaTime, suspicionToAlert);
State = suspicion >= suspicionToAlert ? AwarenessState.Alert : AwarenessState.Suspicious;
return;
}
timeSinceSeen += Time.deltaTime;
if (State == AwarenessState.Alert && timeSinceSeen > memoryDuration)
{
State = AwarenessState.Suspicious;
}
else if (State == AwarenessState.Suspicious && timeSinceSeen > memoryDuration * 2f)
{
State = AwarenessState.Unaware;
suspicion = 0f;
}
}
public void HearNoise(Vector3 position, float intensity)
{
if (State == AwarenessState.Alert)
{
LastKnownPosition = position;
return;
}
if (intensity >= noiseSuspicionThreshold || State == AwarenessState.Suspicious)
{
LastKnownPosition = position;
State = AwarenessState.Suspicious;
timeSinceSeen = 0f;
}
}
}
Como funciona:
- Suspeita acumula. Ver o jogador por um instante não dispara o alarme; o medidor sobe com o tempo de exposição. Com
suspicionGainPerSecond = 2e limite 1, são 0,5 segundo de visão contínua para o alerta. Dá ao jogador a chance de sair do cone. LastKnownPositioné para onde o inimigo vai quando perde o jogador. Ele não sabe onde o jogador está agora; sabe onde viu ou ouviu por último. O script de movimento (um NavMeshAgent, por exemplo) lê o estado e o ponto: em Alert persegue, em Suspicious vai até o ponto e olha em volta, em Unaware volta à patrulha.- Memória em dois degraus. Depois de
memoryDurationsem ver, o Alert cai para Suspicious (procura); depois do dobro, volta a Unaware. É o "deve ter sido o vento" dos jogos de stealth.
Exiba o estado para o jogador: um ícone de interrogação em Suspicious e de exclamação em Alert. Stealth sem feedback é frustração.
Versão 2D com Physics2D.Raycast
Em 2D (top-down ou plataforma), a lógica é a mesma, com APIs de física 2D e a "frente" do inimigo vindo da direção para onde ele olha. Crie EnemyVision2D.cs:
using System.Collections.Generic;
using UnityEngine;
public class EnemyVision2D : MonoBehaviour
{
[SerializeField] private float viewRadius = 7f;
[SerializeField, Range(0f, 360f)] private float viewAngle = 80f;
[SerializeField] private LayerMask targetMask;
[SerializeField] private LayerMask obstacleMask;
private readonly List<Collider2D> overlapResults = new List<Collider2D>();
private ContactFilter2D targetFilter;
public Vector2 Facing { get; set; } = Vector2.right;
private void Awake()
{
targetFilter = new ContactFilter2D();
targetFilter.SetLayerMask(targetMask);
targetFilter.useTriggers = false;
}
public bool TrySeeTarget(out Transform seenTarget)
{
seenTarget = null;
Vector2 origin = transform.position;
int count = Physics2D.OverlapCircle(origin, viewRadius, targetFilter, overlapResults);
for (int i = 0; i < count; i++)
{
Vector2 toTarget = (Vector2)overlapResults[i].bounds.center - origin;
if (Vector2.Angle(Facing, toTarget) > viewAngle * 0.5f)
{
continue;
}
RaycastHit2D hit = Physics2D.Raycast(origin, toTarget.normalized, toTarget.magnitude, obstacleMask);
if (hit.collider != null)
{
continue;
}
seenTarget = overlapResults[i].transform;
return true;
}
return false;
}
}
Diferenças para o 3D:
Facingé explícito. Em plataforma, atualize comVector2.rightouVector2.leftquando o sprite vira; em top-down, com a direção do último movimento (rb.linearVelocity.normalized, se não for zero).Physics2D.OverlapCirclecomContactFilter2DeListdevolve a quantidade de colliders e reaproveita a lista. A documentação do Unity 6.3 diz que a lista cresce se faltar espaço, então não há limite fixo como no buffer do 3D.Physics2D.Raycastdevolve umRaycastHit2D, e o teste éhit.collider != null, não um bool.- Mire no centro do collider (
bounds.center) para não lançar o raio no pé do sprite.
O EnemyAwareness funciona igual com a versão 2D: troque o tipo do campo de visão (ou crie uma interface comum com TrySeeTarget) e use Physics2D.OverlapCircle no emissor de ruído.
Erros comuns: inimigo vendo através da parede
Vê através da parede. A parede não está na obstacleMask, não tem collider, ou o collider é trigger e o Raycast ignora triggers. Confira a layer e o collider da parede.
Nunca vê o jogador. O jogador não está na targetMask, ou, na versão 2D, o raio sai de dentro do collider do próprio inimigo e bate nele (com Queries Start In Colliders ligado, o padrão da física 2D; a física 3D ignora o collider de onde o raio parte). Coloque o inimigo numa layer fora da obstacleMask.
Cone com o dobro do tamanho. Comparação com viewAngle em vez de viewAngle * 0.5f.
Vê o jogador agachado atrás de mureta. O raio mira no ponto errado. Com o jogador agachado, reduza o targetHeightOffset para a altura real da cabeça dele.
Detecção instantânea e injusta. Sem acúmulo de suspeita. Use o medidor de exposição.
Inimigo 2D "olha" para o lado errado. Facing não foi atualizado quando o sprite virou.
Próximo passo
Monte uma sala com três caixas, um guarda com EnemyVision e EnemyAwareness, e faça o jogador correr (barulho alto) e andar (sem barulho). Exiba o estado acima da cabeça do guarda com um texto. Quando você conseguir passar por ele usando as caixas e o silêncio, o sistema está pronto para virar fase. O próximo passo é a linha de visão para outros usos (tiro, interação), assunto do post de Raycast na Unity.
Perguntas frequentes
Como fazer o campo de visão de um inimigo na Unity?
Faça três checagens em ordem: o jogador está dentro do raio de visão, o ângulo entre a frente do inimigo e a direção do jogador é menor que metade do ângulo do cone, e um Raycast do olho do inimigo até o jogador não bate em parede. Só se as três passarem, o inimigo vê.
Por que o inimigo vê o jogador através da parede?
Falta a checagem de linha de visão, ou o Raycast usa uma LayerMask que não inclui a layer das paredes. Confira também se a parede tem collider e se ele não é trigger. Lance o raio da altura dos olhos e com a máscara só de obstáculos.
Como fazer o inimigo ouvir o jogador na Unity?
Quando o jogador faz barulho (corre, cai, quebra algo), procure inimigos num raio proporcional à intensidade com Physics.OverlapSphere ou a versão com buffer e avise cada um da posição do som. O inimigo vai investigar o ponto, sem saber onde o jogador está agora.
Qual a diferença entre Vector3.Angle e produto escalar para o cone de visão?
Vector3.Angle devolve o ângulo em graus, fácil de ler e comparar com o ângulo do cone. O produto escalar (Vector3.Dot) de vetores normalizados devolve o cosseno, que é um pouco mais barato. Para poucos inimigos, use Angle pela clareza.



