Introduction
NavMesh, or Navigation Mesh, is Unity's pathfinding navigation system, essential for simulating realistic AI behaviors in 3D games. Imagine a horde of enemies intelligently navigating around your barricades or an NPC reaching a specific point without passing through walls: that's NavMesh generating a navigation mesh from your scene geometry.
In 2026, with Unity 2023 LTS and its AI Navigation 2.0 optimizations, NavMesh handles dynamic open worlds, real-time obstacles, and mutual agent avoidance. This intermediate tutorial guides you step by step: from initial baking to advanced multi-agent scripts. Why is it crucial? Without NavMesh, your AI slides or clips; with it, it feels alive. By the end, you'll deploy a complete scene with 10 agents avoiding obstacles and each other. Estimated time: 45 min. Ready to boost your gameplay?
Prerequisites
- Unity 2023.2 LTS or higher (downloadable via Unity Hub).
- Basic knowledge of C# and Unity components (Transform, Update()).
- A simple 3D scene: flat ground (scaled Cube), walls (Cubes), lighting.
- AI Navigation package enabled (Window > Package Manager > Unity Registry > AI Navigation).
Step 1: Set Up and Bake the Basic NavMesh
Open Window > AI > Navigation to display the Navigation window.
- Select your ground: Navigation > Static > Navigation Static (check to include in the bake).
- Add NavMesh Surface (GameObject > AI > NavMesh Surface) to an empty GameObject named 'NavMeshHolder'.
- In the NavMesh Surface inspector:
Analogy: Baking is like generating a road map from your city (geometry). Test by adding a Capsule with NavMesh Agent: it now navigates intelligently!
Simple Agent Script to Fixed Target
using UnityEngine;
using UnityEngine.AI;
public class SimpleNavAgent : MonoBehaviour
{
[SerializeField] private Transform target;
private NavMeshAgent agent;
void Start()
{
agent = GetComponent<NavMeshAgent>();
if (target == null)
{
Debug.LogError("Assign a target in the inspector!");
return;
}
agent.SetDestination(target.position);
}
void Update()
{
// Stop if arrived (distance < 0.5m)
if (!agent.pathPending && agent.remainingDistance < 0.5f)
{
agent.ResetPath();
}
}
}This complete script assigns a NavMeshAgent to a GameObject (Capsule) and directs it to a fixed Transform target. It handles arrival by resetting the path to avoid infinite loops. Pitfall: Forget GetComponent and the agent slides; always check target != null in production.
Step 2: Interactive Navigation (Mouse Click)
For player control: add a Camera in TPS/FPS view. Mouse clicks set the destination.
Bake improvements: Increase Voxel Size to 0.3 for precision, Max Slope to 45° for ramps. Re-bake.
Test: Hit Play, click the ground – the agent follows the optimal path, avoiding walls. Visualize the path in green (Gizmos > Show NavMesh).
Player Controller with Click Destination
using UnityEngine;
using UnityEngine.AI;
public class PlayerNavController : MonoBehaviour
{
[SerializeField] private Camera playerCamera;
[SerializeField] private LayerMask groundLayer = 1 << 6; // Layer 'Ground'
private NavMeshAgent agent;
private RaycastHit hit;
void Start()
{
agent = GetComponent<NavMeshAgent>();
if (playerCamera == null) playerCamera = Camera.main;
agent.updateRotation = false; // Manual rotation
}
void Update()
{
if (Input.GetMouseButtonDown(0))
{
Ray ray = playerCamera.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out hit, 100f, groundLayer))
{
NavMeshHit navHit;
if (NavMesh.SamplePosition(hit.point, out navHit, 1.0f, -1))
{
agent.SetDestination(navHit.position);
}
}
}
}
}This controller raycasts the mouse click on the Ground layer, samples a valid NavMesh position (avoids invalid edges), and sets the destination. Use NavMesh.SamplePosition for reliability. Pitfall: Without layerMask, clicks go through everything; assign Layer 6 to your ground.
Step 3: Dynamic Obstacles with NavMeshObstacle
For moving crates: Add NavMeshObstacle (GameObject > AI > NavMesh Obstacle) to an enemy Cube.
- Shape: Box, Carve: true (carves the NavMesh at runtime).
- Move Threshold: 0.5m for updates.
Dynamic Obstacle Toggle Script
using UnityEngine;
using UnityEngine.AI;
public class DynamicObstacle : MonoBehaviour
{
private NavMeshObstacle obstacle;
[SerializeField] private float moveSpeed = 2f;
[SerializeField] private Vector3 patrolPoint;
void Start()
{
obstacle = GetComponent<NavMeshObstacle>();
patrolPoint = new Vector3(5f, 0.5f, 5f); // Patrol point
}
void Update()
{
// Simple patrol movement
transform.position = Vector3.MoveTowards(transform.position, patrolPoint, moveSpeed * Time.deltaTime);
// Toggle carve if speed > threshold
if (obstacle.velocity.magnitude > 0.1f)
{
obstacle.carveOnlyStationary = false;
}
else
{
obstacle.carveOnlyStationary = true;
}
}
}This script animates an obstacle on patrol, enabling Carve during movement to dynamically update the NavMesh. Velocity check optimizes performance. Pitfall: Without carveOnlyStationary, CPU spikes; test with 20+ obstacles.
Step 4: Multi-Agent Avoidance
Duplicate 5 agents. In Navigation window > Agents > Humanoid > Avoidance Priority: 50 (medium).
Obstacle Avoidance Type: High Quality Obstacle Avoidance.
Agents push past each other without physics collisions. Boost with random AI script.
AI with Random Destinations + Avoidance
using UnityEngine;
using UnityEngine.AI;
public class AIAgentRandom : MonoBehaviour
{
[SerializeField] private float patrolRadius = 10f;
[SerializeField] private float waitTime = 3f;
private NavMeshAgent agent;
private Vector3 startPos;
private float waitTimer;
void Start()
{
agent = GetComponent<NavMeshAgent>();
agent.avoidancePriority = Random.Range(0, 99);
startPos = transform.position;
SetRandomDestination();
}
void Update()
{
if (!agent.pathPending && agent.remainingDistance < 0.5f)
{
waitTimer += Time.deltaTime;
if (waitTimer > waitTime)
{
SetRandomDestination();
waitTimer = 0;
}
}
}
void SetRandomDestination()
{
Vector3 randomDir = Random.insideUnitSphere * patrolRadius;
randomDir += startPos;
NavMeshHit hit;
if (NavMesh.SamplePosition(randomDir, out hit, patrolRadius, -1))
{
agent.SetDestination(hit.position);
}
}
}Generates random destinations within a radius, with pauses and unique avoidance priorities (0-99). SamplePosition ensures validity. Pitfall: Without random avoidancePriority, herd collisions occur; scales to 50+ agents.
Advanced Path Query for Validation
using UnityEngine;
using UnityEngine.AI;
public class PathValidator : MonoBehaviour
{
[SerializeField] private Transform target;
private NavMeshAgent agent;
private NavMeshPath path;
void Start()
{
agent = GetComponent<NavMeshAgent>();
path = new NavMeshPath();
}
void Update()
{
if (target != null)
{
NavMesh.CalculatePath(transform.position, target.position, NavMesh.AllAreas, path);
if (path.status == NavMeshPathStatus.PathComplete)
{
Debug.Log($"Valid path, length: {path.CalculateCornerLength()}");
agent.SetPath(path);
}
else
{
Debug.LogWarning("Path impossible!");
}
}
}
void OnDrawGizmos()
{
if (path != null)
{
Gizmos.color = Color.green;
NavMeshVisualization.DrawPathGizmos(path);
}
}
}Calculates and validates path before SetDestination, logs status/length. Gizmos visualize (add using UnityEngine.AI.NavMeshVisualization if custom). Pitfall: Ignoring CalculatePath leads to bad paths; use for closed doors.
Step 5: Runtime NavMesh for Procedural Worlds
NavMesh Surface > Build > Advanced > Use Geometry > Render Meshes.
For proc-gen: Call surface.BuildNavMesh() after spawning. Limit: Use async for performance.
Async Runtime NavMesh Baking
using UnityEngine;
using UnityEngine.AI;
public class RuntimeNavMeshBuilder : MonoBehaviour
{
[SerializeField] private NavMeshSurface navMeshSurface;
[ContextMenu("Build NavMesh")]
public async void BuildNavMeshAsync()
{
// Simulate procedural spawn
GameObject procGround = GameObject.CreatePrimitive(PrimitiveType.Plane);
procGround.transform.position = new Vector3(10, 0, 10);
procGround.AddComponent<NavMeshModifier>().OverrideArea = 0;
await System.Threading.Tasks.Task.Run(() =>
{
navMeshSurface.BuildNavMesh();
});
Debug.Log("NavMesh rebaked at runtime!");
}
}Builds NavMesh async after procedural spawn (Plane + Modifier). Task.Run offloads CPU. Pitfall: Sync build freezes the game; always async for >1000 polys.
Best Practices
- Voxel Size < 0.3 for precision, but test FPS (bake time x10).
- Avoidance priorities: Random 0-99 per agent, boss at 0 (high priority).
- Areas & Costs: Multi-layers (water cost 3.0) for tactical AI.
- Cache paths with NavMesh.CalculatePath in coroutine.
- Profiler > AI: Aim for <1ms/frame with 50 agents.
Common Errors to Avoid
- No Navigation Static: Empty NavMesh, static agents.
- Agent radius too large: Stuck in narrow doors; match geometry.
- Bake without SamplePosition: Invalid destinations, teleporting.
- Forget carve on moving obstacles: Agents pass through crates.
- Sync runtime bake: Lag spikes; force async.
Next Steps
- Official docs: Unity AI Navigation Manual.
- Advanced tutorial: OffMeshLinks for jumps/elevators.
- Performance: NavMesh modifiers for no-go zones.
- Check our Learni trainings on Unity and AI to master DOTS Navigation in 2026.