tools-unity-navmesh
Unity NavMesh pathfinding patterns, safety checks, and performance optimization.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Unity NavMesh pathfinding patterns, safety checks, and performance optimization.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
| name | tools-unity-navmesh |
| description | Unity NavMesh pathfinding patterns, safety checks, and performance optimization. |
NavMesh provides AI pathfinding in Unity. This skill covers safe usage patterns to prevent the 30K+ pathfinding errors seen in production.
using Pathfinding;
public class SafePathfinder
{
private readonly Seeker _seeker;
public bool TryCalculatePath(Vector3 start, Vector3 end, out Path path)
{
path = null;
// CRITICAL: Check graph exists
if (AstarPath.active == null)
{
Debug.LogError("No AstarPath instance active");
return false;
}
if (AstarPath.active.graphs == null || AstarPath.active.graphs.Length == 0)
{
Debug.LogError("No graphs available");
return false;
}
// Check if position is on navmesh
var startNode = AstarPath.active.GetNearest(start).node;
var endNode = AstarPath.active.GetNearest(end).node;
if (startNode == null || !startNode.Walkable)
{
Debug.LogWarning($"Start position {start} not on walkable navmesh");
return false;
}
if (endNode == null || !endNode.Walkable)
{
Debug.LogWarning($"End position {end} not on walkable navmesh");
return false;
}
// Safe to calculate path
_seeker.StartPath(start, end, OnPathComplete);
return true;
}
private void OnPathComplete(Path p)
{
if (p.error)
{
Debug.LogWarning($"Path failed: {p.errorLog}");
return;
}
// Use path
}
}
public class PathfindingGuard
{
public static bool IsPathfindingReady()
{
// Check 1: AstarPath singleton exists
if (AstarPath.active == null)
return false;
// Check 2: Data is loaded
if (AstarPath.active.data == null)
return false;
// Check 3: Graphs exist
if (AstarPath.active.data.graphs == null)
return false;
// Check 4: At least one graph is scanned
foreach (var graph in AstarPath.active.data.graphs)
{
if (graph != null && graph.CountNodes() > 0)
return true;
}
return false;
}
}
// Usage in AI
public class EnemyAI : MonoBehaviour
{
private void RequestPath()
{
if (!PathfindingGuard.IsPathfindingReady())
{
// Defer pathfinding or use fallback behavior
StartCoroutine(WaitForPathfinding());
return;
}
_seeker.StartPath(transform.position, _target.position);
}
private IEnumerator WaitForPathfinding()
{
while (!PathfindingGuard.IsPathfindingReady())
{
yield return new WaitForSeconds(0.5f);
}
RequestPath();
}
}
public class SafeNavMeshAgent : MonoBehaviour
{
private NavMeshAgent _agent;
private bool _isNavigating;
private void Awake()
{
_agent = GetComponent<NavMeshAgent>();
}
public bool TrySetDestination(Vector3 destination)
{
if (_agent == null || !_agent.isOnNavMesh)
{
Debug.LogWarning($"{name} not on NavMesh");
return false;
}
// Check if destination is reachable
NavMeshPath path = new NavMeshPath();
if (!_agent.CalculatePath(destination, path))
{
Debug.LogWarning($"Cannot calculate path to {destination}");
return false;
}
if (path.status != NavMeshPathStatus.PathComplete)
{
Debug.LogWarning($"Path incomplete: {path.status}");
return false;
}
_agent.SetPath(path);
_isNavigating = true;
return true;
}
public void Stop()
{
if (_agent != null && _agent.isOnNavMesh)
{
_agent.isStopped = true;
_agent.ResetPath();
}
_isNavigating = false;
}
private void OnDisable()
{
Stop();
}
}
public class AIPathfinder : MonoBehaviour
{
private Seeker _seeker;
private AIPath _aiPath;
private void Awake()
{
_seeker = GetComponent<Seeker>();
_aiPath = GetComponent<AIPath>();
// Configure seeker
_seeker.pathCallback += OnPathComplete;
// Set traversable tags
_seeker.traversableTags = ~0; // All tags
}
private void OnDestroy()
{
if (_seeker != null)
{
_seeker.pathCallback -= OnPathComplete;
_seeker.CancelCurrentPathRequest();
}
}
public void MoveTo(Vector3 destination)
{
if (!PathfindingGuard.IsPathfindingReady())
{
Debug.LogWarning("Pathfinding not ready");
return;
}
_aiPath.destination = destination;
_aiPath.SearchPath();
}
private void OnPathComplete(Path p)
{
if (p.error)
{
HandlePathError(p);
return;
}
// Path ready
}
private void HandlePathError(Path p)
{
Debug.LogWarning($"Path error: {p.errorLog}");
// Fallback behavior - move directly or stay
}
}
public class PathCache
{
private readonly Dictionary<(Vector3Int, Vector3Int), Path> _cache = new();
private readonly float _cacheValidTime = 5f;
private readonly Dictionary<(Vector3Int, Vector3Int), float> _cacheTimestamps = new();
private const float GridSize = 1f;
public bool TryGetCachedPath(Vector3 start, Vector3 end, out Path path)
{
var key = (ToGridPos(start), ToGridPos(end));
if (_cache.TryGetValue(key, out path))
{
if (_cacheTimestamps.TryGetValue(key, out var timestamp))
{
if (Time.time - timestamp < _cacheValidTime)
{
return true;
}
}
// Cache expired
_cache.Remove(key);
_cacheTimestamps.Remove(key);
}
path = null;
return false;
}
public void CachePath(Vector3 start, Vector3 end, Path path)
{
var key = (ToGridPos(start), ToGridPos(end));
_cache[key] = path;
_cacheTimestamps[key] = Time.time;
}
private Vector3Int ToGridPos(Vector3 pos)
{
return new Vector3Int(
Mathf.RoundToInt(pos.x / GridSize),
Mathf.RoundToInt(pos.y / GridSize),
Mathf.RoundToInt(pos.z / GridSize)
);
}
}
public class PathBatcher
{
private readonly Queue<PathRequest> _pendingRequests = new();
private readonly int _maxRequestsPerFrame = 3;
private bool _isProcessing;
public void RequestPath(Vector3 start, Vector3 end, Action<Path> callback)
{
_pendingRequests.Enqueue(new PathRequest(start, end, callback));
if (!_isProcessing)
{
ProcessBatch().Forget();
}
}
private async UniTaskVoid ProcessBatch()
{
_isProcessing = true;
while (_pendingRequests.Count > 0)
{
var processedThisFrame = 0;
while (_pendingRequests.Count > 0 && processedThisFrame < _maxRequestsPerFrame)
{
var request = _pendingRequests.Dequeue();
var path = await CalculatePathAsync(request.Start, request.End);
request.Callback?.Invoke(path);
processedThisFrame++;
}
await UniTask.Yield();
}
_isProcessing = false;
}
private async UniTask<Path> CalculatePathAsync(Vector3 start, Vector3 end)
{
var tcs = new UniTaskCompletionSource<Path>();
var seeker = AstarPath.active.GetComponent<Seeker>();
seeker.StartPath(start, end, p => tcs.TrySetResult(p));
return await tcs.Task;
}
private readonly struct PathRequest
{
public readonly Vector3 Start;
public readonly Vector3 End;
public readonly Action<Path> Callback;
public PathRequest(Vector3 start, Vector3 end, Action<Path> callback)
{
Start = start;
End = end;
Callback = callback;
}
}
}
public class LODPathfinder
{
private readonly float _preciseRadius = 20f;
private readonly float _mediumRadius = 50f;
public void RequestPath(Vector3 agent, Vector3 target)
{
var distance = Vector3.Distance(agent, target);
if (distance <= _preciseRadius)
{
// Use full detail graph
RequestPrecisePath(agent, target);
}
else if (distance <= _mediumRadius)
{
// Use simplified graph
RequestMediumPath(agent, target);
}
else
{
// Use waypoint system
RequestWaypointPath(agent, target);
}
}
}
public class DynamicObstacle : MonoBehaviour
{
private NavMeshObstacle _obstacle;
private Coroutine _carveDelayCoroutine;
private void Awake()
{
_obstacle = GetComponent<NavMeshObstacle>();
_obstacle.carving = true;
_obstacle.carveOnlyStationary = true;
}
public void EnableCarving()
{
if (_carveDelayCoroutine != null)
{
StopCoroutine(_carveDelayCoroutine);
}
// Delay carving to prevent rapid updates
_carveDelayCoroutine = StartCoroutine(DelayedCarveEnable());
}
private IEnumerator DelayedCarveEnable()
{
yield return new WaitForSeconds(0.5f);
_obstacle.carving = true;
}
public void DisableCarving()
{
if (_carveDelayCoroutine != null)
{
StopCoroutine(_carveDelayCoroutine);
}
_obstacle.carving = false;
}
}
public class NavMeshUpdater
{
public void UpdateArea(Bounds bounds)
{
if (AstarPath.active == null) return;
var graphUpdateObject = new GraphUpdateObject(bounds)
{
updatePhysics = true,
modifyWalkability = true
};
AstarPath.active.UpdateGraphs(graphUpdateObject);
}
public void UpdateAreaAsync(Bounds bounds, Action onComplete)
{
if (AstarPath.active == null)
{
onComplete?.Invoke();
return;
}
var graphUpdateObject = new GraphUpdateObject(bounds)
{
updatePhysics = true
};
AstarPath.active.UpdateGraphs(graphUpdateObject, onComplete);
}
}
public class StuckDetector : MonoBehaviour
{
private Vector3 _lastPosition;
private float _stuckTime;
private const float StuckThreshold = 0.1f;
private const float StuckTimeout = 3f;
public event Action OnStuck;
private void Update()
{
var moved = Vector3.Distance(transform.position, _lastPosition);
if (moved < StuckThreshold)
{
_stuckTime += Time.deltaTime;
if (_stuckTime >= StuckTimeout)
{
OnStuck?.Invoke();
_stuckTime = 0f;
}
}
else
{
_stuckTime = 0f;
}
_lastPosition = transform.position;
}
}
public class NavMeshRecovery
{
public bool TryRecoverPosition(Transform agent, float searchRadius = 10f)
{
// Sample nearest valid position
if (NavMesh.SamplePosition(agent.position, out var hit, searchRadius, NavMesh.AllAreas))
{
agent.position = hit.position;
return true;
}
// Try expanding search
for (float radius = searchRadius; radius <= 50f; radius += 10f)
{
if (NavMesh.SamplePosition(agent.position, out hit, radius, NavMesh.AllAreas))
{
agent.position = hit.position;
return true;
}
}
return false;
}
}
| Issue | Solution |
|---|---|
| NullReferenceException in pathfinding | Add graph null checks |
| Agent not moving | Check isOnNavMesh |
| Path incomplete | Verify destination reachable |
| Slow pathfinding | Batch requests, cache paths |
| Agent stuck | Implement stuck detection + recovery |
| Dynamic obstacles ignored | Enable carving, update graphs |
Creates an Architecture Decision Record (ADR) documenting a significant technical decision, its context, alternatives considered, and consequences. Every major technical choice should have an ADR.
Mobile-specific Unity optimization patterns for memory, battery, thermal, and performance.
End-of-story completion review. Reads the story file, verifies each acceptance criterion against the implementation, checks for GDD/ADR deviations, prompts code review, updates story status to Complete, and surfaces the next ready story from the sprint.
Unity Addressables patterns for asset loading, memory management, reference counting, and remote content delivery.
Unity animation patterns including Animancer, state machines, and performance optimization.
Behavior Designer patterns for AI behavior trees including task creation, shared variables, conditionals, and debugging.