一键导入
navmesh
Unity navigation — NavMeshAgent configuration, NavMeshSurface, off-mesh links, dynamic obstacles, pathfinding patterns.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Unity navigation — NavMeshAgent configuration, NavMeshSurface, off-mesh links, dynamic obstacles, pathfinding patterns.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Card game patterns — deck building, hand management, turn structure, card effects, battlefield zones
Racing game patterns — vehicle physics, track design, lap tracking, AI opponents, drift mechanics
Roguelike/roguelite patterns — procedural dungeons, permadeath, meta-progression, loot systems, turn-based or real-time
Tower defense game patterns — placement grids, enemy pathing, wave spawning, tower upgrades, economy
Endless runner architecture — procedural chunk spawning, lane-based or free movement, obstacle patterns, speed ramping, coin/collectible systems, distance scoring.
Hyper-casual mobile game architecture — one-tap/swipe controls, instant onboarding, short sessions, ad monetization, minimalist visuals, level progression, score systems.
| name | navmesh |
| description | Unity navigation — NavMeshAgent configuration, NavMeshSurface, off-mesh links, dynamic obstacles, pathfinding patterns. |
| globs | ["**/*Nav*.cs","**/*Pathfind*.cs","**/*Agent*.cs"] |
NavMeshSurface component to environment parent objectNavMeshAgent to moving characters[SerializeField] private NavMeshAgent m_Agent;
private void Awake()
{
m_Agent = GetComponent<NavMeshAgent>();
m_Agent.speed = 3.5f;
m_Agent.acceleration = 8f;
m_Agent.angularSpeed = 120f;
m_Agent.stoppingDistance = 0.5f;
m_Agent.autoBraking = true;
}
public void MoveTo(Vector3 destination)
{
m_Agent.SetDestination(destination);
}
private void Update()
{
if (m_Agent.pathPending) return; // Still calculating
switch (m_Agent.pathStatus)
{
case NavMeshPathStatus.PathComplete:
// Full path found
break;
case NavMeshPathStatus.PathPartial:
// Can only get partway — obstacle or unreachable
break;
case NavMeshPathStatus.PathInvalid:
// No path possible
break;
}
// Check if arrived
if (!m_Agent.pathPending && m_Agent.remainingDistance <= m_Agent.stoppingDistance)
{
// Arrived at destination
}
}
public sealed class PatrolBehavior : MonoBehaviour
{
[SerializeField] private Transform[] m_Waypoints;
[SerializeField] private float m_WaitTime = 2f;
private NavMeshAgent m_Agent;
private int m_CurrentWaypoint;
private float m_WaitTimer;
private void Update()
{
if (m_Agent.pathPending) return;
if (m_Agent.remainingDistance <= m_Agent.stoppingDistance)
{
m_WaitTimer -= Time.deltaTime;
if (m_WaitTimer <= 0f)
{
m_CurrentWaypoint = (m_CurrentWaypoint + 1) % m_Waypoints.Length;
m_Agent.SetDestination(m_Waypoints[m_CurrentWaypoint].position);
m_WaitTimer = m_WaitTime;
}
}
}
}
For jumps, ladders, teleporters — connections between disconnected NavMesh areas.
NavMeshLink component between two points// Rebake at runtime (e.g., after terrain change)
m_NavMeshSurface.BuildNavMesh();
// Or update only:
m_NavMeshSurface.UpdateNavMesh(m_NavMeshSurface.navMeshData);
m_Agent.SetAreaCost(areaIndex, cost)NavMesh.Raycast tests whether a straight line between two points on the NavMesh is unobstructed. This is not a physics raycast — it tests NavMesh connectivity only.
using UnityEngine;
using UnityEngine.AI;
public sealed class NavMeshLineOfSight
{
// Returns true if there is a clear NavMesh path in a straight line
public bool HasDirectPath(Vector3 from, Vector3 to)
{
NavMeshHit hit;
bool isBlocked = NavMesh.Raycast(from, to, out hit, NavMesh.AllAreas);
return !isBlocked;
}
// Returns the point where the NavMesh line is broken
public Vector3 GetBlockedPoint(Vector3 from, Vector3 to)
{
NavMeshHit hit;
NavMesh.Raycast(from, to, out hit, NavMesh.AllAreas);
return hit.position;
}
}
Use cases: checking if an enemy can charge in a straight line, validating shortcut paths, determining if a flee direction is open.
When a position might be off the NavMesh (click on a wall, spawning at an arbitrary point), snap it to the nearest valid location.
public static bool TryGetNearestNavMeshPoint(Vector3 source, float maxDistance, out Vector3 result)
{
NavMeshHit hit;
if (NavMesh.SamplePosition(source, out hit, maxDistance, NavMesh.AllAreas))
{
result = hit.position;
return true;
}
result = source;
return false;
}
Always call SamplePosition before SetDestination when the target comes from player input or external data. A destination off the NavMesh causes SetDestination to fail silently.
Calculate a path without committing the agent to it. Useful for AI decision-making.
public sealed class PathEvaluator
{
private readonly NavMeshPath m_Path = new NavMeshPath();
// Evaluate path length without moving the agent
public float GetPathLength(Vector3 from, Vector3 to)
{
if (!NavMesh.CalculatePath(from, to, NavMesh.AllAreas, m_Path))
{
return float.MaxValue;
}
if (m_Path.status != NavMeshPathStatus.PathComplete)
{
return float.MaxValue;
}
float totalDistance = 0f;
Vector3[] corners = m_Path.corners;
for (int cornerIndex = 1; cornerIndex < corners.Length; cornerIndex++)
{
totalDistance += Vector3.Distance(corners[cornerIndex - 1], corners[cornerIndex]);
}
return totalDistance;
}
// Check if destination is reachable before committing
public bool IsReachable(Vector3 from, Vector3 to)
{
NavMesh.CalculatePath(from, to, NavMesh.AllAreas, m_Path);
return m_Path.status == NavMeshPathStatus.PathComplete;
}
}
Reuse the NavMeshPath instance to avoid allocations. Calling new NavMeshPath() once and reusing it is the correct pattern.
Formations define offset positions relative to a leader. Each member is assigned a slot index.
using UnityEngine;
// Pure C# formation calculator — no MonoBehaviour, no Unity API beyond Vector3
public sealed class FormationCalculator
{
public enum FormationType
{
Line,
Triangle,
Circle,
Wedge
}
private const float k_DefaultSpacing = 2f;
// Returns local offsets for each slot (relative to leader facing forward along Z)
public Vector3[] CalculateSlotOffsets(FormationType type, int memberCount, float spacing = k_DefaultSpacing)
{
return type switch
{
FormationType.Line => CalculateLineOffsets(memberCount, spacing),
FormationType.Triangle => CalculateTriangleOffsets(memberCount, spacing),
FormationType.Circle => CalculateCircleOffsets(memberCount, spacing),
FormationType.Wedge => CalculateWedgeOffsets(memberCount, spacing),
_ => CalculateLineOffsets(memberCount, spacing)
};
}
private Vector3[] CalculateLineOffsets(int count, float spacing)
{
var offsets = new Vector3[count];
float startX = -(count - 1) * spacing * 0.5f;
for (int slotIndex = 0; slotIndex < count; slotIndex++)
{
offsets[slotIndex] = new Vector3(startX + slotIndex * spacing, 0f, 0f);
}
return offsets;
}
private Vector3[] CalculateTriangleOffsets(int count, float spacing)
{
var offsets = new Vector3[count];
int row = 0;
int placed = 0;
while (placed < count)
{
int columnsInRow = row + 1;
float rowStartX = -(columnsInRow - 1) * spacing * 0.5f;
for (int col = 0; col < columnsInRow && placed < count; col++)
{
offsets[placed] = new Vector3(rowStartX + col * spacing, 0f, -row * spacing);
placed++;
}
row++;
}
return offsets;
}
private Vector3[] CalculateCircleOffsets(int count, float spacing)
{
var offsets = new Vector3[count];
float radius = count * spacing / (2f * Mathf.PI);
radius = Mathf.Max(radius, spacing);
for (int slotIndex = 0; slotIndex < count; slotIndex++)
{
float angle = slotIndex * (2f * Mathf.PI / count);
offsets[slotIndex] = new Vector3(Mathf.Sin(angle) * radius, 0f, Mathf.Cos(angle) * radius);
}
return offsets;
}
private Vector3[] CalculateWedgeOffsets(int count, float spacing)
{
var offsets = new Vector3[count];
offsets[0] = Vector3.zero; // Leader at front
for (int slotIndex = 1; slotIndex < count; slotIndex++)
{
int row = (slotIndex + 1) / 2;
float side = (slotIndex % 2 == 1) ? -1f : 1f;
offsets[slotIndex] = new Vector3(side * row * spacing, 0f, -row * spacing);
}
return offsets;
}
}
// Convert local formation offset to world position based on leader transform
public static Vector3 GetWorldSlotPosition(Vector3 leaderPosition, Vector3 leaderForward, Vector3 localOffset)
{
Quaternion rotation = Quaternion.LookRotation(leaderForward, Vector3.up);
return leaderPosition + rotation * localOffset;
}
Each follower agent calls SetDestination to its world slot position. Update slot positions only when the leader moves beyond a threshold distance (e.g., 1 unit) to avoid constant repathing.
When a member dies or joins, recalculate offsets for the new count and reassign slots. Prefer shifting surviving members to the closest available slot rather than reassigning all slots to minimize path changes.
| Feature | Carve (NavMeshObstacle.carving = true) | Block (carving = false) |
|---|---|---|
| NavMesh modified | Yes — cuts a hole at runtime | No — agents steer around via avoidance |
| Performance cost | High — triggers local NavMesh rebuild | Low — avoidance only |
| Path accuracy | Perfect — paths go around the carved hole | Approximate — agents may clip through |
| Use case | Doors, placed buildings, barricades | Moving NPCs, rolling boulders |
On NavMeshObstacle with carving enabled:
Carve Only Stationary: enable this for obstacles that move then stop (placed turrets, furniture)Move Threshold: minimum distance the obstacle must move before re-carving (default 0.1, increase for less frequent updates)Time To Stationary: seconds of no movement before the obstacle is considered stationary and carving triggers (default 0.5)Keep the total number of carving obstacles low (under 20 actively moving). Each carve triggers a local NavMesh rebuild.
Before placing a dynamic obstacle, verify the position is on the NavMesh:
public bool CanPlaceObstacle(Vector3 position, float checkRadius)
{
NavMeshHit hit;
return NavMesh.SamplePosition(position, out hit, checkRadius, NavMesh.AllAreas);
}
public sealed class NavMeshDoor : MonoBehaviour
{
[SerializeField] private NavMeshObstacle m_Obstacle;
private void Awake()
{
m_Obstacle = GetComponent<NavMeshObstacle>();
m_Obstacle.carving = true;
}
public void Open()
{
m_Obstacle.carving = false; // Remove the carved hole, agents can path through
}
public void Close()
{
m_Obstacle.carving = true; // Re-carve, agents path around
}
}
Toggle carving rather than enabling/disabling the obstacle component. Disabling the component removes avoidance entirely.
When pathStatus becomes PathInvalid or PathPartial, the agent needs a new destination. Do not retry the same unreachable destination every frame.
private void HandleUnreachableDestination()
{
if (m_Agent.pathStatus == NavMeshPathStatus.PathInvalid)
{
// Find nearest reachable point to the original target
NavMeshHit hit;
if (NavMesh.SamplePosition(m_TargetPosition, out hit, 10f, NavMesh.AllAreas))
{
m_Agent.SetDestination(hit.position);
}
}
}
When chasing a moving target, do not call SetDestination every frame. Use a timer or distance threshold.
private const float k_RepathInterval = 0.3f; // Repath every 0.3 seconds
private float m_RepathTimer;
private void Update()
{
m_RepathTimer -= Time.deltaTime;
if (m_RepathTimer <= 0f)
{
m_RepathTimer = k_RepathInterval;
m_Agent.SetDestination(m_Target.position);
}
}
For 50+ agents chasing moving targets, stagger repath timers so they do not all repath on the same frame. Initialize m_RepathTimer to Random.Range(0f, k_RepathInterval).
Sometimes getting close is good enough (fleeing enemies, area denial). Accept partial paths and move to the closest reachable point:
if (m_Agent.pathStatus == NavMeshPathStatus.PathPartial)
{
// Agent will move to the end of the partial path automatically
// Decide: is partial good enough, or pick a different destination?
}
NavMeshAgent components should not be destroyed and recreated. Pool the entire GameObject.
public sealed class NavAgentPoolHelper : MonoBehaviour
{
[SerializeField] private NavMeshAgent m_Agent;
public void OnGetFromPool(Vector3 spawnPosition)
{
// Disable agent before moving to prevent path recalculation during warp
m_Agent.enabled = false;
transform.position = spawnPosition;
m_Agent.enabled = true;
// Warp to ensure agent is properly placed on NavMesh
m_Agent.Warp(spawnPosition);
}
public void OnReturnToPool()
{
m_Agent.ResetPath();
m_Agent.enabled = false;
gameObject.SetActive(false);
}
}
m_Agent.enabled = false: disables pathfinding and movement but keeps the component. Agent is removed from NavMesh simulation.m_Agent.isStopped = true: agent stays on NavMesh and blocks other agents but does not move. Use for paused/stunned enemies.gameObject.SetActive(false): fully removes from all systems. Use for pooled objects.Never set transform.position directly on an object with an active NavMeshAgent. The agent will try to correct the position back to the NavMesh surface it was on.
// Correct teleportation sequence
m_Agent.enabled = false;
transform.position = newPosition;
m_Agent.enabled = true;
m_Agent.Warp(newPosition);
Before returning an agent to the pool, always:
m_Agent.ResetPath() to clear the current pathm_Agent.enabled = false to remove from NavMesh simulationOn retrieval, re-enable and warp before setting a new destination.
NavMeshAgent.avoidancePriority ranges from 0 (highest priority, others avoid it) to 99 (lowest priority, avoids everyone).
Priority 0-10: Bosses, VIP NPCs (never pushed aside)
Priority 20-40: Player companions (mostly hold formation)
Priority 50-70: Regular enemies (standard avoidance)
Priority 80-99: Minions, swarm units (easily pushed aside)
Higher-priority agents do not steer around lower-priority agents. Use this to prevent important NPCs from being pushed off their path by crowds.
NavMesh avoidance alone does not prevent agents from overlapping at their destination. Add a separation pass:
// Run after NavMeshAgent updates, in LateUpdate or a system tick
public static Vector3 CalculateSeparation(
Vector3 agentPosition,
Vector3[] neighborPositions,
int neighborCount,
float desiredSeparation)
{
Vector3 separationForce = Vector3.zero;
for (int neighborIndex = 0; neighborIndex < neighborCount; neighborIndex++)
{
Vector3 offset = agentPosition - neighborPositions[neighborIndex];
float distance = offset.magnitude;
if (distance > 0f && distance < desiredSeparation)
{
separationForce += offset.normalized * (desiredSeparation - distance);
}
}
return separationForce;
}
Apply the separation force as a velocity offset or adjust the agent's destination by a small amount. Do not fight the NavMeshAgent's own steering — nudge, do not override.
When many agents share a destination (e.g., all enemies rushing a chokepoint):
| Agent Count | Recommendation |
|---|---|
| 1-20 | No special handling needed |
| 20-50 | Stagger repath timers, use avoidance priority |
| 50-100 | Reduce repath frequency, disable avoidance on distant agents |
| 100+ | Use simple steering for distant agents, only NavMeshAgent for nearby |
For agents far from the camera, consider disabling NavMeshAgent and using simple Vector3.MoveTowards until they enter a relevant range.
When an agent traverses an off-mesh link, m_Agent.isOnOffMeshLink becomes true. The agent will not auto-complete the traversal unless you handle it:
private void Update()
{
if (m_Agent.isOnOffMeshLink)
{
// Complete the link traversal
m_Agent.CompleteOffMeshLink();
}
}
For animated traversals (jumping, climbing), use m_Agent.autoTraverseOffMeshLink = false and manually move the agent across the link with UniTask, then call CompleteOffMeshLink().
The NavMesh is baked with a specific agent radius and height. If the NavMeshAgent component's radius differs from the bake settings, the agent may:
Always match NavMeshAgent radius/height to the NavMeshSurface agent settings. For different agent sizes (small minion vs large boss), bake multiple NavMesh surfaces with different agent type settings.
SetDestination fails silently if the agent is not on the NavMesh. This happens when:
WarpAlways verify placement:
private bool IsAgentOnNavMesh()
{
NavMeshHit hit;
return NavMesh.SamplePosition(m_Agent.transform.position, out hit, 0.5f, NavMesh.AllAreas)
&& m_Agent.isOnNavMesh;
}
Destroy on a NavMeshAgent component and re-adding it later is expensive and error-pronem_Agent.enabled = false) and deactivate the GameObject for poolingSetDestination on a disabled agent — it will throw or fail silently depending on Unity versionBy default, NavMeshAgent controls both position and rotation. If you also apply forces via Rigidbody or animate root motion:
m_Agent.updatePosition = false and sync manually via m_Agent.nextPositionm_Agent.updateRotation = false if you handle rotation through animation or custom code