# NavMesh in Unity 6 (AI Navigation 2.x) **Target:** Unity 6000.5 and above (Unity 6). Package: **AI Navigation** (`com.unity.ai.navigation`) 2.x. Companion to [[Navmesh (3D) in Untiy]] (patrol and chase basics). Feeds [[Enemy AI State Machine]] and [[Organic Enemy Sensing]]. ## Overview 1. What changed (old Navigation window vs AI Navigation 2.x) 2. Baking with NavMeshSurface 3. Modifiers, volumes and areas 4. Agent types 5. NavMeshLink (replaces Off Mesh Link) 6. Obstacles and carving 7. Runtime and async baking 8. Script control: destinations, path status, sampling, CalculatePath 9. Manual movement and root motion 10. A complete patrol agent ## Step 1 - What changed | Before (2021 and earlier) | Unity 6 + AI Navigation 2.x | |---|---| | **Window > AI > Navigation** "Bake" tab baked one NavMesh per scene, from objects marked *Navigation Static* | You add a **NavMeshSurface** component and bake from its inspector. Several surfaces per scene, one per agent type if you want. | | Per-object "Navigation Area" in the static flags | **NavMeshModifier** component (per object) and **NavMeshModifierVolume** (box region) | | **OffMeshLink** component | **NavMeshLink**. In 2.0 OffMeshLink is deprecated and can't be added from Add Component any more. | | Baked at edit time only (in practice) | `NavMeshSurface.BuildNavMesh()` and `UpdateNavMesh()` (async) at runtime | Verified changes in AI Navigation **2.0** (from the package's *What's new* page): - NavMeshLink end points can now be **Transform references** (`startTransform` / `endTransform`), so links follow moving objects. - New `NavMeshLink.activated` property to turn a link on or off for agents. - `NavMeshLink.costModifier` is now a `float`. - `OffMeshLink` deprecated; use NavMeshLink the same way. The runtime API you call on agents (`UnityEngine.AI.NavMeshAgent`, `NavMesh`, `NavMeshObstacle`) is still in the engine. The authoring components live in the `Unity.AI.Navigation` namespace in the package. > [!warning] Unverified > I couldn't confirm any NavMesh feature added specifically in 6000.5 itself. The Navigation window still exists for **Agents** and **Areas** settings. Treat the legacy scene "Bake" tab as gone and use NavMeshSurface. Check your package version under **Window > Package Manager** and read its changelog. ## Step 2 - Bake with NavMeshSurface 1. **Window > Package Manager > Unity Registry > AI Navigation > Install.** It comes preinstalled in the 3D URP template. 2. Create an empty GameObject called `NavMesh`, then **Add Component > NavMeshSurface**. 3. Set these fields: - **Agent Type**: Humanoid (the default) - **Collect Objects**: *All Game Objects*, *Volume*, or *Current Object Hierarchy*. Pick *Current Object Hierarchy* if you parent the level geometry under one root. - **Include Layers**: leave out the Player, enemies and props that move. - **Use Geometry**: *Render Meshes* or *Physics Colliders*. Colliders usually give a cleaner result. 4. Click **Bake**. A `NavMesh-<name>.asset` file is saved next to the scene. Turn on the gizmo to see the blue mesh. Why do it this way: each surface is its own asset. You can rebake one room or prefab without touching the rest, and prefabs can carry their own NavMesh. ## Step 3 - Modifiers, volumes and areas - **NavMeshModifier** on an object: *Override Area* (for example, mark water as `Not Walkable`, or as a custom `Mud` area with Cost 5), or *Ignore From Build*. - **NavMeshModifierVolume**: a box that sets the area for everything inside it. Useful for "danger zones" without any geometry. - Areas and their costs are set under **Window > AI > Navigation > Areas**. A higher cost makes agents prefer other routes. A per-agent mask (`agent.areaMask`) controls which areas they can walk on at all. ## Step 4 - Agent types **Window > AI > Navigation > Agents**: add a type (for example `Large`, with radius 1.2 and height 3). Then add a **second NavMeshSurface** with Agent Type = Large and bake it. Set the **NavMeshAgent > Agent Type** on large enemies to match. Each agent only uses the surface built for its type. ## Step 5 - NavMeshLink Links connect parts of the mesh that don't touch: gaps, ledges, ladders, doors. 1. Add a **NavMeshLink** to a GameObject. 2. Set the Start and End points. In 2.x you can drag in Transforms instead of typing offsets. 3. Set **Width** (to make a wide link), **Bidirectional**, and **Area** / **Cost Modifier**. 4. Turn the link on or off from script with `link.activated = false;` (for example, when a door closes). By default agents cross a link in a straight line. To do your own jump animation, see `LinkTraverser` in Step 9. ## Step 6 - Obstacles and carving **NavMeshObstacle** on a moving crate or door: - **Carve off**: agents avoid it locally, but their path doesn't change. Best for objects that move all the time. - **Carve on**: it cuts a hole in the mesh, so paths go around it. Use **Move Threshold**, **Time To Stationary**, and **Carve Only Stationary** so it only re-carves once the object stops. Carving every frame is expensive. Rule of thumb: carve for things that move sometimes, and use avoidance only for things that are always moving. ## Step 7 - Runtime and async baking ```csharp using System.Collections; using Unity.AI.Navigation; using UnityEngine; /// Rebuilds a NavMeshSurface at runtime, e.g. after procedural level generation /// or when a destructible wall falls. public class RuntimeNavMeshBaker : MonoBehaviour { [SerializeField] NavMeshSurface surface; [SerializeField] bool buildOnStart = true; void Start() { if (buildOnStart) surface.BuildNavMesh(); // synchronous: fine during a loading screen } /// Async incremental update. Keeps the game running while it rebuilds. public void RebuildAsync() => StartCoroutine(RebuildRoutine()); IEnumerator RebuildRoutine() { if (surface.navMeshData == null) { surface.BuildNavMesh(); yield break; } AsyncOperation op = surface.UpdateNavMesh(surface.navMeshData); while (!op.isDone) yield return null; Debug.Log("NavMesh rebuilt"); } } ``` `BuildNavMesh()` blocks until it finishes. `UpdateNavMesh(data)` returns an `AsyncOperation` and updates the existing data in the background. ## Step 8 - Script control The important parts of `NavMeshAgent`: | API | Use | |---|---| | `SetDestination(Vector3)` | Asks for a path. It is computed **asynchronously**, so `pathPending` is true for a frame or more. | | `pathPending`, `hasPath` | Wait until `!pathPending` before you trust `remainingDistance`. | | `pathStatus` | `PathComplete`, `PathPartial` (got as close as it can), `PathInvalid` | | `remainingDistance`, `stoppingDistance` | Checking for arrival | | `isStopped`, `ResetPath()` | Pause, or clear the path | | `Warp(Vector3)` | Teleport onto the mesh (spawning). Don't set `transform.position` directly. | | `speed`, `angularSpeed`, `acceleration` | Movement tuning (Chase is faster than Patrol) | And on the static `NavMesh` class: - `NavMesh.SamplePosition(point, out NavMeshHit hit, maxDistance, NavMesh.AllAreas)`: finds the nearest point on the mesh. Always run random or clicked points through it. - `NavMesh.CalculatePath(from, to, NavMesh.AllAreas, path)`: computes a path right away without moving anything. Use it to check whether a point can be reached before you commit to it. ```csharp using UnityEngine; using UnityEngine.AI; /// Helpers every AI script ends up needing. public static class NavUtil { /// Random reachable point within radius of origin, or origin if none found. public static Vector3 RandomPoint(Vector3 origin, float radius, int areaMask = NavMesh.AllAreas, int tries = 10) { for (int i = 0; i < tries; i++) { Vector3 candidate = origin + Random.insideUnitSphere * radius; if (NavMesh.SamplePosition(candidate, out NavMeshHit hit, 2f, areaMask)) return hit.position; } return origin; } /// True if a complete path exists. Also returns path length. public static bool IsReachable(Vector3 from, Vector3 to, out float length, int areaMask = NavMesh.AllAreas) { var path = new NavMeshPath(); length = 0f; if (!NavMesh.CalculatePath(from, to, areaMask, path) || path.status != NavMeshPathStatus.PathComplete) return false; for (int i = 1; i < path.corners.Length; i++) length += Vector3.Distance(path.corners[i - 1], path.corners[i]); return true; } /// Has the agent arrived? Handles the async pathPending gotcha. public static bool HasArrived(NavMeshAgent agent) { if (agent.pathPending) return false; if (agent.remainingDistance > agent.stoppingDistance) return false; return !agent.hasPath || agent.velocity.sqrMagnitude < 0.01f; } } ``` ## Step 9 - Manual movement, root motion and link traversal When `updatePosition = false`, the agent still plans the path and works out `desiredVelocity` and `nextPosition`, but **you** move the transform. That's what you need for root motion, where the animation moves the character and the agent only steers. ```csharp using UnityEngine; using UnityEngine.AI; /// Root-motion locomotion driven by a NavMeshAgent. /// Animator needs float params "Speed" and "Turn" driving a blend tree, and Apply Root Motion ON. [RequireComponent(typeof(NavMeshAgent), typeof(Animator))] public class RootMotionAgent : MonoBehaviour { NavMeshAgent agent; Animator anim; static readonly int SpeedHash = Animator.StringToHash("Speed"); static readonly int TurnHash = Animator.StringToHash("Turn"); void Awake() { agent = GetComponent<NavMeshAgent>(); anim = GetComponent<Animator>(); agent.updatePosition = false; // animation moves us agent.updateRotation = true; // agent still turns us } void Update() { // Express desired velocity in local space for the blend tree. Vector3 local = transform.InverseTransformDirection(agent.desiredVelocity); anim.SetFloat(SpeedHash, local.z, 0.1f, Time.deltaTime); anim.SetFloat(TurnHash, local.x, 0.1f, Time.deltaTime); // Keep the simulated agent from drifting away from the body. Vector3 drift = agent.nextPosition - transform.position; if (drift.magnitude > agent.radius) agent.nextPosition = transform.position + 0.9f * drift; } void OnAnimatorMove() { // Apply root motion, then snap Y to the agent so we stay on the mesh. Vector3 pos = anim.rootPosition; pos.y = agent.nextPosition.y; transform.position = pos; agent.nextPosition = transform.position; } } ``` Custom crossing for NavMeshLinks (a jump arc instead of the default straight line): ```csharp using System.Collections; using UnityEngine; using UnityEngine.AI; [RequireComponent(typeof(NavMeshAgent))] public class LinkTraverser : MonoBehaviour { [SerializeField] float jumpHeight = 1.5f; [SerializeField] float jumpDuration = 0.6f; NavMeshAgent agent; bool traversing; void Awake() { agent = GetComponent<NavMeshAgent>(); agent.autoTraverseOffMeshLink = false; // we handle links ourselves } void Update() { if (agent.isOnOffMeshLink && !traversing) StartCoroutine(Jump()); } IEnumerator Jump() { traversing = true; OffMeshLinkData data = agent.currentOffMeshLinkData; Vector3 start = transform.position; Vector3 end = data.endPos + Vector3.up * agent.baseOffset; for (float t = 0; t < 1f; t += Time.deltaTime / jumpDuration) { float arc = 4f * jumpHeight * t * (1f - t); // parabola peaking at t=0.5 transform.position = Vector3.Lerp(start, end, t) + Vector3.up * arc; yield return null; } transform.position = end; agent.CompleteOffMeshLink(); traversing = false; } } ``` > NavMeshLink crossings still go through the `isOnOffMeshLink` / `currentOffMeshLinkData` / `CompleteOffMeshLink` API. The name "OffMeshLink" stayed on the agent side. ## Step 10 - Complete patrol agent ```csharp using UnityEngine; using UnityEngine.AI; /// Patrols waypoints in order or randomly, waits at each, and can roam /// randomly when there are no waypoints. [RequireComponent(typeof(NavMeshAgent))] public class PatrolAgent : MonoBehaviour { [SerializeField] Transform[] waypoints; [SerializeField] bool randomOrder; [SerializeField] Vector2 waitRange = new(1f, 3f); [SerializeField] float roamRadius = 10f; NavMeshAgent agent; int index; float waitUntil; void Awake() => agent = GetComponent<NavMeshAgent>(); void Start() => GoNext(); void Update() { if (agent.pathStatus == NavMeshPathStatus.PathInvalid) { GoNext(); return; } if (!NavUtil.HasArrived(agent)) return; if (waitUntil < 0f) waitUntil = Time.time + Random.Range(waitRange.x, waitRange.y); else if (Time.time >= waitUntil) GoNext(); } void GoNext() { waitUntil = -1f; Vector3 target; if (waypoints == null || waypoints.Length == 0) target = NavUtil.RandomPoint(transform.position, roamRadius); else { index = randomOrder ? Random.Range(0, waypoints.Length) : (index + 1) % waypoints.Length; target = waypoints[index].position; } agent.SetDestination(target); } void OnDrawGizmosSelected() { if (agent == null || !agent.hasPath) return; Gizmos.color = Color.cyan; var c = agent.path.corners; for (int i = 1; i < c.Length; i++) Gizmos.DrawLine(c[i - 1], c[i]); } } ``` **Try it:** bake a surface, add a NavMeshAgent and `PatrolAgent` to a capsule, assign three waypoints, and press Play. Next, hand control to a state machine in [[Enemy AI State Machine]]. ## Common pitfalls - Reading `remainingDistance` straight after `SetDestination`: it's still the old value while `pathPending` is true. - Agent "not close enough to NavMesh" errors: spawn with `Warp()` on a sampled point. - Moving objects included in the bake: leave them out with layers, and use NavMeshObstacle instead. - Several agent types but only one surface: the large agents won't find a mesh.