๐ถ Lesson 6.2: Moving Agents โ Destinations & Patrol Routes
Last lesson you baked a NavMesh and stood an agent on it. Now you'll make it go. It turns out moving a NavMeshAgent is almost embarrassingly simple โ one line โ but doing it well (knowing when it arrived, looping a patrol, handling moving obstacles) is where the craft lives. That's this lesson.
๐ฏ Learning Objectives
By the end of this lesson, you will be able to:
- Send an agent anywhere with
agent.SetDestination(...) - Detect arrival reliably using
pathPendingandremainingDistance - Build a looping patrol between an array of waypoints
- Understand
hasPath,velocity, and why you never move the Transform yourself - Choose between a NavMeshObstacle (carving) and re-baking for obstacles that move
Estimated Time: 45 minutes ยท Prerequisite: Lesson 6.1 (The NavMesh: Baking & Agents)
In This Lesson
SetDestination: One Line to Move
To move an agent, you give it a world position and it does the rest โ finds the path, steers around obstacles, and walks there:
using UnityEngine;
using UnityEngine.AI;
[RequireComponent(typeof(NavMeshAgent))]
public class GoToTarget : MonoBehaviour
{
[SerializeField] Transform target;
NavMeshAgent agent;
void Awake() => agent = GetComponent<NavMeshAgent>();
void Start()
{
if (target != null)
agent.SetDestination(target.position); // that's the whole thing
}
}
That single call kicks off asynchronous pathfinding. Unity computes a corner list across the NavMesh from the agent to the destination, then the agent follows it, updating its own Transform position and rotation every frame. You never touch transform.position yourself โ if you do, you'll fight the agent and get jitter.
๐ Definition
Path: the sequence of straight segments (corners) the agent walks to reach its destination. SetDestination requests one; it may not be ready the same frame you ask, which is why the next section matters.
โ ๏ธ Don't call SetDestination every frame with the same value
Recomputing an identical path each frame is wasted work. Set the destination when it actually changes (a new waypoint, the player moved far enough). For a moving target, throttle it โ recompute a few times a second, not 60. You'll do exactly this in the chase state next lesson.
Knowing When You've Arrived
The trickiest part of agent movement isn't starting โ it's knowing when the agent has finished. The naive check "is my distance to the target small?" fails, because the agent walks the NavMesh path, which is usually longer than the straight line. Unity gives you the right tools:
pathPendingโ true while Unity is still computing the path. You must wait for this to be false before trusting the distance.remainingDistanceโ the distance left along the path. This is the number to compare againststoppingDistance.hasPathandvelocityโ a fully arrived agent has no path and (near) zero velocity.
The idiomatic "have I arrived?" check combines them:
bool ReachedDestination()
{
// 1. path still being calculated? not arrived yet
if (agent.pathPending) return false;
// 2. still farther than our stopping distance? still walking
if (agent.remainingDistance > agent.stoppingDistance) return false;
// 3. either no path left, or we've slowed to a stop
return !agent.hasPath || agent.velocity.sqrMagnitude < 0.01f;
}
All three checks matter. Skip pathPending and you'll get a false "arrived" on the very first frame (when remainingDistance is still 0 because no path exists yet). The velocity check handles the case where the agent has a path but is idling at the destination.
โ Pro Tip
Compare squared magnitudes (velocity.sqrMagnitude) instead of velocity.magnitude when you just need "is it basically zero?". Skipping the square root is a tiny, free optimization โ and a habit worth building for any per-frame distance/speed test.
A Waypoint Patrol
A patrol is the classic enemy behaviour: walk to waypoint 0, then 1, then 2, then loop back to 0 forever. With SetDestination and an arrival check, it's short:
using UnityEngine;
using UnityEngine.AI;
[RequireComponent(typeof(NavMeshAgent))]
public class Patrol : MonoBehaviour
{
[SerializeField] Transform[] waypoints;
[SerializeField] float waitAtPoint = 1.5f;
NavMeshAgent agent;
int index = 0;
float waitTimer = 0f;
void Awake() => agent = GetComponent<NavMeshAgent>();
void Start()
{
if (waypoints.Length > 0)
agent.SetDestination(waypoints[index].position);
}
void Update()
{
if (waypoints.Length == 0) return;
if (ReachedDestination())
{
// pause a beat at the waypoint, then advance to the next
waitTimer += Time.deltaTime;
if (waitTimer >= waitAtPoint)
{
waitTimer = 0f;
index = (index + 1) % waypoints.Length; // loop
agent.SetDestination(waypoints[index].position);
}
}
}
bool ReachedDestination()
{
if (agent.pathPending) return false;
if (agent.remainingDistance > agent.stoppingDistance) return false;
return !agent.hasPath || agent.velocity.sqrMagnitude < 0.01f;
}
}
The modulo (% waypoints.Length) is what makes it loop: after the last waypoint, index wraps back to 0. Drop empty GameObjects around your level, drag them into the waypoints array in order, and the agent walks the circuit. Here's a top-down view of what that circuit looks like:
Reading the Route
The Update loop above hides a small state machine: the agent is either travelling to a waypoint or waiting at one, and arrival is what flips it. Seeing that logic as a flow makes the wrap-around click:
(not pending AND
remaining โค stopping)"} C -- "No" --> B C -- "Yes" --> D["Wait at point
(waitTimer += deltaTime)"] D --> E{"waited long enough?"} E -- "No" --> D E -- "Yes" --> F["index = (index + 1) % length"] F --> G["SetDestination(waypoints[index])"] G --> B
Figure 2: The waypoint-advance logic. Arrival triggers a wait; the wait expiring advances the index (wrapping with modulo) and sets the next destination.
A few properties are worth knowing for debugging and for the states you'll build next lesson:
agent.destinationโ the current target position (read it back to confirm what you set).agent.isStoppedโ settrueto freeze the agent in place without losing its path; setfalseto resume. Great for "pause while attacking."agent.pathStatusโComplete,Partial(couldn't fully reach โ target is off the mesh or blocked), orInvalid. Check forPartialwhen a destination might be unreachable.
๐ก Warp, don't teleport. If you ever need to move an agent instantly (respawn, cutscene), useagent.Warp(position), nottransform.position = position. Warp re-seats the agent on the NavMesh correctly; a raw Transform assignment can leave it confused about where it is on the mesh.
Obstacles: Carving vs. Re-baking
Your baked NavMesh is a snapshot. So what happens when something blocks the path after the bake โ a door slams shut, a crate gets pushed into the corridor, a car parks across the road? You have two tools, and picking the right one matters.
- NavMeshObstacle (with Carving) โ add a
NavMeshObstaclecomponent to the moving thing. With Carve ticked, it cuts a hole in the NavMesh at runtime wherever it sits, and agents path around it live. This is the right choice for things that move: doors, crates, other characters, destructible cover. - Re-baking โ regenerate the whole surface. Expensive and causes a hitch, so it's for author-time or rare, deliberate level changes (a wall collapses once in a scripted event), not per-frame movement.
The rule of thumb:
๐ Definition
Carving: a NavMeshObstacle dynamically subtracts its volume from the baked mesh while it's in place. Static level geometry gets baked into the mesh; things that move should carve instead. Never try to re-bake every frame to handle movement.
There's one important subtlety. An obstacle with carving off still pushes agents away via avoidance, but it does not alter the path โ agents will keep trying to walk their original route and just bump against it. Turn carving on when you need agents to actually re-route around the blockage. Carving is more expensive (it edits the mesh), so use it only where re-routing is required.
โ ๏ธ Don't put a NavMeshObstacle and a NavMeshAgent on the same object
They're opposites: an Agent walks the mesh, an Obstacle blocks it. Other characters avoid each other through the agents' own avoidance (Radius / Priority), not by giving each one an obstacle. Reserve NavMeshObstacle for non-agent movers like doors and physics crates.
Hands-on Challenge
๐๏ธ Exercise 1: Walk the loop
Objective: A capsule that patrols four waypoints forever.
- In the room you baked last lesson, create four empty GameObjects as waypoints, spread around the floor, and name them
WP0โWP3. - Add the
Patrolscript to your agent capsule and drag the four waypoints into the array in the order you want them visited. - Press Play. Confirm the capsule walks 0 โ 1 โ 2 โ 3 โ 0, pausing briefly at each.
- Reorder the array and confirm the route changes accordingly.
๐ก Hint: it reaches WP0 then never moves again
Your arrival check is probably firing on frame one before the path is ready. Make sure ReachedDestination() returns false while agent.pathPending is true โ that's the guard that stops a phantom "arrived" at startup.
โ Success check
The capsule cleanly visits every waypoint in order, pauses ~1.5s at each, and loops indefinitely without jitter or stopping short.
๐๏ธ Exercise 2: Block the path live
Put a cube in the middle of one patrol segment and give it a NavMeshObstacle with Carve on. Press Play and watch the agent re-route around it. Now toggle Carve off and observe the difference: the agent no longer paths around it and instead pushes against it. This is the carving-vs-avoidance distinction made visible.
๐ฏ Quick Quiz
Question 1: Why must you check pathPending before trusting remainingDistance?
Question 2: A door that opens and closes during play needs agents to re-route around it while shut. What do you add to the door?
Question 3: What makes the patrol in the Patrol script loop back to the first waypoint?
Summary
๐ Key Takeaways
agent.SetDestination(position)is the whole move โ it requests an async path and the agent drives its own Transform there.- Check arrival with all three:
!pathPending,remainingDistance <= stoppingDistance, and no path / near-zero velocity. - A patrol is an array of waypoints plus an index that advances with
(index + 1) % lengthto loop. - Useful members:
isStopped(freeze/resume),pathStatus(Complete / Partial / Invalid), andWarp()for instant repositioning. - For things that move, add a NavMeshObstacle with carving โ never re-bake per frame. Bake static geometry; carve dynamic blockers.
๐ What's Next?
Right now the agent only ever patrols โ it can't react to anything. In Lesson 6.3: An Enemy State Machine you'll wrap this patrol in a code-driven FSM with Patrol, Chase, and Attack states, so the enemy switches behaviour based on where the player is, and feeds the Animator from Module 1 as it does.
๐ถ Your agent gets around now
Set a destination, know when it's reached, loop a patrol, re-route around movers. These four moves are the entire vocabulary of NavMesh movement โ the enemy AI you build next is just deciding which destination to set, and when.