From e7daa630101d9c80ec9b949eb3d9c7d07bd62a19 Mon Sep 17 00:00:00 2001 From: "JSD\\13999" <1399945104@qq.com> Date: Sat, 13 Jun 2026 14:41:55 +0800 Subject: [PATCH] Add Astar RVO benchmark adapter --- .../Benchmark/AvoidancePerfBootstrap.cs | 77 ++++- .../FrameworkAdapters/AstarRvoAdapter.cs | 286 ++++++++++++++++++ .../FrameworkAdapters/AstarRvoAdapter.cs.meta | 11 + .../AvoidanceAdapterRegistry.cs | 1 + .../FrameworkAdapters/NoAvoidanceAdapter.cs | 4 +- .../UnavailableAvoidanceAdapter.cs | 2 +- .../Docs/local-avoidance-benchmark-notes.md | 9 + 7 files changed, 371 insertions(+), 19 deletions(-) create mode 100644 FishROV/Assets/Scripts/Benchmark/FrameworkAdapters/AstarRvoAdapter.cs create mode 100644 FishROV/Assets/Scripts/Benchmark/FrameworkAdapters/AstarRvoAdapter.cs.meta diff --git a/FishROV/Assets/Scripts/Benchmark/AvoidancePerfBootstrap.cs b/FishROV/Assets/Scripts/Benchmark/AvoidancePerfBootstrap.cs index b725e05..6f1b7a8 100644 --- a/FishROV/Assets/Scripts/Benchmark/AvoidancePerfBootstrap.cs +++ b/FishROV/Assets/Scripts/Benchmark/AvoidancePerfBootstrap.cs @@ -26,8 +26,8 @@ namespace FishROV.AvoidanceBenchmark private void Start() { - Application.targetFrameRate = 60; QualitySettings.vSyncCount = 0; + Application.targetFrameRate = -1; pendingFishCount = fishCount; metrics.Start(); EnsureCameraAndLight(); @@ -63,33 +63,34 @@ namespace FishROV.AvoidanceBenchmark private void OnGUI() { GUILayout.BeginArea(new Rect(12, 12, 390, Screen.height - 24), GUI.skin.box); - GUILayout.Label("FishROV Local Avoidance Benchmark"); - GUILayout.Label($"Adapter: {adapter?.Name ?? "None"}"); - GUILayout.Label($"Status: {adapter?.Status ?? "Not initialized"}"); - GUILayout.Label($"Agents: fish {fishCount}, shark {sharkCount}, total {agents.Count}"); - GUILayout.Label($"FPS avg {metrics.AverageFps:F1} | 1% low {metrics.OnePercentLowFps:F1}"); - GUILayout.Label($"Simulation {metrics.SimulationMs:F2} ms | GC {metrics.GcAllocBytes / 1024f:F1} KB"); + GUILayout.Label("FishROV 本地避障压测"); + GUILayout.Label($"当前方案:{adapter?.Name ?? "未初始化"}"); + GUILayout.Label($"状态:{adapter?.Status ?? "未初始化"}"); + GUILayout.Label($"对象数量:小鱼 {fishCount},鲨鱼 {sharkCount},总数 {agents.Count}"); + GUILayout.Label($"帧率:平均 {metrics.AverageFps:F1} | 1% Low {metrics.OnePercentLowFps:F1}"); + GUILayout.Label($"模拟耗时:{metrics.SimulationMs:F2} ms | GC:{metrics.GcAllocBytes / 1024f:F1} KB"); + GUILayout.Label($"帧率限制:不锁帧,vSync={QualitySettings.vSyncCount},targetFrameRate={Application.targetFrameRate}"); GUILayout.Space(8); - DrawEnumButtons("Framework", framework, value => + DrawEnumButtons("避障方案", framework, value => { framework = value; ResetBenchmark(); }); - DrawEnumButtons("Scenario", scenario, value => + DrawEnumButtons("测试场景", scenario, value => { scenario = value; ResetBenchmark(); }); - DrawEnumButtons("Dimension", dimensionMode, value => + DrawEnumButtons("计算维度", dimensionMode, value => { dimensionMode = value; ResetBenchmark(); }); - DrawEnumButtons("View", viewMode, value => viewMode = value); + DrawEnumButtons("观察视角", viewMode, value => viewMode = value); GUILayout.Space(8); - GUILayout.Label($"Fish Count: {pendingFishCount}"); + GUILayout.Label($"小鱼数量:{pendingFishCount}"); GUILayout.BeginHorizontal(); foreach (var preset in CountPresets) { @@ -105,18 +106,18 @@ namespace FishROV.AvoidanceBenchmark pendingFishCount = Mathf.RoundToInt(GUILayout.HorizontalSlider(pendingFishCount, 10, 5000)); GUILayout.BeginHorizontal(); - if (GUILayout.Button("Apply Count")) + if (GUILayout.Button("应用数量")) { fishCount = pendingFishCount; ResetBenchmark(); } - if (GUILayout.Button(paused ? "Resume" : "Pause")) + if (GUILayout.Button(paused ? "继续" : "暂停")) { paused = !paused; } - if (GUILayout.Button("Reset")) + if (GUILayout.Button("重置")) { ResetBenchmark(); } @@ -322,7 +323,7 @@ namespace FishROV.AvoidanceBenchmark foreach (T value in System.Enum.GetValues(typeof(T))) { GUI.enabled = !EqualityComparer.Default.Equals(value, current); - if (GUILayout.Button(value.ToString(), GUILayout.Height(24))) + if (GUILayout.Button(GetDisplayName(value), GUILayout.Height(24))) { onChanged(value); } @@ -330,5 +331,49 @@ namespace FishROV.AvoidanceBenchmark GUI.enabled = true; GUILayout.EndHorizontal(); } + + private static string GetDisplayName(T value) where T : System.Enum + { + var boxed = (object)value; + switch (boxed) + { + case AvoidanceFrameworkKind.NoAvoidance: + return "无避障"; + case AvoidanceFrameworkKind.AstarRvo: + return "A* RVO"; + case AvoidanceFrameworkKind.Rvo2: + return "RVO2"; + case AvoidanceFrameworkKind.UnityNavMesh: + return "NavMesh"; + case AvoidanceScenarioKind.FreeSwim: + return "自由巡游"; + case AvoidanceScenarioKind.CrossFlow: + return "对向穿流"; + case AvoidanceScenarioKind.SharkCharge: + return "鲨鱼冲群"; + case AvoidanceScenarioKind.NarrowPassage: + return "狭窄通道"; + case AvoidanceScenarioKind.DenseCircleStress: + return "圆阵高压"; + case AvoidanceDimensionMode.XZ2D: + return "2D/XZ"; + case AvoidanceDimensionMode.XZ25D: + return "2.5D"; + case AvoidanceDimensionMode.XYZ3D: + return "3D/XYZ"; + case BenchmarkViewMode.Overview: + return "总览"; + case BenchmarkViewMode.TopDown: + return "俯视"; + case BenchmarkViewMode.Side: + return "侧视"; + case BenchmarkViewMode.Shark: + return "跟鲨鱼"; + case BenchmarkViewMode.FollowSchool: + return "跟鱼群"; + default: + return value.ToString(); + } + } } } diff --git a/FishROV/Assets/Scripts/Benchmark/FrameworkAdapters/AstarRvoAdapter.cs b/FishROV/Assets/Scripts/Benchmark/FrameworkAdapters/AstarRvoAdapter.cs new file mode 100644 index 0000000..d0c66ea --- /dev/null +++ b/FishROV/Assets/Scripts/Benchmark/FrameworkAdapters/AstarRvoAdapter.cs @@ -0,0 +1,286 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using UnityEngine; + +namespace FishROV.AvoidanceBenchmark +{ + public sealed class AstarRvoAdapter : IAvoidanceAdapter + { + private const BindingFlags MemberFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; + + private readonly Dictionary bindings = + new Dictionary(512); + + private Type rvoControllerType; + private Type rvoSimulatorType; + private MethodInfo setTargetMethod; + private MethodInfo calculateMovementDeltaMethod; + private GameObject simulatorObject; + private AvoidanceBenchmarkConfig config; + private string status = "Not initialized"; + + public string Name => "A* RVO 本地避障"; + public bool IsAvailable => rvoControllerType != null && rvoSimulatorType != null; + public string Status => status; + + public void Initialize(AvoidanceBenchmarkConfig config) + { + this.config = config; + ResolveAstarTypes(); + + if (!IsAvailable) + { + status = "未检测到 A* Pathfinding Project Pro 的 RVO 模块;请导入 A* Pro,确保存在 Pathfinding.RVO.RVOController 和 RVOSimulator。当前退回基线移动。"; + return; + } + + simulatorObject = new GameObject("A* RVO 模拟器"); + var simulator = simulatorObject.AddComponent(rvoSimulatorType); + ConfigureSimulator(simulator); + + var dimensionNote = config.DimensionMode == AvoidanceDimensionMode.XYZ3D + ? "3D 压测会投影到 A* RVO 的 XZ 平面" + : "XZ 平面"; + status = $"A* Pro RVO 已通过反射接入({dimensionNote})。"; + } + + public BenchmarkAgent CreateAgent(BenchmarkAgentSpawnInfo spawnInfo, Transform parent) + { + var agent = BenchmarkAgent.CreatePrimitive(spawnInfo, parent); + if (!IsAvailable) + { + return agent; + } + + var controller = agent.gameObject.AddComponent(rvoControllerType); + ConfigureController(controller, spawnInfo); + bindings[agent] = new RvoAgentBinding(agent, controller); + return agent; + } + + public void SetPreferredVelocity(BenchmarkAgent agent, Vector3 preferredVelocity) + { + if (!IsAvailable || !bindings.TryGetValue(agent, out var binding)) + { + agent.ApplyVelocity(preferredVelocity); + return; + } + + binding.PreferredVelocity = Vector3.ClampMagnitude(preferredVelocity, agent.MaxSpeed); + } + + public void SetTarget(BenchmarkAgent agent, Vector3 target) + { + agent.Target = target; + } + + public void Tick(float deltaTime) + { + if (!IsAvailable || deltaTime <= 0f) + { + return; + } + + foreach (var binding in bindings.Values) + { + var velocity = CalculateAvoidedVelocity(binding, deltaTime); + binding.Agent.ApplyVelocity(velocity); + } + } + + public void Dispose() + { + bindings.Clear(); + if (simulatorObject != null) + { + UnityEngine.Object.Destroy(simulatorObject); + simulatorObject = null; + } + } + + private void ResolveAstarTypes() + { + rvoControllerType = FindType("Pathfinding.RVO.RVOController"); + rvoSimulatorType = FindType("Pathfinding.RVO.RVOSimulator"); + + if (rvoControllerType == null) + { + return; + } + + setTargetMethod = rvoControllerType.GetMethod( + "SetTarget", + MemberFlags, + null, + new[] { typeof(Vector3), typeof(float), typeof(float), typeof(Vector3) }, + null); + calculateMovementDeltaMethod = rvoControllerType.GetMethod( + "CalculateMovementDelta", + MemberFlags, + null, + new[] { typeof(Vector3), typeof(float) }, + null); + } + + private void ConfigureSimulator(object simulator) + { + SetMember(simulator, "desiredSimulationFPS", 60); + SetMember(simulator, "movementPlane", "XZ"); + SetMember(simulator, "doubleBuffering", true); + SetMember(simulator, "workerThreads", "None"); + SetMember(simulator, "symmetryBreakingBias", 0.1f); + } + + private void ConfigureController(object controller, BenchmarkAgentSpawnInfo spawnInfo) + { + SetMember(controller, "radius", spawnInfo.Radius); + SetMember(controller, "height", Mathf.Max(spawnInfo.Height, spawnInfo.Radius * 2f)); + SetMember(controller, "center", Vector3.up * spawnInfo.Height * 0.5f); + SetMember(controller, "maxSpeed", spawnInfo.MaxSpeed); + SetMember(controller, "priority", Mathf.Clamp01(spawnInfo.Priority)); + SetMember(controller, "layer", spawnInfo.Layer); + SetMember(controller, "collidesWith", spawnInfo.CollidesWith); + SetMember(controller, "locked", spawnInfo.IsKinematicObstacle); + SetMember(controller, "lockWhenNotMoving", spawnInfo.IsKinematicObstacle); + } + + private Vector3 CalculateAvoidedVelocity(RvoAgentBinding binding, float deltaTime) + { + var agent = binding.Agent; + var preferredVelocity = binding.PreferredVelocity; + var position = agent.transform.position; + var target = position + preferredVelocity; + var speed = preferredVelocity.magnitude; + + if (setTargetMethod != null) + { + TryInvoke( + setTargetMethod, + binding.Controller, + new object[] { target, speed, agent.MaxSpeed, target }); + } + + if (calculateMovementDeltaMethod == null) + { + return preferredVelocity; + } + + try + { + var delta = calculateMovementDeltaMethod.Invoke( + binding.Controller, + new object[] { position, deltaTime }); + if (delta is Vector3 movementDelta) + { + return movementDelta / deltaTime; + } + } + catch (Exception exception) + { + status = $"A* RVO 反射调用失败:{exception.GetBaseException().Message}"; + } + + return preferredVelocity; + } + + private static void TryInvoke(MethodInfo method, object target, object[] parameters) + { + try + { + method.Invoke(target, parameters); + } + catch + { + // Keep the benchmark running even when an A* version changes a reflected member. + } + } + + private static void SetMember(object target, string name, object value) + { + if (target == null) + { + return; + } + + var type = target.GetType(); + var property = type.GetProperty(name, MemberFlags); + if (property != null && property.CanWrite) + { + TrySetValue(property.PropertyType, value, converted => property.SetValue(target, converted, null)); + return; + } + + var field = type.GetField(name, MemberFlags); + if (field != null) + { + TrySetValue(field.FieldType, value, converted => field.SetValue(target, converted)); + } + } + + private static void TrySetValue(Type targetType, object value, Action setter) + { + try + { + setter(ConvertValue(targetType, value)); + } + catch + { + // Optional reflected configuration only; defaults are acceptable across A* versions. + } + } + + private static object ConvertValue(Type targetType, object value) + { + if (value == null || targetType.IsInstanceOfType(value)) + { + return value; + } + + if (targetType.IsEnum) + { + if (value is string enumName) + { + return Enum.Parse(targetType, enumName); + } + + return Enum.ToObject(targetType, value); + } + + return Convert.ChangeType(value, targetType); + } + + private static Type FindType(string fullName) + { + var direct = Type.GetType(fullName); + if (direct != null) + { + return direct; + } + + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + var type = assembly.GetType(fullName); + if (type != null) + { + return type; + } + } + + return null; + } + + private sealed class RvoAgentBinding + { + public RvoAgentBinding(BenchmarkAgent agent, object controller) + { + Agent = agent; + Controller = controller; + } + + public BenchmarkAgent Agent { get; } + public object Controller { get; } + public Vector3 PreferredVelocity { get; set; } + } + } +} diff --git a/FishROV/Assets/Scripts/Benchmark/FrameworkAdapters/AstarRvoAdapter.cs.meta b/FishROV/Assets/Scripts/Benchmark/FrameworkAdapters/AstarRvoAdapter.cs.meta new file mode 100644 index 0000000..55cd477 --- /dev/null +++ b/FishROV/Assets/Scripts/Benchmark/FrameworkAdapters/AstarRvoAdapter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3f0e3f32e10e42eda4a2c6e58d0f9f8b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/FishROV/Assets/Scripts/Benchmark/FrameworkAdapters/AvoidanceAdapterRegistry.cs b/FishROV/Assets/Scripts/Benchmark/FrameworkAdapters/AvoidanceAdapterRegistry.cs index 0b82cf7..14987ce 100644 --- a/FishROV/Assets/Scripts/Benchmark/FrameworkAdapters/AvoidanceAdapterRegistry.cs +++ b/FishROV/Assets/Scripts/Benchmark/FrameworkAdapters/AvoidanceAdapterRegistry.cs @@ -9,6 +9,7 @@ namespace FishROV.AvoidanceBenchmark case AvoidanceFrameworkKind.NoAvoidance: return new NoAvoidanceAdapter(); case AvoidanceFrameworkKind.AstarRvo: + return new AstarRvoAdapter(); case AvoidanceFrameworkKind.Rvo2: case AvoidanceFrameworkKind.UnityNavMesh: return new UnavailableAvoidanceAdapter(kind); diff --git a/FishROV/Assets/Scripts/Benchmark/FrameworkAdapters/NoAvoidanceAdapter.cs b/FishROV/Assets/Scripts/Benchmark/FrameworkAdapters/NoAvoidanceAdapter.cs index a24ec90..ddf0ffe 100644 --- a/FishROV/Assets/Scripts/Benchmark/FrameworkAdapters/NoAvoidanceAdapter.cs +++ b/FishROV/Assets/Scripts/Benchmark/FrameworkAdapters/NoAvoidanceAdapter.cs @@ -4,9 +4,9 @@ namespace FishROV.AvoidanceBenchmark { public sealed class NoAvoidanceAdapter : IAvoidanceAdapter { - public string Name => "No Avoidance"; + public string Name => "无避障"; public bool IsAvailable => true; - public string Status => "Baseline movement only"; + public string Status => "基线移动:只按期望速度移动,不做避障。"; public void Initialize(AvoidanceBenchmarkConfig config) { diff --git a/FishROV/Assets/Scripts/Benchmark/FrameworkAdapters/UnavailableAvoidanceAdapter.cs b/FishROV/Assets/Scripts/Benchmark/FrameworkAdapters/UnavailableAvoidanceAdapter.cs index ad866da..2e2e5fe 100644 --- a/FishROV/Assets/Scripts/Benchmark/FrameworkAdapters/UnavailableAvoidanceAdapter.cs +++ b/FishROV/Assets/Scripts/Benchmark/FrameworkAdapters/UnavailableAvoidanceAdapter.cs @@ -13,7 +13,7 @@ namespace FishROV.AvoidanceBenchmark public string Name => kind.ToString(); public bool IsAvailable => false; - public string Status => "Adapter placeholder. Implement this framework in its worktree."; + public string Status => "当前方案在这个 worktree 中只是占位;请选择本分支要测试的避障方案。"; public void Initialize(AvoidanceBenchmarkConfig config) { diff --git a/FishROV/Docs/local-avoidance-benchmark-notes.md b/FishROV/Docs/local-avoidance-benchmark-notes.md index b9eed64..25dea6f 100644 --- a/FishROV/Docs/local-avoidance-benchmark-notes.md +++ b/FishROV/Docs/local-avoidance-benchmark-notes.md @@ -46,3 +46,12 @@ Adapter implementations may add package-specific components to created agents, b `NoAvoidanceAdapter` is the control group. It moves agents directly by preferred velocity and does not perform avoidance. The A* RVO, RVO2, and Unity NavMesh adapters currently start as placeholders so their worktree diffs stay easy to review. + +## A* Pathfinding Project Pro RVO Adapter + +`AstarRvoAdapter` is isolated under `Assets/Scripts/Benchmark/FrameworkAdapters/` and is selected only by `AvoidanceFrameworkKind.AstarRvo`. + +- It uses reflection to find `Pathfinding.RVO.RVOController` and `Pathfinding.RVO.RVOSimulator`, so the project still compiles when A* Pathfinding Project Pro is not imported. +- When the A* Pro RVO types are missing, the Game view status reports the missing package and the agents fall back to baseline preferred-velocity movement. +- When available, the adapter creates one runtime `RVOSimulator`, adds `RVOController` components to benchmark-created agents, feeds each preferred velocity into A* RVO, and writes the reflected avoidance velocity back through `BenchmarkAgent.ApplyVelocity`. +- The shared benchmark scene, HUD, scenarios, count presets, metrics, and camera logic remain unchanged; the adapter is the only intended variable for this branch.