diff --git a/FishROV/Assets/Scripts/Benchmark/FrameworkAdapters/AvoidanceAdapterRegistry.cs b/FishROV/Assets/Scripts/Benchmark/FrameworkAdapters/AvoidanceAdapterRegistry.cs index d6b9350..c3b3577 100644 --- a/FishROV/Assets/Scripts/Benchmark/FrameworkAdapters/AvoidanceAdapterRegistry.cs +++ b/FishROV/Assets/Scripts/Benchmark/FrameworkAdapters/AvoidanceAdapterRegistry.cs @@ -13,7 +13,7 @@ namespace FishROV.AvoidanceBenchmark case AvoidanceFrameworkKind.Rvo2: return new Rvo2Adapter(); case AvoidanceFrameworkKind.UnityNavMesh: - return new UnavailableAvoidanceAdapter(kind); + return new UnityNavMeshAdapter(); default: return new UnavailableAvoidanceAdapter(kind); } diff --git a/FishROV/Assets/Scripts/Benchmark/FrameworkAdapters/UnityNavMeshAdapter.cs b/FishROV/Assets/Scripts/Benchmark/FrameworkAdapters/UnityNavMeshAdapter.cs new file mode 100644 index 0000000..f584c74 --- /dev/null +++ b/FishROV/Assets/Scripts/Benchmark/FrameworkAdapters/UnityNavMeshAdapter.cs @@ -0,0 +1,150 @@ +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.AI; + +namespace FishROV.AvoidanceBenchmark +{ + public sealed class UnityNavMeshAdapter : IAvoidanceAdapter + { + private readonly Dictionary navAgents = new Dictionary(512); + private AvoidanceBenchmarkConfig config; + private bool hasNavMeshData; + private int fallbackAgentCount; + + public string Name => "Unity NavMeshAgent"; + public bool IsAvailable => hasNavMeshData; + + public string Status + { + get + { + if (!hasNavMeshData) + { + return "No runtime NavMesh data found; falling back to baseline movement. Bake a NavMesh or enable com.unity.ai.navigation and build a runtime NavMeshSurface."; + } + + if (fallbackAgentCount > 0) + { + return $"Using NavMeshAgent for {navAgents.Count} agents; {fallbackAgentCount} agents could not be placed on the NavMesh and use baseline movement."; + } + + return $"Using Unity NavMeshAgent avoidance for {navAgents.Count} agents."; + } + } + + public void Initialize(AvoidanceBenchmarkConfig config) + { + this.config = config.Clone(); + navAgents.Clear(); + fallbackAgentCount = 0; + + var triangulation = NavMesh.CalculateTriangulation(); + hasNavMeshData = triangulation.vertices != null && triangulation.vertices.Length > 0; + } + + public BenchmarkAgent CreateAgent(BenchmarkAgentSpawnInfo spawnInfo, Transform parent) + { + var benchmarkAgent = BenchmarkAgent.CreatePrimitive(spawnInfo, parent); + if (!hasNavMeshData) + { + return benchmarkAgent; + } + + if (!TryPlaceOnNavMesh(benchmarkAgent, spawnInfo)) + { + fallbackAgentCount++; + return benchmarkAgent; + } + + var navAgent = benchmarkAgent.gameObject.AddComponent(); + ConfigureNavMeshAgent(navAgent, spawnInfo); + navAgents[benchmarkAgent] = navAgent; + return benchmarkAgent; + } + + public void SetPreferredVelocity(BenchmarkAgent agent, Vector3 preferredVelocity) + { + if (!hasNavMeshData || !navAgents.TryGetValue(agent, out var navAgent) || !navAgent.isOnNavMesh) + { + agent.ApplyVelocity(preferredVelocity); + return; + } + + var navVelocity = preferredVelocity; + if (config.DimensionMode != AvoidanceDimensionMode.XYZ3D) + { + navVelocity.y = 0f; + } + + navAgent.nextPosition = agent.transform.position; + navAgent.velocity = Vector3.ClampMagnitude(navVelocity, agent.MaxSpeed); + } + + public void SetTarget(BenchmarkAgent agent, Vector3 target) + { + agent.Target = target; + } + + public void Tick(float deltaTime) + { + if (!hasNavMeshData) + { + return; + } + + foreach (var entry in navAgents) + { + var benchmarkAgent = entry.Key; + var navAgent = entry.Value; + if (benchmarkAgent == null || navAgent == null || !navAgent.isOnNavMesh) + { + continue; + } + + benchmarkAgent.ApplyVelocity(navAgent.velocity); + navAgent.nextPosition = benchmarkAgent.transform.position; + } + } + + public void Dispose() + { + foreach (var navAgent in navAgents.Values) + { + if (navAgent != null) + { + Object.Destroy(navAgent); + } + } + + navAgents.Clear(); + fallbackAgentCount = 0; + } + + private bool TryPlaceOnNavMesh(BenchmarkAgent benchmarkAgent, BenchmarkAgentSpawnInfo spawnInfo) + { + var searchDistance = Mathf.Max(2f, spawnInfo.Height + spawnInfo.Radius); + if (!NavMesh.SamplePosition(spawnInfo.Position, out var hit, searchDistance, NavMesh.AllAreas)) + { + return false; + } + + benchmarkAgent.transform.position = hit.position; + return true; + } + + private static void ConfigureNavMeshAgent(NavMeshAgent navAgent, BenchmarkAgentSpawnInfo spawnInfo) + { + navAgent.radius = Mathf.Max(0.01f, spawnInfo.Radius); + navAgent.height = Mathf.Max(0.1f, spawnInfo.Height); + navAgent.speed = Mathf.Max(0.01f, spawnInfo.MaxSpeed); + navAgent.acceleration = navAgent.speed * 8f; + navAgent.angularSpeed = 720f; + navAgent.obstacleAvoidanceType = ObstacleAvoidanceType.HighQualityObstacleAvoidance; + navAgent.avoidancePriority = Mathf.Clamp(Mathf.RoundToInt((1f - spawnInfo.Priority) * 99f), 0, 99); + navAgent.autoBraking = false; + navAgent.autoRepath = false; + navAgent.updatePosition = false; + navAgent.updateRotation = false; + } + } +} diff --git a/FishROV/Assets/Scripts/Benchmark/FrameworkAdapters/UnityNavMeshAdapter.cs.meta b/FishROV/Assets/Scripts/Benchmark/FrameworkAdapters/UnityNavMeshAdapter.cs.meta new file mode 100644 index 0000000..86600cd --- /dev/null +++ b/FishROV/Assets/Scripts/Benchmark/FrameworkAdapters/UnityNavMeshAdapter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7d3b299dd0504f849b83e4af9ed272a7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/FishROV/Docs/local-avoidance-benchmark-notes.md b/FishROV/Docs/local-avoidance-benchmark-notes.md index 269a00b..8e53f13 100644 --- a/FishROV/Docs/local-avoidance-benchmark-notes.md +++ b/FishROV/Docs/local-avoidance-benchmark-notes.md @@ -65,3 +65,9 @@ The A* RVO, RVO2, and Unity NavMesh adapters are selected through the same debug - The adapter looks for a simulator type such as `RVO.Simulator`, `RVO2.Simulator`, `RVO3D.Simulator`, or `RVO.Simulator3D`. - The required simulator methods are `addAgent`/`AddAgent`, `setAgentPrefVelocity`/`SetAgentPrefVelocity`, `getAgentVelocity`/`GetAgentVelocity`, and `doStep`/`DoStep`. - 2D and 2.5D modes bind to an RVO `Vector2` style type and map Unity `x/z` to RVO `x/y`. 3D mode binds to an RVO `Vector3` style type when available. + +## Unity NavMesh Adapter Notes + +`UnityNavMeshAdapter` uses the built-in `UnityEngine.AI.NavMeshAgent` API when NavMesh data is already available at runtime. The benchmark scene creates its fish and shark primitives dynamically, so a normal empty runtime cube scene may not have baked NavMesh data. In that case the adapter reports the missing NavMesh in the HUD status and falls back to baseline movement instead of adding invalid `NavMeshAgent` components. + +To exercise Unity's NavMesh avoidance path, provide NavMesh data before selecting `UnityNavMesh`. A baked scene NavMesh is sufficient. If the project should build the benchmark floor at runtime, enable `com.unity.ai.navigation` and add a runtime `NavMeshSurface` setup before the adapter initializes.