Patrulha com Waypoints na Unity: Inimigo que Faz Ronda
Patrulha com waypoints (Unity patrol waypoints) é o inimigo andando por uma lista de pontos colocados na cena: vai até o ponto atual, espera, segue para o próximo. Você resolve com um array de Transform, Vector3.MoveTowards para andar e uma comparação de distância para saber que chegou. Aqui você monta um script de patrulha reutilizável, com modo loop e ida e volta, espera em cada ponto, rota desenhada no editor com Gizmos, uma versão com NavMeshAgent e a ligação com a perseguição por uma máquina de estados simples.
O foco aqui é só a patrulha. O que fazer quando o inimigo vê o jogador tem post próprio; esta página mostra como a ronda entrega o controle para a perseguição e como retoma depois sem parecer robô.
O que é patrulha por waypoints
Waypoint é um marcador de posição. Na Unity, é um GameObject vazio cuja única função é ter um Transform. O inimigo guarda uma lista desses marcadores e anda de um para o outro.
Por que GameObjects na cena e não coordenadas digitadas no script? Porque o level designer mexe na rota arrastando pontos na Scene view, sem abrir código, e a rota acompanha quando você move a sala inteira. É o padrão que quase todo jogo com guarda usa, de stealth a RPG.
Patrulha também é o primeiro estado de uma máquina de estados de inimigo. Se você quer o panorama da IA inteira (patrulhar, perseguir, atacar) antes de entrar no detalhe, o post de IA de inimigo: perseguir e patrulhar faz isso na Godot, com a mesma lógica.
Montar os waypoints na cena
- Crie um GameObject vazio
Rota_Guarda01na cena, sem relação com o inimigo. - Dentro dele, filhos vazios
WP_0,WP_1,WP_2,WP_3, posicionados no chão onde o guarda deve passar. - Dê um ícone a cada um para enxergar na Scene view: selecione, clique no cubo colorido no topo do Inspector e escolha um rótulo.
A rota não deve ser filha do inimigo. Se for, quando o inimigo anda, os pontos andam junto e ele nunca chega. Esse é o erro mais comum e o mais difícil de perceber, porque na Scene view tudo parece certo antes do Play.
Script de patrulha na Unity: andar até o próximo ponto
Crie WaypointPatrol.cs:
using UnityEngine;
public class WaypointPatrol : MonoBehaviour
{
public enum PatrolMode { Loop, PingPong }
[SerializeField] private Transform[] waypoints;
[SerializeField] private PatrolMode mode = PatrolMode.Loop;
[SerializeField] private float moveSpeed = 2.5f;
[SerializeField] private float turnSpeed = 10f;
[SerializeField] private float arriveThreshold = 0.1f;
[SerializeField] private float waitTime = 1.5f;
private int currentIndex;
private int direction = 1;
private float waitTimer;
public Transform CurrentWaypoint => waypoints.Length > 0 ? waypoints[currentIndex] : null;
private void Update()
{
if (waypoints == null || waypoints.Length == 0)
{
return;
}
if (waitTimer > 0f)
{
waitTimer -= Time.deltaTime;
return;
}
Vector3 targetPosition = waypoints[currentIndex].position;
targetPosition.y = transform.position.y;
Vector3 toTarget = targetPosition - transform.position;
if (toTarget.sqrMagnitude <= arriveThreshold * arriveThreshold)
{
waitTimer = waitTime;
AdvanceIndex();
return;
}
transform.position = Vector3.MoveTowards(transform.position, targetPosition, moveSpeed * Time.deltaTime);
Quaternion look = Quaternion.LookRotation(toTarget.normalized);
transform.rotation = Quaternion.Slerp(transform.rotation, look, turnSpeed * Time.deltaTime);
}
private void AdvanceIndex()
{
if (waypoints.Length == 1)
{
return;
}
if (mode == PatrolMode.Loop)
{
currentIndex = (currentIndex + 1) % waypoints.Length;
return;
}
int next = currentIndex + direction;
if (next < 0 || next >= waypoints.Length)
{
direction = -direction;
next = currentIndex + direction;
}
currentIndex = next;
}
public void ResumeFromNearest()
{
float bestSqr = float.MaxValue;
for (int i = 0; i < waypoints.Length; i++)
{
float sqr = (waypoints[i].position - transform.position).sqrMagnitude;
if (sqr < bestSqr)
{
bestSqr = sqr;
currentIndex = i;
}
}
waitTimer = 0f;
}
}
Coloque no inimigo e arraste WP_0 a WP_3 para o array Waypoints, na ordem.
As decisões do código:
Vector3.MoveTowardsanda no máximomoveSpeed * Time.deltaTimepor frame e nunca passa do alvo. ComLerpo inimigo desaceleraria perto do ponto e nunca chegaria exatamente; com soma de direção e velocidade, ele passaria do ponto e voltaria.targetPosition.y = transform.position.yignora a altura do marcador. Se o waypoint ficou 10 cm acima do chão, o inimigo não flutua até ele.sqrMagnitudecontraarriveThreshold * arriveThresholdcompara distâncias ao quadrado e evita a raiz quadrada doVector3.Distance. O resultado da comparação é o mesmo.ResumeFromNearesté a função que a máquina de estados chama ao voltar da perseguição. Explico na seção de estados.
Este script move pelo Transform, o que serve para inimigo sem física ou com Rigidbody Kinematic. Se o inimigo tem Rigidbody dinâmico, troque a atribuição de posição por rb.MovePosition dentro do FixedUpdate.
Loop vs ping-pong e espera em cada ponto
Loop fecha o circuito: 0, 1, 2, 3, 0, 1... Use quando a rota contorna algo (um pátio, um quarteirão, uma fogueira).
Ping-pong vai e volta: 0, 1, 2, 3, 2, 1, 0, 1... Use em linha reta: corredor, muralha, ponte. Um loop num corredor faria o guarda atravessar paredes do último ponto direto para o primeiro.
A espera é o que faz o guarda parecer gente. Andar sem parar parece máquina; parar 1 a 2 segundos em cada ponto (olhando em volta) dá ritmo e cria a janela que o jogador de stealth procura. Uma melhoria simples: espera diferente por ponto. Troque o float waitTime por um array float[] waitTimes do mesmo tamanho dos waypoints e use waitTimes[currentIndex] antes de avançar. O guarda fica 4 segundos no portão e 1 segundo nas esquinas.
Variação aleatória também ajuda: waitTimer = waitTime + Random.Range(-0.3f, 0.3f); quebra a sincronia quando vários guardas usam o mesmo tempo.
Desenhar a rota com OnDrawGizmos
Uma rota invisível no editor é uma rota que ninguém revisa. Acrescente este método dentro da classe WaypointPatrol:
private void OnDrawGizmos()
{
if (waypoints == null || waypoints.Length == 0)
{
return;
}
Gizmos.color = Color.cyan;
for (int i = 0; i < waypoints.Length; i++)
{
if (waypoints[i] == null)
{
continue;
}
Gizmos.DrawWireSphere(waypoints[i].position, 0.3f);
bool isLast = i == waypoints.Length - 1;
if (isLast && mode == PatrolMode.PingPong)
{
continue;
}
Transform next = waypoints[isLast ? 0 : i + 1];
if (next != null)
{
Gizmos.DrawLine(waypoints[i].position, next.position);
}
}
}
OnDrawGizmos roda no editor, com o objeto selecionado ou não, e não entra no build. A rota aparece como esferas ligadas por linhas; em ping-pong, a linha do último para o primeiro não é desenhada, porque o guarda nunca faz esse trecho. Se a cena tem muitos guardas e a vista fica poluída, troque para OnDrawGizmosSelected, que só desenha com o objeto selecionado.
Patrulha funcionando sozinha numa cena de teste é a parte fácil. O que costuma travar é o inimigo completo: ronda, perseguição, ataque, animação sincronizada, som de passos e vários guardas sem um atrapalhar o outro. Cada tutorial mostra uma peça; organizar todas num jogo que você consegue terminar e publicar é outra habilidade.
Versão com NavMeshAgent
Em 3D com obstáculos entre os pontos, deixe o NavMeshAgent calcular o caminho. Crie WaypointPatrolAgent.cs:
using UnityEngine;
using UnityEngine.AI;
[RequireComponent(typeof(NavMeshAgent))]
public class WaypointPatrolAgent : MonoBehaviour
{
[SerializeField] private Transform[] waypoints;
[SerializeField] private float waitTime = 1.5f;
[SerializeField] private float arriveDistance = 0.3f;
private NavMeshAgent agent;
private int currentIndex;
private float waitTimer;
private void Awake()
{
agent = GetComponent<NavMeshAgent>();
agent.autoBraking = true;
}
private void OnEnable()
{
if (agent != null && waypoints.Length > 0 && agent.isOnNavMesh)
{
agent.SetDestination(waypoints[currentIndex].position);
}
}
private void Update()
{
if (waypoints.Length == 0 || !agent.isOnNavMesh || agent.pathPending)
{
return;
}
if (agent.remainingDistance > Mathf.Max(arriveDistance, agent.stoppingDistance))
{
return;
}
waitTimer += Time.deltaTime;
if (waitTimer < waitTime)
{
return;
}
waitTimer = 0f;
currentIndex = (currentIndex + 1) % waypoints.Length;
agent.SetDestination(waypoints[currentIndex].position);
}
}
O detalhe crucial é o agent.pathPending. Logo depois de SetDestination, o caminho ainda está sendo calculado e remainingDistance não é confiável. Sem essa checagem, o script lê um valor errado, acha que chegou e pula para o próximo ponto no mesmo frame. A documentação do NavMeshAgent descreve remainingDistance como a distância restante no caminho atual, e só existe caminho atual depois que o cálculo termina.
Ligar a patrulha à perseguição com estados
A patrulha não deve saber que perseguição existe. Quem decide é um terceiro script, o "cérebro". Crie GuardBrain.cs:
using UnityEngine;
[RequireComponent(typeof(WaypointPatrol))]
public class GuardBrain : MonoBehaviour
{
private enum GuardState { Patrol, Chase }
[SerializeField] private Transform player;
[SerializeField] private float detectionRadius = 6f;
[SerializeField] private float loseRadius = 9f;
[SerializeField] private float chaseSpeed = 4.5f;
private WaypointPatrol patrol;
private GuardState state = GuardState.Patrol;
private void Awake()
{
patrol = GetComponent<WaypointPatrol>();
}
private void Update()
{
if (player == null)
{
return;
}
float sqrDistance = (player.position - transform.position).sqrMagnitude;
switch (state)
{
case GuardState.Patrol:
if (sqrDistance <= detectionRadius * detectionRadius)
{
state = GuardState.Chase;
patrol.enabled = false;
}
break;
case GuardState.Chase:
if (sqrDistance > loseRadius * loseRadius)
{
state = GuardState.Patrol;
patrol.ResumeFromNearest();
patrol.enabled = true;
return;
}
Vector3 target = player.position;
target.y = transform.position.y;
transform.position = Vector3.MoveTowards(transform.position, target, chaseSpeed * Time.deltaTime);
break;
}
}
}
Ligar e desligar o componente (patrol.enabled) é a forma mais simples de estado: o Update de um MonoBehaviour desligado não roda. Ao voltar, ResumeFromNearest faz o guarda retomar do ponto mais perto em vez de atravessar o mapa até o ponto onde parou. A perseguição aqui é mínima de propósito; a versão completa, com distância de parada e desistência por tempo, está no post do inimigo que persegue o jogador.
Quando o inimigo ganha mais estados (investigar ruído, atacar, fugir), esse switch cresce. É a hora de passar para uma máquina de estados com uma classe por estado, assunto do post de máquina de estados (FSM) na Unity e do padrão State em padrões de projeto para jogos.
Vários guardas e rotas compartilhadas
Com o script pronto, a rota vira dado de level design. Três práticas que poupam retrabalho:
- Uma rota por objeto, reaproveitável. Dois guardas podem usar o mesmo
Rota_Patio. Para não andarem grudados, comece o segundo num índice diferente: acrescente um[SerializeField] private int startIndex;e atribuacurrentIndex = startIndex;noStart. Os dois fazem a mesma ronda em lados opostos do pátio. - Prefab do guarda sem a rota. O prefab leva o inimigo e o script; a rota fica na cena e é arrastada para o campo Waypoints de cada instância. Assim você reutiliza o guarda em qualquer fase.
- Rota que conta uma história. O guarda que para na janela, depois no portão, depois na fogueira, comunica o que ele protege. Coloque as paradas longas nos pontos que o jogador precisa observar para planejar a passagem.
Teste a rota em velocidade de jogo, não acelerada. O jogador vai viver cada segundo de espera, e é ali que ele decide se a fase é justa.
Erros comuns: inimigo girando no lugar, pulando ponto
Inimigo gira em volta do ponto sem parar. O arriveThreshold é menor do que o inimigo consegue acertar, ou ele se move por física e passa do ponto. Com NavMeshAgent, autoBraking desligado e stoppingDistance 0 causam o mesmo. Aumente o limite para 0,1 a 0,3 e ligue o autoBraking.
Inimigo pula um ponto. Com NavMeshAgent, faltou checar pathPending. Com o script de Transform, dois waypoints muito próximos ficam dentro do limite ao mesmo tempo.
Inimigo não sai do lugar. Os waypoints são filhos do inimigo e andam com ele.
Inimigo flutua ou afunda. O waypoint está fora da altura do chão e o script usa o Y do ponto. Mantenha o targetPosition.y = transform.position.y, ou posicione os marcadores no chão.
Rotação gira bruscamente no ponto. O LookRotation recebeu um vetor quase zero. O script só gira quando ainda falta distância, depois da checagem de chegada; mantenha essa ordem.
Próximo passo
Monte dois guardas: um em loop num pátio e outro em ping-pong num corredor, com esperas diferentes por ponto. Jogue tentando passar por eles sem ser visto. Você vai sentir na hora que esperas e rotas são game design, não só código, e que o próximo passo é dar olhos ao guarda com ângulo de visão em vez de um raio cego.
Perguntas frequentes
Como fazer um inimigo patrulhar na Unity?
Coloque GameObjects vazios na cena como pontos da rota, guarde os Transforms num array e mova o inimigo até o ponto atual com Vector3.MoveTowards. Quando a distância ficar abaixo de um limite, espere alguns segundos e avance o índice para o próximo ponto.
Qual a diferença entre patrulha em loop e ping-pong?
Em loop, depois do último ponto o inimigo volta ao primeiro, fechando um circuito. Em ping-pong, ele percorre os pontos na ordem inversa, indo e voltando pela mesma linha. Loop serve para rondas em volta de um pátio; ping-pong, para corredores e muralhas.
Por que usar sqrMagnitude em vez de Vector3.Distance?
Vector3.Distance calcula uma raiz quadrada. Para saber se o inimigo chegou, basta comparar a distância ao quadrado com o limite ao quadrado, o que dá o mesmo resultado sem a raiz. Num inimigo a diferença é pequena; em dezenas, por frame, soma.
Como fazer o inimigo voltar a patrulhar depois de perseguir o jogador?
Ao sair da perseguição, procure o waypoint mais próximo da posição atual e retome dali, em vez de voltar para o índice onde parou. Assim o inimigo não atravessa o mapa para retomar a ronda do começo.



