From dc8ad8bfe687e675eca21c0ead0b6e573ae7303d Mon Sep 17 00:00:00 2001 From: "JSD\\13999" <1399945104@qq.com> Date: Mon, 15 Jun 2026 16:19:48 +0800 Subject: [PATCH] =?UTF-8?q?refactor(benchmark):=20=E6=95=B4=E7=90=86?= =?UTF-8?q?=E9=81=BF=E9=9A=9C=E5=8E=8B=E6=B5=8B=E9=80=BB=E8=BE=91=E5=92=8C?= =?UTF-8?q?=E6=8A=A5=E5=91=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Benchmark/Agents/BenchmarkAgent.cs | 89 ++- .../Benchmark/AvoidanceBenchmarkLogWriter.cs | 21 +- .../Benchmark/AvoidancePerfBootstrap.cs | 310 ++++++++- .../SamplingLocalAvoidanceAdapter.cs | 326 --------- .../SamplingLocalAvoidanceAdapter.cs.meta | 11 - .../Scenarios/AvoidanceScenarioController.cs | 68 +- .../avoidance-benchmark-report-20260613.html | 637 ++++++++++++++++++ ...ce-benchmark-report-20260615-android2.html | 153 +++++ .../avoidance-benchmark-report-20260615.html | 562 +++++++++++++++ FishROV/Docs/avoidance-core-comparison.html | 631 ----------------- .../Docs/local-avoidance-benchmark-notes.md | 68 +- FishROV/Docs/rvo-py-mas-core-principles.md | 2 +- ...ampling-local-avoidance-core-principles.md | 234 ------- 13 files changed, 1856 insertions(+), 1256 deletions(-) delete mode 100644 FishROV/Assets/Scripts/Benchmark/FrameworkAdapters/SamplingLocalAvoidanceAdapter.cs delete mode 100644 FishROV/Assets/Scripts/Benchmark/FrameworkAdapters/SamplingLocalAvoidanceAdapter.cs.meta create mode 100644 FishROV/Docs/avoidance-benchmark-report-20260613.html create mode 100644 FishROV/Docs/avoidance-benchmark-report-20260615-android2.html create mode 100644 FishROV/Docs/avoidance-benchmark-report-20260615.html delete mode 100644 FishROV/Docs/avoidance-core-comparison.html delete mode 100644 FishROV/Docs/sampling-local-avoidance-core-principles.md diff --git a/FishROV/Assets/Scripts/Benchmark/Agents/BenchmarkAgent.cs b/FishROV/Assets/Scripts/Benchmark/Agents/BenchmarkAgent.cs index 40cd5a2..b821a92 100644 --- a/FishROV/Assets/Scripts/Benchmark/Agents/BenchmarkAgent.cs +++ b/FishROV/Assets/Scripts/Benchmark/Agents/BenchmarkAgent.cs @@ -9,8 +9,10 @@ namespace FishROV.AvoidanceBenchmark private static Mesh sharedConeMesh; private static Material sharedConeMaterial; private static MaterialPropertyBlock colorPropertyBlock; + private static bool useCustomConeMaterial; private Vector3 velocity; + private Vector3 previousVelocity; private float maxSpeed = 1f; public BenchmarkAgentRole Role { get; private set; } @@ -18,8 +20,18 @@ namespace FishROV.AvoidanceBenchmark public float Radius { get; private set; } public float MaxSpeed => maxSpeed; public Vector3 Velocity => velocity; + public float CurrentSpeed { get; private set; } + public float PeakSpeed { get; private set; } + public float CurrentAcceleration { get; private set; } + public float PeakAcceleration { get; private set; } public Vector3 Target { get; set; } + public static void SetSharedConeMaterial(Material material) + { + useCustomConeMaterial = material != null; + sharedConeMaterial = material; + } + public static BenchmarkAgent CreatePrimitive(BenchmarkAgentSpawnInfo spawnInfo, Transform parent) { var go = CreateConeObject(); @@ -50,6 +62,12 @@ namespace FishROV.AvoidanceBenchmark GroupId = spawnInfo.GroupId; Radius = spawnInfo.Radius; maxSpeed = Mathf.Max(0.01f, spawnInfo.MaxSpeed); + velocity = Vector3.zero; + previousVelocity = Vector3.zero; + CurrentSpeed = 0f; + PeakSpeed = 0f; + CurrentAcceleration = 0f; + PeakAcceleration = 0f; ApplyColor(); } @@ -58,8 +76,20 @@ namespace FishROV.AvoidanceBenchmark velocity = Vector3.ClampMagnitude(preferredVelocity, maxSpeed); } + public void SetMaxSpeed(float value) + { + maxSpeed = Mathf.Max(0.01f, value); + velocity = Vector3.ClampMagnitude(velocity, maxSpeed); + } + public void Integrate(float deltaTime, AvoidanceDimensionMode dimensionMode, float arenaRadius) { + CurrentSpeed = velocity.magnitude; + PeakSpeed = Mathf.Max(PeakSpeed, CurrentSpeed); + CurrentAcceleration = deltaTime > 0f ? (velocity - previousVelocity).magnitude / deltaTime : 0f; + PeakAcceleration = Mathf.Max(PeakAcceleration, CurrentAcceleration); + previousVelocity = velocity; + var next = transform.position + velocity * deltaTime; if (dimensionMode != AvoidanceDimensionMode.XYZ3D) { @@ -91,6 +121,11 @@ namespace FishROV.AvoidanceBenchmark return; } + if (useCustomConeMaterial) + { + return; + } + var color = Color.white; switch (Role) { @@ -107,6 +142,7 @@ namespace FishROV.AvoidanceBenchmark var propertyBlock = GetColorPropertyBlock(); propertyBlock.SetColor("_Color", color); + propertyBlock.SetColor("_BaseColor", color); renderer.SetPropertyBlock(propertyBlock); } @@ -173,13 +209,64 @@ namespace FishROV.AvoidanceBenchmark return sharedConeMaterial; } - sharedConeMaterial = new Material(Shader.Find("Standard")) + useCustomConeMaterial = false; + sharedConeMaterial = FindFirstBuiltinMaterial( + "Default-Material.mat", + "Default-Diffuse.mat"); + if (sharedConeMaterial != null) + { + return sharedConeMaterial; + } + + var shader = FindFirstAvailableShader( + "Standard", + "Universal Render Pipeline/Lit", + "Universal Render Pipeline/Simple Lit", + "Universal Render Pipeline/Unlit", + "Sprites/Default", + "UI/Default"); + + if (shader == null) + { + Debug.LogError("BenchmarkAgent failed to find a built-in shader for runtime cone agents."); + return null; + } + + sharedConeMaterial = new Material(shader) { name = "Benchmark Agent Shared Material" }; return sharedConeMaterial; } + private static Material FindFirstBuiltinMaterial(params string[] materialNames) + { + for (var i = 0; i < materialNames.Length; i++) + { + var material = Resources.GetBuiltinResource(materialNames[i]); + if (material != null) + { + return material; + } + } + + return null; + } + + private static Shader FindFirstAvailableShader(params string[] shaderNames) + { + for (var i = 0; i < shaderNames.Length; i++) + { + var shader = Shader.Find(shaderNames[i]); + if (shader != null) + { + return shader; + } + } + + return null; + } + private static MaterialPropertyBlock GetColorPropertyBlock() { return colorPropertyBlock ??= new MaterialPropertyBlock(); diff --git a/FishROV/Assets/Scripts/Benchmark/AvoidanceBenchmarkLogWriter.cs b/FishROV/Assets/Scripts/Benchmark/AvoidanceBenchmarkLogWriter.cs index a953c5c..00284df 100644 --- a/FishROV/Assets/Scripts/Benchmark/AvoidanceBenchmarkLogWriter.cs +++ b/FishROV/Assets/Scripts/Benchmark/AvoidanceBenchmarkLogWriter.cs @@ -37,6 +37,7 @@ namespace FishROV.AvoidanceBenchmark public void Record( AvoidanceMetricsRecorder metrics, AvoidanceCollisionProbe collisionProbe, + AvoidancePerfBootstrap.MotionStats motionStats, int agentCount) { if (!hasRun || Time.unscaledTime < nextSampleTime || samples.Count >= MaxSamples) @@ -55,7 +56,11 @@ namespace FishROV.AvoidanceBenchmark GcAllocKb = metrics.GcAllocBytes / 1024.0, HorizontalOverlapPairs = collisionProbe.HorizontalOverlapPairs, SpatialOverlapPairs = collisionProbe.SpatialOverlapPairs, - MinimumClearance = collisionProbe.MinimumClearance + MinimumClearance = collisionProbe.MinimumClearance, + AverageSpeed = motionStats.AverageSpeed, + MaxSpeed = motionStats.MaxSpeed, + AverageAcceleration = motionStats.AverageAcceleration, + MaxAcceleration = motionStats.MaxAcceleration }); } @@ -72,7 +77,7 @@ namespace FishROV.AvoidanceBenchmark using (var writer = new StreamWriter(path, false)) { - writer.WriteLine("framework,scenario,dimension,fishCount,arenaRadius,screenWidth,screenHeight,targetFrameRate,timeSeconds,agentCount,averageFps,onePercentLowFps,simulationMs,gcAllocKb,horizontalOverlapPairs,spatialOverlapPairs,minimumClearance"); + writer.WriteLine("framework,scenario,dimension,fishCount,arenaRadius,screenWidth,screenHeight,targetFrameRate,timeSeconds,agentCount,averageFps,onePercentLowFps,simulationMs,gcAllocKb,horizontalOverlapPairs,spatialOverlapPairs,minimumClearance,averageSpeed,maxSpeed,averageAcceleration,maxAcceleration"); for (var i = 0; i < samples.Count; i++) { var sample = samples[i]; @@ -109,6 +114,14 @@ namespace FishROV.AvoidanceBenchmark writer.Write(sample.SpatialOverlapPairs); writer.Write(','); WriteFloat(writer, sample.MinimumClearance); + writer.Write(','); + WriteFloat(writer, sample.AverageSpeed); + writer.Write(','); + WriteFloat(writer, sample.MaxSpeed); + writer.Write(','); + WriteFloat(writer, sample.AverageAcceleration); + writer.Write(','); + WriteFloat(writer, sample.MaxAcceleration); writer.WriteLine(); } } @@ -151,6 +164,10 @@ namespace FishROV.AvoidanceBenchmark public int HorizontalOverlapPairs; public int SpatialOverlapPairs; public float MinimumClearance; + public float AverageSpeed; + public float MaxSpeed; + public float AverageAcceleration; + public float MaxAcceleration; } } } diff --git a/FishROV/Assets/Scripts/Benchmark/AvoidancePerfBootstrap.cs b/FishROV/Assets/Scripts/Benchmark/AvoidancePerfBootstrap.cs index 2e3e6b9..0813218 100644 --- a/FishROV/Assets/Scripts/Benchmark/AvoidancePerfBootstrap.cs +++ b/FishROV/Assets/Scripts/Benchmark/AvoidancePerfBootstrap.cs @@ -12,6 +12,13 @@ namespace FishROV.AvoidanceBenchmark [SerializeField] private AvoidanceDimensionMode dimensionMode = AvoidanceDimensionMode.XZ25D; [SerializeField] private BenchmarkViewMode viewMode = BenchmarkViewMode.Overview; [SerializeField] private int fishCount = 300; + [SerializeField] private Material fishMaterial; + [SerializeField] private float fishMaxSpeed = 2.4f; + [SerializeField] private float baitDemoMaxSpeed = 4.2f; + [SerializeField] private float speedMultiplier = 1f; + [SerializeField] private float timeScale = 1f; + [SerializeField] private float hookDropHeight = 10f; + [SerializeField] private float hookDropDuration = 1.5f; private readonly List agents = new List(512); private readonly AvoidanceScenarioController scenarioController = new AvoidanceScenarioController(); @@ -21,17 +28,25 @@ namespace FishROV.AvoidanceBenchmark private AvoidanceBenchmarkConfig config; private IAvoidanceAdapter adapter; private Transform agentRoot; + private Transform baitMarker; private Camera benchmarkCamera; private bool paused; private int pendingFishCount; + private MotionStats motionStats; + private bool hookDropping; + private float hookDropElapsed; + private Vector3 hookDropStart; + private Vector3 hookDropTarget; private void Start() { QualitySettings.vSyncCount = 0; Application.targetFrameRate = -1; + ApplyTimeScale(); pendingFishCount = fishCount; metrics.Start(); EnsureCameraAndLight(); + EnsureBaitMarker(); ResetBenchmark(); } @@ -46,6 +61,9 @@ namespace FishROV.AvoidanceBenchmark } metrics.BeginSimulation(); + UpdateHookDrop(Time.deltaTime); + ApplyRuntimeSpeed(); + scenarioController.Tick(Time.deltaTime); foreach (var agent in agents) { var velocity = scenarioController.GetPreferredVelocity(agent, Time.deltaTime); @@ -58,24 +76,40 @@ namespace FishROV.AvoidanceBenchmark agent.Integrate(Time.deltaTime, config.DimensionMode, config.ArenaRadius); } + motionStats = CalculateMotionStats(); collisionProbe.Update(agents, config.DimensionMode, Time.unscaledDeltaTime); metrics.EndSimulation(); - logWriter.Record(metrics, collisionProbe, agents.Count); + logWriter.Record(metrics, collisionProbe, motionStats, agents.Count); } private void OnGUI() { - GUILayout.BeginArea(new Rect(12, 12, 390, Screen.height - 24), GUI.skin.box); + GUILayout.BeginArea(new Rect(12, 12, 430, Screen.height - 24), GUI.skin.box); GUILayout.Label("FishROV 本地避障压测"); GUILayout.Label($"当前方案:{adapter?.Name ?? "未初始化"}"); GUILayout.Label($"状态:{adapter?.Status ?? "未初始化"}"); - GUILayout.Label($"对象数量:鱼 {fishCount},总数 {agents.Count}"); + GUILayout.Label($"鱼数量:{fishCount} | 对象总数:{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($"碰撞调试:XZ重叠 {collisionProbe.HorizontalOverlapPairs} 对 | 3D重叠 {collisionProbe.SpatialOverlapPairs} 对 | 最近间距 {collisionProbe.MinimumClearance:F2}"); - GUILayout.Label($"帧率限制:不锁帧,vSync={QualitySettings.vSyncCount},targetFrameRate={Application.targetFrameRate}"); + GUILayout.Label($"模拟耗时:{metrics.SimulationMs:F2} ms | GC {metrics.GcAllocBytes / 1024f:F1} KB"); + GUILayout.Label($"速度:极速设定 {(config?.FishSpeed ?? fishMaxSpeed):F1} | 实测峰值 {motionStats.MaxSpeed:F2} | 平均 {motionStats.AverageSpeed:F2}"); + GUILayout.Label($"加速度:峰值 {motionStats.MaxAcceleration:F2} | 平均 {motionStats.AverageAcceleration:F2}"); + GUILayout.Label($"调试倍率:速度 x{speedMultiplier:F2} | 时间 x{timeScale:F2}"); + GUILayout.Label(hookDropping + ? $"鱼钩下落中:{Mathf.Clamp01(hookDropElapsed / Mathf.Max(0.01f, hookDropDuration)) * 100f:F0}%" + : "鱼钩状态:未下落"); + GUILayout.Label($"重叠:XZ {collisionProbe.HorizontalOverlapPairs} | 3D {collisionProbe.SpatialOverlapPairs} | 最近间距 {collisionProbe.MinimumClearance:F2}"); + GUILayout.Label($"帧率限制:不锁帧,vSync={QualitySettings.vSyncCount},target={Application.targetFrameRate}"); GUILayout.Label($"日志样本:{logWriter.SampleCount} | {logWriter.LastExportPath}"); + GUILayout.Space(8); + if (GUILayout.Button("投放鱼钩:下落后惊扰争饵", GUILayout.Height(28))) + { + StartBaitScrambleDemo(); + } + + GUILayout.Space(8); + DrawRuntimeTuningControls(); + GUILayout.Space(8); DrawEnumButtons("避障方案", framework, value => { @@ -95,7 +129,7 @@ namespace FishROV.AvoidanceBenchmark DrawEnumButtons("观察视角", viewMode, value => viewMode = value); GUILayout.Space(8); - GUILayout.Label($"小鱼数量:{pendingFishCount}"); + GUILayout.Label($"鱼数量:{pendingFishCount}"); GUILayout.BeginHorizontal(); foreach (var preset in CountPresets) { @@ -137,6 +171,7 @@ namespace FishROV.AvoidanceBenchmark private void OnDestroy() { + Time.timeScale = 1f; logWriter.ExportIfNeeded(); adapter?.Dispose(); metrics.Dispose(); @@ -160,6 +195,9 @@ namespace FishROV.AvoidanceBenchmark Scenario = scenario, DimensionMode = dimensionMode, FishCount = fishCount, + FishSpeed = scenario == AvoidanceScenarioKind.BaitScramble + ? Mathf.Max(fishMaxSpeed, baitDemoMaxSpeed) + : Mathf.Max(0.1f, fishMaxSpeed), UseSchoolProxy = false }; @@ -167,12 +205,110 @@ namespace FishROV.AvoidanceBenchmark agents.Clear(); adapter = AvoidanceAdapterRegistry.Create(config.Framework); adapter.Initialize(config); + BenchmarkAgent.SetSharedConeMaterial(fishMaterial); SpawnFish(); scenarioController.Reset(config, agents); + if (scenario == AvoidanceScenarioKind.BaitScramble) + { + DropBaitAtSchoolCenter(); + } + else if (baitMarker != null) + { + baitMarker.gameObject.SetActive(false); + } + + hookDropping = false; + motionStats = CalculateMotionStats(); logWriter.StartRun(config, adapter.Name); } + private void DrawRuntimeTuningControls() + { + GUILayout.Label($"速度倍率:x{speedMultiplier:F2}"); + speedMultiplier = GUILayout.HorizontalSlider(speedMultiplier, 0.25f, 4f); + GUILayout.BeginHorizontal(); + if (GUILayout.Button("0.5x")) + { + speedMultiplier = 0.5f; + } + + if (GUILayout.Button("1x")) + { + speedMultiplier = 1f; + } + + if (GUILayout.Button("2x")) + { + speedMultiplier = 2f; + } + + if (GUILayout.Button("4x")) + { + speedMultiplier = 4f; + } + + GUILayout.EndHorizontal(); + + GUILayout.Label($"时间缩放:x{timeScale:F2}"); + var newTimeScale = GUILayout.HorizontalSlider(timeScale, 0.1f, 5f); + if (!Mathf.Approximately(newTimeScale, timeScale)) + { + timeScale = newTimeScale; + ApplyTimeScale(); + } + + GUILayout.BeginHorizontal(); + if (GUILayout.Button("0.25x")) + { + timeScale = 0.25f; + ApplyTimeScale(); + } + + if (GUILayout.Button("1x")) + { + timeScale = 1f; + ApplyTimeScale(); + } + + if (GUILayout.Button("2x")) + { + timeScale = 2f; + ApplyTimeScale(); + } + + if (GUILayout.Button("5x")) + { + timeScale = 5f; + ApplyTimeScale(); + } + + GUILayout.EndHorizontal(); + } + + private void ApplyRuntimeSpeed() + { + if (config == null) + { + return; + } + + var runtimeMaxSpeed = Mathf.Max(0.01f, config.FishSpeed * Mathf.Max(0.01f, speedMultiplier)); + for (var i = 0; i < agents.Count; i++) + { + var agent = agents[i]; + if (agent != null) + { + agent.SetMaxSpeed(runtimeMaxSpeed); + } + } + } + + private void ApplyTimeScale() + { + Time.timeScale = Mathf.Clamp(timeScale, 0.05f, 8f); + } + private void SpawnFish() { for (var i = 0; i < config.FishCount; i++) @@ -207,6 +343,17 @@ namespace FishROV.AvoidanceBenchmark return new Vector3(Mathf.Cos(angle) * radius, LayerY(index), Mathf.Sin(angle) * radius); } + if (scenario == AvoidanceScenarioKind.BaitScramble) + { + var angle = index * Mathf.PI * 2f / Mathf.Max(1, total); + var band = 0.55f + (index % 7) * 0.055f; + var jitter = Random.insideUnitCircle * radius * 0.12f; + return new Vector3( + Mathf.Cos(angle) * radius * band + jitter.x, + LayerY(index), + Mathf.Sin(angle) * radius * band + jitter.y); + } + var point = Random.insideUnitCircle * radius; return new Vector3(point.x, LayerY(index), point.y); } @@ -226,6 +373,77 @@ namespace FishROV.AvoidanceBenchmark } } + private void StartBaitScrambleDemo() + { + scenario = AvoidanceScenarioKind.BaitScramble; + StartHookDropAtSchoolCenter(); + } + + private void StartHookDropAtSchoolCenter() + { + var position = CalculateAgentCenter(); + position.y = 0f; + hookDropTarget = position; + hookDropStart = position + Vector3.up * Mathf.Max(0f, hookDropHeight); + hookDropElapsed = 0f; + hookDropping = true; + + if (baitMarker != null) + { + baitMarker.position = hookDropStart; + baitMarker.gameObject.SetActive(true); + } + } + + private void DropBaitAtSchoolCenter() + { + var position = CalculateAgentCenter(); + position.y = 0f; + DropBaitAtPosition(position); + } + + private void DropBaitAtPosition(Vector3 position) + { + if (config != null) + { + config.Scenario = AvoidanceScenarioKind.BaitScramble; + config.FishSpeed = Mathf.Max(config.FishSpeed, baitDemoMaxSpeed); + } + + scenarioController.DropBait(position); + if (baitMarker != null) + { + baitMarker.position = position; + baitMarker.gameObject.SetActive(true); + } + } + + private void UpdateHookDrop(float deltaTime) + { + if (!hookDropping) + { + return; + } + + hookDropElapsed += deltaTime; + var duration = Mathf.Max(0.01f, hookDropDuration); + var t = Mathf.Clamp01(hookDropElapsed / duration); + var eased = 1f - (1f - t) * (1f - t); + var position = Vector3.Lerp(hookDropStart, hookDropTarget, eased); + + if (baitMarker != null) + { + baitMarker.position = position; + baitMarker.gameObject.SetActive(true); + } + + if (t >= 1f) + { + hookDropping = false; + DropBaitAtPosition(hookDropTarget); + } + } + private void EnsureCameraAndLight() { benchmarkCamera = Camera.main; @@ -248,6 +466,32 @@ namespace FishROV.AvoidanceBenchmark } } + private void EnsureBaitMarker() + { + if (baitMarker != null) + { + return; + } + + var marker = GameObject.CreatePrimitive(PrimitiveType.Sphere); + marker.name = "Benchmark Bait Marker"; + marker.transform.localScale = Vector3.one * 0.55f; + var renderer = marker.GetComponent(); + if (renderer != null && fishMaterial != null) + { + renderer.sharedMaterial = fishMaterial; + } + + var collider = marker.GetComponent(); + if (collider != null) + { + collider.enabled = false; + } + + baitMarker = marker.transform; + marker.SetActive(false); + } + private void UpdateCamera() { if (benchmarkCamera == null || config == null) @@ -300,6 +544,40 @@ namespace FishROV.AvoidanceBenchmark return sum / agents.Count; } + private MotionStats CalculateMotionStats() + { + if (agents.Count == 0) + { + return default; + } + + var speedSum = 0f; + var accelerationSum = 0f; + var maxSpeedValue = 0f; + var maxAccelerationValue = 0f; + for (var i = 0; i < agents.Count; i++) + { + var agent = agents[i]; + if (agent == null) + { + continue; + } + + speedSum += agent.CurrentSpeed; + accelerationSum += agent.CurrentAcceleration; + maxSpeedValue = Mathf.Max(maxSpeedValue, agent.PeakSpeed); + maxAccelerationValue = Mathf.Max(maxAccelerationValue, agent.PeakAcceleration); + } + + return new MotionStats + { + AverageSpeed = speedSum / agents.Count, + MaxSpeed = maxSpeedValue, + AverageAcceleration = accelerationSum / agents.Count, + MaxAcceleration = maxAccelerationValue + }; + } + private static void DrawEnumButtons(string label, T current, System.Action onChanged) where T : System.Enum { GUILayout.Label(label); @@ -325,8 +603,10 @@ namespace FishROV.AvoidanceBenchmark return "无避障"; case AvoidanceFrameworkKind.Rvo2: return "RVO2"; - case AvoidanceFrameworkKind.SamplingLocal: - return "自研版"; + case AvoidanceFrameworkKind.AstarProRvo: + return "A* Pro RVO"; + case AvoidanceFrameworkKind.AstarProRvoVelocityLike: + return "A* Pro RVO Velocity"; case AvoidanceScenarioKind.FreeSwim: return "自由巡游"; case AvoidanceScenarioKind.CrossFlow: @@ -335,6 +615,8 @@ namespace FishROV.AvoidanceBenchmark return "狭窄通道"; case AvoidanceScenarioKind.DenseCircleStress: return "圆阵高压"; + case AvoidanceScenarioKind.BaitScramble: + return "鱼钩争饵"; case AvoidanceDimensionMode.XZ2D: return "2D/XZ"; case AvoidanceDimensionMode.XZ25D: @@ -348,10 +630,18 @@ namespace FishROV.AvoidanceBenchmark case BenchmarkViewMode.Side: return "侧视"; case BenchmarkViewMode.FollowSchool: - return "跟鱼群"; + return "跟随鱼群"; default: return value.ToString(); } } + + public struct MotionStats + { + public float AverageSpeed; + public float MaxSpeed; + public float AverageAcceleration; + public float MaxAcceleration; + } } } diff --git a/FishROV/Assets/Scripts/Benchmark/FrameworkAdapters/SamplingLocalAvoidanceAdapter.cs b/FishROV/Assets/Scripts/Benchmark/FrameworkAdapters/SamplingLocalAvoidanceAdapter.cs deleted file mode 100644 index a393259..0000000 --- a/FishROV/Assets/Scripts/Benchmark/FrameworkAdapters/SamplingLocalAvoidanceAdapter.cs +++ /dev/null @@ -1,326 +0,0 @@ -using System.Collections.Generic; -using UnityEngine; - -namespace FishROV.AvoidanceBenchmark -{ - public sealed class SamplingLocalAvoidanceAdapter : IAvoidanceAdapter - { - private const float TimeHorizon = 1.25f; - private const float NeighbourRangeMultiplier = 5.5f; - private const float GoalWeight = 0.85f; - private const float SmoothWeight = 0.12f; - private const float CollisionWeight = 18f; - private const float CurrentOverlapWeight = 80f; - private const float BoundaryWeight = 12f; - private const int DirectionSamples = 16; - - private readonly Dictionary states = - new Dictionary(512); - - private readonly List orderedStates = new List(512); - private readonly Dictionary> buckets = - new Dictionary>(1024); - - private AvoidanceBenchmarkConfig config; - private float cellSize; - - public string Name => "采样避障(学习版)"; - public bool IsAvailable => true; - public string Status => "学习版:空间哈希找邻居,采样候选速度,用预测碰撞/平滑/目标偏差/边界惩罚打分。"; - - public void Initialize(AvoidanceBenchmarkConfig config) - { - this.config = config.Clone(); - states.Clear(); - orderedStates.Clear(); - buckets.Clear(); - cellSize = config.FishRadius * NeighbourRangeMultiplier; - } - - public BenchmarkAgent CreateAgent(BenchmarkAgentSpawnInfo spawnInfo, Transform parent) - { - var agent = BenchmarkAgent.CreatePrimitive(spawnInfo, parent); - var state = new AgentState(agent); - states[agent] = state; - orderedStates.Add(state); - return agent; - } - - public void SetPreferredVelocity(BenchmarkAgent agent, Vector3 preferredVelocity) - { - if (!states.TryGetValue(agent, out var state)) - { - agent.ApplyVelocity(preferredVelocity); - return; - } - - state.PreferredVelocity = ClampForMode(preferredVelocity, agent.MaxSpeed); - } - - public void SetTarget(BenchmarkAgent agent, Vector3 target) - { - agent.Target = target; - } - - public void Tick(float deltaTime) - { - if (deltaTime <= 0f) - { - return; - } - - BuildBuckets(); - - for (var i = 0; i < orderedStates.Count; i++) - { - var state = orderedStates[i]; - if (state.Agent == null) - { - continue; - } - - var velocity = ChooseVelocity(state, deltaTime); - state.Agent.ApplyVelocity(velocity); - state.LastVelocity = velocity; - } - } - - public void Dispose() - { - states.Clear(); - orderedStates.Clear(); - buckets.Clear(); - } - - private Vector3 ChooseVelocity(AgentState state, float deltaTime) - { - var preferred = state.PreferredVelocity; - var bestVelocity = preferred; - var bestScore = EvaluateVelocity(state, preferred, deltaTime); - - TestCandidate(state, Vector3.zero, deltaTime, ref bestVelocity, ref bestScore); - - var flatPreferred = Flatten(preferred); - var baseDirection = flatPreferred.sqrMagnitude > 0.0001f - ? flatPreferred.normalized - : ForwardFromAgent(state.Agent); - - var maxSpeed = state.Agent.MaxSpeed; - for (var i = 0; i < DirectionSamples; i++) - { - var angle = i * 360f / DirectionSamples; - var direction = Quaternion.AngleAxis(angle, Vector3.up) * baseDirection; - TestCandidate(state, RestoreVertical(direction * maxSpeed, preferred), deltaTime, ref bestVelocity, ref bestScore); - TestCandidate(state, RestoreVertical(direction * maxSpeed * 0.55f, preferred), deltaTime, ref bestVelocity, ref bestScore); - } - - return ClampForMode(bestVelocity, maxSpeed); - } - - private void TestCandidate( - AgentState state, - Vector3 candidate, - float deltaTime, - ref Vector3 bestVelocity, - ref float bestScore) - { - var score = EvaluateVelocity(state, candidate, deltaTime); - if (score < bestScore) - { - bestScore = score; - bestVelocity = candidate; - } - } - - private float EvaluateVelocity(AgentState state, Vector3 candidate, float deltaTime) - { - candidate = ClampForMode(candidate, state.Agent.MaxSpeed); - var preferred = state.PreferredVelocity; - var score = GoalWeight * (candidate - preferred).sqrMagnitude; - score += SmoothWeight * (candidate - state.LastVelocity).sqrMagnitude; - score += BoundaryPenalty(state.Agent.transform.position, candidate, deltaTime); - - var position = state.Agent.transform.position; - var centerCell = ToCell(position); - var queryRange = Mathf.CeilToInt( - state.Agent.Radius * NeighbourRangeMultiplier / cellSize); - queryRange = Mathf.Clamp(queryRange, 1, 4); - - for (var x = -queryRange; x <= queryRange; x++) - { - for (var z = -queryRange; z <= queryRange; z++) - { - var cell = new Vector2Int(centerCell.x + x, centerCell.y + z); - if (!buckets.TryGetValue(cell, out var bucket)) - { - continue; - } - - for (var i = 0; i < bucket.Count; i++) - { - var other = bucket[i]; - if (other == state || other.Agent == null) - { - continue; - } - - score += CollisionPenalty(state, other, candidate); - } - } - } - - return score; - } - - private float CollisionPenalty(AgentState state, AgentState other, Vector3 candidate) - { - var position = state.Agent.transform.position; - var otherPosition = other.Agent.transform.position; - var relativePosition = otherPosition - position; - var combinedRadius = state.Agent.Radius + other.Agent.Radius; - var neighbourRange = Mathf.Max(combinedRadius * 2f, state.Agent.Radius * NeighbourRangeMultiplier); - - if (config.DimensionMode != AvoidanceDimensionMode.XYZ3D) - { - relativePosition.y = 0f; - } - - var distanceSq = relativePosition.sqrMagnitude; - if (distanceSq > neighbourRange * neighbourRange) - { - return 0f; - } - - var relativeVelocity = candidate - other.LastVelocity; - if (config.DimensionMode != AvoidanceDimensionMode.XYZ3D) - { - relativeVelocity.y = 0f; - } - - var clearanceRadius = combinedRadius * 1.08f; - var currentDistance = Mathf.Sqrt(Mathf.Max(0.0001f, distanceSq)); - var currentClearance = currentDistance - clearanceRadius; - var penalty = 0f; - - if (currentClearance < 0f) - { - var overlap = -currentClearance / clearanceRadius; - penalty += CurrentOverlapWeight * overlap * overlap; - } - - var relativeSpeedSq = relativeVelocity.sqrMagnitude; - if (relativeSpeedSq < 0.0001f) - { - return penalty; - } - - var closestTime = Mathf.Clamp( - -Vector3.Dot(relativePosition, relativeVelocity) / relativeSpeedSq, - 0f, - TimeHorizon); - var closest = relativePosition + relativeVelocity * closestTime; - var closestDistance = closest.magnitude; - var predictedClearance = closestDistance - clearanceRadius; - if (predictedClearance < 0f) - { - var risk = -predictedClearance / clearanceRadius; - var urgency = 1f + (TimeHorizon - closestTime) / TimeHorizon; - penalty += CollisionWeight * risk * risk * urgency; - } - - return penalty; - } - - private float BoundaryPenalty(Vector3 position, Vector3 velocity, float deltaTime) - { - var next = position + velocity * deltaTime * TimeHorizon; - var flat = new Vector2(next.x, next.z); - var softRadius = config.ArenaRadius * 0.92f; - if (flat.magnitude <= softRadius) - { - return 0f; - } - - var overflow = (flat.magnitude - softRadius) / Mathf.Max(0.001f, config.ArenaRadius - softRadius); - return BoundaryWeight * overflow * overflow; - } - - private void BuildBuckets() - { - buckets.Clear(); - for (var i = 0; i < orderedStates.Count; i++) - { - var state = orderedStates[i]; - if (state.Agent == null) - { - continue; - } - - var cell = ToCell(state.Agent.transform.position); - if (!buckets.TryGetValue(cell, out var bucket)) - { - bucket = new List(8); - buckets[cell] = bucket; - } - - bucket.Add(state); - } - } - - private Vector2Int ToCell(Vector3 position) - { - return new Vector2Int( - Mathf.FloorToInt(position.x / cellSize), - Mathf.FloorToInt(position.z / cellSize)); - } - - private Vector3 ClampForMode(Vector3 velocity, float maxSpeed) - { - if (config.DimensionMode != AvoidanceDimensionMode.XYZ3D) - { - velocity.y = 0f; - } - - return Vector3.ClampMagnitude(velocity, maxSpeed); - } - - private Vector3 RestoreVertical(Vector3 flatVelocity, Vector3 preferred) - { - if (config.DimensionMode == AvoidanceDimensionMode.XYZ3D) - { - flatVelocity.y = preferred.y; - } - else - { - flatVelocity.y = 0f; - } - - return flatVelocity; - } - - private static Vector3 Flatten(Vector3 value) - { - value.y = 0f; - return value; - } - - private static Vector3 ForwardFromAgent(BenchmarkAgent agent) - { - var forward = agent != null ? agent.transform.forward : Vector3.forward; - forward.y = 0f; - return forward.sqrMagnitude > 0.0001f ? forward.normalized : Vector3.forward; - } - - private sealed class AgentState - { - public AgentState(BenchmarkAgent agent) - { - Agent = agent; - } - - public BenchmarkAgent Agent { get; } - public Vector3 PreferredVelocity { get; set; } - public Vector3 LastVelocity { get; set; } - } - } -} diff --git a/FishROV/Assets/Scripts/Benchmark/FrameworkAdapters/SamplingLocalAvoidanceAdapter.cs.meta b/FishROV/Assets/Scripts/Benchmark/FrameworkAdapters/SamplingLocalAvoidanceAdapter.cs.meta deleted file mode 100644 index e9a077b..0000000 --- a/FishROV/Assets/Scripts/Benchmark/FrameworkAdapters/SamplingLocalAvoidanceAdapter.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 41e3ccd900cb4a37bf60c67fa49c906c -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/FishROV/Assets/Scripts/Benchmark/Scenarios/AvoidanceScenarioController.cs b/FishROV/Assets/Scripts/Benchmark/Scenarios/AvoidanceScenarioController.cs index f6e2e96..998cba1 100644 --- a/FishROV/Assets/Scripts/Benchmark/Scenarios/AvoidanceScenarioController.cs +++ b/FishROV/Assets/Scripts/Benchmark/Scenarios/AvoidanceScenarioController.cs @@ -8,11 +8,15 @@ namespace FishROV.AvoidanceBenchmark private readonly Dictionary targets = new Dictionary(); private AvoidanceBenchmarkConfig config; private float elapsed; + private float baitDropTime; + private Vector3 baitPosition; public void Reset(AvoidanceBenchmarkConfig benchmarkConfig, IReadOnlyList agents) { config = benchmarkConfig.Clone(); elapsed = 0f; + baitDropTime = 0f; + baitPosition = Vector3.zero; targets.Clear(); foreach (var agent in agents) @@ -21,16 +25,34 @@ namespace FishROV.AvoidanceBenchmark } } - public Vector3 GetPreferredVelocity(BenchmarkAgent agent, float deltaTime) + public void Tick(float deltaTime) { elapsed += deltaTime; + } + public void DropBait(Vector3 position) + { + baitPosition = position; + baitDropTime = elapsed; + if (config != null) + { + config.Scenario = AvoidanceScenarioKind.BaitScramble; + } + } + + public Vector3 GetPreferredVelocity(BenchmarkAgent agent, float deltaTime) + { if (!targets.TryGetValue(agent, out var target)) { target = CreateTarget(agent); targets[agent] = target; } + if (config.Scenario == AvoidanceScenarioKind.BaitScramble) + { + return GetBaitScrambleVelocity(agent); + } + if ((target - agent.transform.position).sqrMagnitude < 4f) { target = CreateTarget(agent); @@ -57,6 +79,8 @@ namespace FishROV.AvoidanceBenchmark switch (config.Scenario) { + case AvoidanceScenarioKind.BaitScramble: + return baitPosition; case AvoidanceScenarioKind.CrossFlow: return agent.GroupId % 2 == 0 ? new Vector3(radius, HeightFor(agent), Random.Range(-radius, radius)) @@ -87,5 +111,47 @@ namespace FishROV.AvoidanceBenchmark return Random.Range(-4f, 4f); } + + private Vector3 GetBaitScrambleVelocity(BenchmarkAgent agent) + { + var position = agent.transform.position; + var flatOffset = new Vector3(position.x - baitPosition.x, 0f, position.z - baitPosition.z); + if (flatOffset.sqrMagnitude < 0.0001f) + { + flatOffset = Quaternion.Euler(0f, agent.GroupId * 137.5f, 0f) * Vector3.forward; + } + + var outward = flatOffset.normalized; + var tangentSign = agent.GroupId % 2 == 0 ? 1f : -1f; + var tangent = new Vector3(-outward.z, 0f, outward.x) * tangentSign; + var distance = flatOffset.magnitude; + var age = elapsed - baitDropTime; + + Vector3 desired; + if (age < 1.2f) + { + desired = outward * agent.MaxSpeed * 1.15f + tangent * agent.MaxSpeed * 0.25f; + } + else + { + var ringRadius = Mathf.Lerp(5.5f, 2.2f, Mathf.Clamp01((age - 1.2f) / 7.5f)); + var radialError = distance - ringRadius; + var radial = -outward * Mathf.Clamp(radialError * 0.55f, -0.75f, 1f); + var swirl = tangent * Mathf.Lerp(0.35f, 0.95f, Mathf.Clamp01((age - 1.2f) / 5f)); + var baitPull = -outward * Mathf.Lerp(0.25f, 0.55f, Mathf.Clamp01((age - 1.2f) / 4f)); + desired = (radial + swirl + baitPull).normalized * agent.MaxSpeed; + } + + if (config.DimensionMode == AvoidanceDimensionMode.XYZ3D) + { + desired.y = Mathf.Clamp(baitPosition.y - position.y, -1f, 1f) * agent.MaxSpeed * 0.35f; + } + else + { + desired.y = 0f; + } + + return desired; + } } } diff --git a/FishROV/Docs/avoidance-benchmark-report-20260613.html b/FishROV/Docs/avoidance-benchmark-report-20260613.html new file mode 100644 index 0000000..0ba00a1 --- /dev/null +++ b/FishROV/Docs/avoidance-benchmark-report-20260613.html @@ -0,0 +1,637 @@ + + + + + + + FishROV RVO2 本地避障 Benchmark 报告 + + + +
+ + +
+
+

2026-06-13 / AvoidanceBenchmarkLogs

+

RVO2 本地避障结果:按唯一变量拆开看

+

这版报告不再把编辑器和百元机混成一张表。先在百元机内部比较算法、数量、维度;再在编辑器内部看同样趋势;最后才做同配置的跨环境差异。

+
+
移动端主依据百元机 / 720x1600
+
趋势参考编辑器 / 750x617
+
推荐默认档300 鱼 + XYZ3D
+
压测红线500 鱼开始警戒
+
+
+ +
+
+
+

Executive Summary

+

结论摘要

+
+

先给结论,再看证据。所有跨表比较都避免同时改变多个核心变量。

+
+
+
300百元机可用档。RVO2 / FreeSwim / XZ25D 第二次运行达到 30.1 FPS,空间重叠均值 1.1。
+
500百元机压测线。XYZ3D 在 FreeSwim 仍有 26.2 FPS,但 1% Low 已降到 15.8。
+
XYZ3D同场景同数量下空间重叠最低,Dense/CrossFlow 1000 都明显优于 XZ2D/XZ25D。
+
6-14x同配置下,编辑器 FPS 约为百元机 6 倍左右;百元机 simulation ms 约为编辑器 10-14 倍。
+
+
+ 推荐口径:玩法默认目标按百元机结果定,不按编辑器定。编辑器只用于观察算法随数量和维度变化的趋势。 +
+
+ +
+
+
+

Phone Only / Controlled Variables

+

百元机内对比

+
+

同一设备环境内比较,避免把硬件差异混进算法结论。

+
+ +
+
+

算法变量:无避障 vs RVO2

+
固定:百元机 / FreeSwim / XZ25D / 300 鱼。唯一变量:避障策略。
+ + + + + + + +
策略FPS1% Low空间重叠最小间距判断
无避障7.00.580.9-0.674不可用
RVO2 第一次26.414.41.6-0.298有尖峰
RVO2 第二次30.123.61.1-0.017可用
+

RVO2 不是只减少重叠,也显著改善了百元机上的可运行性。第一次与第二次 RVO2 结果不同,后续建议保留重复运行取中位数。

+
+ +
+

数量变量:FreeSwim / XYZ3D

+
固定:百元机 / RVO2 / FreeSwim / XYZ3D。唯一变量:鱼数量。
+
+
100 鱼
30.2 FPS
+
500 鱼
26.2 FPS
+
800 鱼
11.8 FPS
+
1500 鱼
8.9 FPS
+
+ + + + + + + + +
数量FPS1% LowSim msP95 ms空间重叠
10030.228.64.437.820.0
50026.215.822.7037.080.7
80011.85.254.07104.847.7
15008.93.1103.64189.0184.2
+
+
+ +
+
+

维度变量:Dense 1000

+
固定:百元机 / RVO2 / DenseCircleStress / 1000 鱼。唯一变量:维度。
+ + + + + + + +
维度FPSSim msP95 ms空间重叠判断
XYZ3D14.255.33103.7887.1质量最好
XZ25D12.169.90144.30474.4偏高
XZ2D11.870.63131.341190.3最差
+
+ +
+

维度变量:CrossFlow 1000

+
固定:百元机 / RVO2 / CrossFlow / 1000 鱼。唯一变量:维度。
+ + + + + + + +
维度FPSSim msP95 ms空间重叠判断
XYZ3D13.160.92117.8138.5质量最好
XZ25D11.977.62202.03384.4尾延迟高
XZ2D12.260.66118.03714.1重叠高
+
+
+
+ +
+
+
+

Editor Only / Controlled Variables

+

编辑器内对比

+
+

编辑器不用于移动端性能结论,但可以看数量增长和维度变化的趋势是否一致。

+
+ +
+
+

数量变量:FreeSwim / XYZ3D

+
固定:编辑器 / RVO2 / FreeSwim / XYZ3D。唯一变量:鱼数量。
+ + + + + + + + + + +
数量FPS1% LowSim msP95 ms空间重叠
100169.493.40.620.840.0
300134.072.71.532.170.0
500129.767.82.332.410.2
100071.533.04.734.985.1
200040.517.712.1825.9755.3
500018.25.736.2286.95774.7
+
+ +
+

维度变量:Dense 1000 / 2000

+
固定:编辑器 / RVO2 / DenseCircleStress / 同数量。唯一变量:维度。
+ + + + + + + + + + +
数量维度FPSSim msP95 ms空间重叠
1000XYZ3D85.25.388.2129.5
1000XZ25D83.84.975.43264.6
1000XZ2D79.85.417.25851.9
2000XYZ3D55.111.1426.22154.2
2000XZ25D50.212.1726.58765.8
2000XZ2D48.710.3411.582093.6
+
+
+ +
+ 编辑器趋势与百元机一致:XYZ3D 的空间重叠始终最低;XZ2D 在 Dense 场景中最容易堆叠。不同的是,编辑器性能余量会掩盖百元机上的真实性能红线。 +
+
+ +
+
+
+

Cross Environment / Same Config

+

编辑器 vs 百元机

+
+

同配置跨环境对比只能说明设备差异,不用于证明算法优劣。

+
+ +
+
固定:RVO2 / 同场景 / 同维度 / 同数量。唯一变量:运行环境。
+ + + + + + + + + + + + + + + + + + + + +
配置编辑器 FPS百元机 FPSFPS 倍率编辑器 Sim ms百元机 Sim msSim 倍率空间重叠:编辑器 / 百元机
FreeSwim / XYZ3D / 100169.430.25.6x0.624.437.1x0.0 / 0.0
FreeSwim / XYZ3D / 500129.726.25.0x2.3322.709.7x0.2 / 0.7
Dense / XYZ3D / 100085.214.26.0x5.3855.3310.3x29.5 / 87.1
Dense / XZ25D / 100083.812.16.9x4.9769.9014.1x264.6 / 474.4
Dense / XZ2D / 100079.811.86.8x5.4170.6313.1x851.9 / 1190.3
+
+ +
+ 读法:跨环境对比的唯一变量是“运行环境”,因此它只回答“编辑器结果离百元机有多远”。它不能替代百元机内的算法、数量、维度对比。 +
+
+ +
+
+
+

Method

+

控制变量口径

+
+
+ +
+
+

百元机内对比

+

环境固定百元机

+

用于回答移动端真实可用性:算法是否有效、数量红线在哪里、哪个维度质量最好。

+
+
+

编辑器内对比

+

环境固定编辑器

+

用于观察趋势,不直接判断移动端上线规模。

+
+
+

跨环境对比

+

配置固定环境变化

+

用于量化编辑器到百元机的性能折损,避免把桌面余量误当移动端余量。

+
+
+
+ +
+
+
+

Next Actions

+

建议动作

+
+
+
+
+

玩法默认档

+

按百元机结果,把默认目标定为 300 鱼 + XYZ3D。500 鱼作为压测档,不作为默认玩法规模。

+
+
+

优化方向

+

500+ 鱼需要分级避障:远场降频或关闭 RVO,只对近场、玩家交互区和高风险鱼群启用。

+
+
+

后续 Benchmark

+

补齐设备、构建类型、VSync、targetFrameRate、Burst/Jobs 状态,并对同配置至少跑 3 次取中位数。

+
+
+

数据源:编辑器批次来自 C:\Users\13999\AppData\LocalLow\DefaultCompany\FishROV\AvoidanceBenchmarkLogs;百元机批次来自 C:\Users\13999\AppData\Local\Temp。

+
+
+
+ + diff --git a/FishROV/Docs/avoidance-benchmark-report-20260615-android2.html b/FishROV/Docs/avoidance-benchmark-report-20260615-android2.html new file mode 100644 index 0000000..3483453 --- /dev/null +++ b/FishROV/Docs/avoidance-benchmark-report-20260615-android2.html @@ -0,0 +1,153 @@ + + + + + +FishROV Android 2 避障复测报告 + + + +
+ +
+

2026-06-15 / 百元机 / XZ2D

A Pro VelocityLike 复测报告

本页分析 C:/Users/13999/Desktop/logs/android_2 下 27 个 CSV。统计时排除了 timeSeconds=0 的启动样本,避免初始化尖峰污染稳态数据。

CSV27
场景3
鱼数范围300-1000
屏幕720x1600
+

结论

  • 300 鱼鱼饵争抢是最干净的样本:VelocityLike 平均 FPS 29.9,比 RVO2 高 +3.2%,比旧 A Pro 高 +2.1%;最小间距从旧 A Pro -0.625 改到 -0.141,重叠深度明显收敛。
  • 300 鱼圆阵高压中,A Pro 两种接法 FPS 接近满帧,但平均重叠约 337.5/340.0,仍高于 RVO2 的 159.0;这更像算法/场景拥挤上限,不只是远目标语义问题。
  • 500 鱼鱼饵争抢三组速度一致但速度倍率已到 maxSpeed 16.8:VelocityLike FPS 27.7,略好于 RVO2/旧 A Pro;重叠与旧 A Pro 基本同级,说明高速度争抢下输入语义已不是主因。
  • CrossFlow 500 不能直接判 VelocityLike:VelocityLike 的 maxSpeed 是 9.6,RVO2/旧 A Pro 是 2.4/2.4,速度变量被改变。
+

样本质量

可以优先采信
  • 鱼饵争抢 / 300 鱼
  • 鱼饵争抢 / 500 鱼
  • 鱼饵争抢 / 800 鱼
  • 圆阵高压 / 1000 鱼
需要谨慎
  • 鱼饵争抢 / 1000 鱼:三后端齐全 不满足
  • 对向穿流 / 300 鱼:三后端齐全 不满足
  • 对向穿流 / 500 鱼:速度一致 不满足
  • 圆阵高压 / 300 鱼:时长充足 不满足
  • 圆阵高压 / 500 鱼:时长充足 不满足
  • 圆阵高压 / 800 鱼:时长充足 不满足
+

控制变量对比

+

鱼饵争抢 / 1000 鱼

后端不齐速度一致时长充足
+
+
Velocity vs RVO2 FPS
+
Velocity vs RVO2 重叠
+
Velocity vs 旧 A Pro FPS+2.2%
+
Velocity vs 旧 A Pro 重叠-3.8%
+
+
后端FPSSim ms重叠最小间距MaxSpeed时长
A Pro RVO13.453.31240.8-0.69916.813.6s
A Pro VelocityLike13.755.01193.7-0.69716.820.5s
+
+
+

鱼饵争抢 / 300 鱼

三后端齐全速度一致时长充足
+
+
Velocity vs RVO2 FPS+3.2%
+
Velocity vs RVO2 重叠+9.9%
+
Velocity vs 旧 A Pro FPS+2.1%
+
Velocity vs 旧 A Pro 重叠-3.2%
+
+
后端FPSSim ms重叠最小间距MaxSpeed时长
RVO229.016.8125.3-0.6244.225.0s
A Pro RVO29.313.9142.1-0.6254.220.5s
A Pro VelocityLike29.913.2137.7-0.1414.221.0s
+
+
+

鱼饵争抢 / 500 鱼

三后端齐全速度一致时长充足
+
+
Velocity vs RVO2 FPS+3.2%
+
Velocity vs RVO2 重叠+93.8%
+
Velocity vs 旧 A Pro FPS+2.9%
+
Velocity vs 旧 A Pro 重叠-1.0%
+
+
后端FPSSim ms重叠最小间距MaxSpeed时长
RVO226.827.3277.1-0.68216.88.0s
A Pro RVO26.924.6542.2-0.68416.813.5s
A Pro VelocityLike27.723.6536.9-0.67816.827.5s
+
+
+

鱼饵争抢 / 800 鱼

三后端齐全速度一致时长充足
+
+
Velocity vs RVO2 FPS+22.8%
+
Velocity vs RVO2 重叠+67.6%
+
Velocity vs 旧 A Pro FPS+11.6%
+
Velocity vs 旧 A Pro 重叠+3.2%
+
+
后端FPSSim ms重叠最小间距MaxSpeed时长
RVO216.847.5640.1-0.69416.810.5s
A Pro RVO18.539.91040.4-0.69716.815.0s
A Pro VelocityLike20.639.91073.2-0.69416.817.5s
+
+
+

对向穿流 / 300 鱼

后端不齐速度一致时长充足
+
+
Velocity vs RVO2 FPS
+
Velocity vs RVO2 重叠
+
Velocity vs 旧 A Pro FPS
+
Velocity vs 旧 A Pro 重叠
+
+
后端FPSSim ms重叠最小间距MaxSpeed时长
RVO230.016.182.2-0.2682.49.5s
+
+
+

对向穿流 / 500 鱼

三后端齐全速度不一致时长充足
+
+
Velocity vs RVO2 FPS+7.1%
+
Velocity vs RVO2 重叠+58.4%
+
Velocity vs 旧 A Pro FPS+0.8%
+
Velocity vs 旧 A Pro 重叠+22.9%
+
+
后端FPSSim ms重叠最小间距MaxSpeed时长
RVO226.426.8171.2-0.3982.420.5s
A Pro RVO28.122.1220.8-0.4592.428.0s
A Pro VelocityLike28.323.4271.3-0.6189.625.0s
+
+
+

圆阵高压 / 1000 鱼

三后端齐全速度一致时长充足
+
+
Velocity vs RVO2 FPS+5.5%
+
Velocity vs RVO2 重叠+30.1%
+
Velocity vs 旧 A Pro FPS+1.6%
+
Velocity vs 旧 A Pro 重叠-0.4%
+
+
后端FPSSim ms重叠最小间距MaxSpeed时长
RVO213.456.71002.1-0.6889.69.5s
A Pro RVO13.956.11308.8-0.6979.625.1s
A Pro VelocityLike14.153.41304.0-0.6929.622.5s
+
+
+

圆阵高压 / 300 鱼

三后端齐全速度一致时长偏短
+
+
Velocity vs RVO2 FPS-0.7%
+
Velocity vs RVO2 重叠+113.8%
+
Velocity vs 旧 A Pro FPS-0.5%
+
Velocity vs 旧 A Pro 重叠+0.7%
+
+
后端FPSSim ms重叠最小间距MaxSpeed时长
RVO230.116.7159.0-0.3792.414.5s
A Pro RVO30.012.5337.5-0.3832.48.0s
A Pro VelocityLike29.912.9340.0-0.3822.47.5s
+
+
+

圆阵高压 / 500 鱼

三后端齐全速度一致时长偏短
+
+
Velocity vs RVO2 FPS+3.6%
+
Velocity vs RVO2 重叠+4.2%
+
Velocity vs 旧 A Pro FPS+1.4%
+
Velocity vs 旧 A Pro 重叠-16.7%
+
+
后端FPSSim ms重叠最小间距MaxSpeed时长
RVO228.322.3600.0-0.4169.62.5s
A Pro RVO28.919.1750.0-0.4169.61.0s
A Pro VelocityLike29.322.8625.0-0.4169.62.0s
+
+
+

圆阵高压 / 800 鱼

三后端齐全速度一致时长偏短
+
+
Velocity vs RVO2 FPS+17.6%
+
Velocity vs RVO2 重叠+327.0%
+
Velocity vs 旧 A Pro FPS-4.3%
+
Velocity vs 旧 A Pro 重叠+0.0%
+
+
后端FPSSim ms重叠最小间距MaxSpeed时长
RVO215.177.9562.1-0.6979.69.5s
A Pro RVO18.639.82400.0-0.5119.62.5s
A Pro VelocityLike17.835.82400.0-0.5219.64.5s
+
+

完整汇总表

+ + + + + + + + + + + + + + + + + + + + + + + + + +
场景鱼数后端MaxSpeed样本时长FPS1% LowSim ms重叠最小间距均速均加速
鱼饵争抢300RVO24.25025.0s29.017.616.8125.3-0.6241.94.0
鱼饵争抢300A Pro RVO4.24120.5s29.313.913.9142.1-0.6251.83.9
鱼饵争抢300A Pro VelocityLike4.24221.0s29.920.513.2137.7-0.1411.83.4
鱼饵争抢500RVO216.8168.0s26.88.527.3277.1-0.68210.554.1
鱼饵争抢500A Pro RVO16.82713.5s26.911.324.6542.2-0.6845.323.0
鱼饵争抢500A Pro VelocityLike16.85527.5s27.713.723.6536.9-0.6785.121.2
鱼饵争抢800RVO216.82110.5s16.86.147.5640.1-0.6949.435.3
鱼饵争抢800A Pro RVO16.83015.0s18.55.839.91040.4-0.6975.626.7
鱼饵争抢800A Pro VelocityLike16.83517.5s20.66.539.91073.2-0.6945.424.6
鱼饵争抢1000A Pro RVO16.82713.6s13.44.753.31240.8-0.6996.026.9
鱼饵争抢1000A Pro VelocityLike16.84120.5s13.75.455.01193.7-0.6976.026.9
对向穿流300RVO22.4199.5s30.021.216.182.2-0.2681.81.9
对向穿流500RVO22.44120.5s26.411.826.8171.2-0.3981.51.3
对向穿流500A Pro RVO2.45628.0s28.114.222.1220.8-0.4591.41.1
对向穿流500A Pro VelocityLike9.65025.0s28.314.123.4271.3-0.6182.45.7
圆阵高压300RVO22.42914.5s30.124.116.7159.0-0.3791.51.7
圆阵高压300A Pro RVO2.4168.0s30.020.712.5337.5-0.3831.60.0
圆阵高压300A Pro VelocityLike2.4157.5s29.916.912.9340.0-0.3821.80.0
圆阵高压500RVO29.652.5s28.37.522.3600.0-0.4169.60.0
圆阵高压500A Pro RVO9.621.0s28.99.519.1750.0-0.4164.80.0
圆阵高压500A Pro VelocityLike9.642.0s29.312.922.8625.0-0.4162.40.0
圆阵高压800RVO29.6199.5s15.14.477.9562.1-0.6976.913.8
圆阵高压800A Pro RVO9.652.5s18.65.839.82400.0-0.5111.90.1
圆阵高压800A Pro VelocityLike9.694.5s17.86.735.82400.0-0.5211.10.0
圆阵高压1000RVO29.6199.5s13.43.256.71002.1-0.6887.211.5
圆阵高压1000A Pro RVO9.65025.1s13.95.556.11308.8-0.6972.87.6
圆阵高压1000A Pro VelocityLike9.64522.5s14.14.653.41304.0-0.6922.98.2
+

下一轮测试建议

  • 固定速度倍率为 1x 后重新跑 CrossFlow 500,因为这一组 VelocityLike 的 MaxSpeed 与另外两个后端不一致。
  • Dense 500 / Dense 800 每个后端至少跑到 20 秒;当前几组只有 1-5 秒,重叠和 1% Low 都容易被启动阶段支配。
  • BaitScramble 建议保留 300、500、800、1000 四档,但同一档必须连续跑完三后端,不要中途改速度倍率。
  • 如果目标是判断 A Pro 能不能用,首要看 VelocityLike 在 300/500 鱼下是否同时满足 FPS 更高、最小间距不恶化、重叠不显著高于 RVO2。
+
+
+ + \ No newline at end of file diff --git a/FishROV/Docs/avoidance-benchmark-report-20260615.html b/FishROV/Docs/avoidance-benchmark-report-20260615.html new file mode 100644 index 0000000..9884a94 --- /dev/null +++ b/FishROV/Docs/avoidance-benchmark-report-20260615.html @@ -0,0 +1,562 @@ + + + + + + + FishROV A Pro RVO vs RVO2 Benchmark 报告 + + + +
+ + +
+
+

2026-06-15 / Android + Editor / XZ2D mainline

+

A Pro RVO 提升性能,但在 2D 下常用重叠换速度

+

这批新日志固定了大部分 2D 主线,新增了 BaitScramble 鱼饵场景。结论不是“A Pro 全面替代 RVO2”:它在百元机 500 鱼附近明显改善 FPS 和 simulation ms,但鱼饵、FreeSwim、Dense 的空间重叠通常更高。

+
+
日志数量69 个 CSV
+
主环境Android 百元机
+
主维度XZ2D
+
新增场景BaitScramble
+
+
+ +
+
+
+

Executive Summary

+

结论摘要

+
+

这次应该把“帧率胜利”和“避障质量胜利”分开看。

+
+
+
500A Pro 的价值最明显:百元机 500 鱼在 FreeSwim / CrossFlow / Dense / Bait 都显著提升 FPS。
+
+306鱼饵 500 鱼 A Pro 比 RVO2 多 306.3 个平均空间重叠对,性能提升伴随拥挤代价。
+
Cross 500百元机 CrossFlow 500 是 A Pro 最像“真胜利”的样本:FPS 28.8 vs 11.4,空间重叠 288.7 vs 512.0。
+
800+A Pro 到 800/1000 后重叠经常爆高,不能直接作为高数量默认方案。
+
+
+ 推荐判断:A Pro 可以继续作为性能候选,但现在还不建议无条件替代 RVO2。鱼饵场景要先解决聚集重叠或增加后处理分离,再谈默认启用。 +
+
+ +
+
+
+

Android / Same Config

+

百元机:A Pro vs RVO2

+
+

固定环境、场景、维度和数量,只比较模式。负数 Sim Δ 表示 A Pro 更快;正数重叠 Δ 表示 A Pro 更拥挤。

+
+ +
+
固定:Android / XZ2D / 同场景 / 同数量。唯一变量:RVO2 vs A Pro RVO。
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
场景数量FPS RVO2FPS A ProFPS ΔSim Δ ms空间重叠 RVO2空间重叠 A Pro重叠 Δ结论
FreeSwim10022.030.2+8.2+0.260.40.40.0可用
FreeSwim30022.530.0+7.5-7.0816.117.9+1.8小赢
FreeSwim50011.928.3+16.4-22.8989.1268.0+178.9性能赢
FreeSwim80019.021.7+2.7-17.21293.1844.3+551.2质量差
CrossFlow50011.428.8+17.4-50.76512.0288.7-223.3真胜利
Dense50013.027.9+14.9-37.78625.0545.5-79.5可试
Dense80013.919.1+5.2-35.02525.72400.0+1874.3重叠爆
BaitScramble50016.528.2+11.7-41.27209.9516.2+306.3性能赢
BaitScramble80012.918.8+5.9-8.02616.01031.9+415.9质量差
BaitScramble100014.015.5+1.5-9.80866.91208.8+341.9不建议
+
+
+ +
+
+
+

BaitScramble

+

鱼饵场景是关键:A Pro 帧率更好,但聚集更挤

+
+

鱼饵场景更接近真实玩法,所以它的质量风险比 FreeSwim 更值得优先处理。

+
+
+
+

Android / BaitScramble / XZ2D

+
固定:Android / BaitScramble / XZ2D。变量:数量与模式。
+ + + + + + + + + + + + + + +
数量模式FPS1% LowSim ms空间重叠最小间距
100RVO230.229.17.5012.8-0.610
100A Pro29.17.56.1518.2-0.649
300RVO226.58.833.41122.2-0.670
300A Pro30.124.815.62235.9-0.674
500RVO216.53.765.92209.9-0.694
500A Pro28.212.824.65516.2-0.686
800RVO212.93.452.27616.0-0.696
800A Pro18.86.044.251031.9-0.692
1000RVO214.04.672.77866.9-0.697
1000A Pro15.55.462.971208.8-0.692
+
+
+

鱼饵场景读法

+
业务解释:鱼饵会制造局部聚集,空间重叠比单纯 FPS 更关键。
+
+
300 FPS
A Pro
+
300 重叠
RVO2
+
500 FPS
A Pro
+
500 重叠
RVO2
+
800 FPS
A Pro
+
800 重叠
RVO2
+
+
+ 当前结论:A Pro 更能把鱼饵场景跑起来,但 300/500/800/1000 全部比 RVO2 更拥挤。它需要局部密度限制、到达鱼饵后的分层/环绕点、或重叠后处理。 +
+
+
+
+ +
+
+
+

Editor Trend

+

编辑器趋势:A Pro 在大数量下也不是质量胜利

+
+

编辑器只看趋势,不当移动端上线依据。

+
+
+
固定:Editor / XZ2D / 同场景 / 同数量。唯一变量:RVO2 vs A Pro。
+ + + + + + + + + + +
场景数量FPS RVO2FPS A ProSim Δ ms空间重叠 RVO2空间重叠 A Pro结论
FreeSwim30099.3100.9-0.226.05.8基本持平
FreeSwim300028.426.7-0.294222.14898.8更拥挤
BaitScramble30098.5105.6-0.24150.9166.1小幅更挤
BaitScramble300030.324.8+2.254955.27677.1双输
CrossFlow300028.026.9+0.773906.36149.5更拥挤
Dense300025.826.0+3.304598.36223.0质量差
+
+
+ +
+
+
+

Environment Gap

+

跨环境差异:编辑器仍会掩盖百元机红线

+
+

固定同模式 / 同场景 / XZ2D / 同数量,只改变运行环境。

+
+
+
固定:RVO2 或 A Pro / XZ2D / 同场景 / 同数量。唯一变量:Editor vs Android。
+ + + + + + + + +
配置Editor FPSAndroid FPSEditor SimAndroid Sim空间重叠 Editor / Android读法
RVO2 / FreeSwim / 30099.322.52.0821.366.0 / 16.1Android 才暴露性能压力
RVO2 / BaitScramble / 30098.526.52.2533.41150.9 / 122.2鱼饵在 Android 上 sim 成本高
A Pro / FreeSwim / 300100.930.01.8614.285.8 / 17.9帧率达标但质量变差
A Pro / BaitScramble / 300105.630.12.0115.62166.1 / 235.9鱼饵聚集在 Android 上更挤
+
+
+ +
+
+
+

Method

+

样本与统计口径

+
+
+
+
+

字段

+

averageFpsonePercentLowFpssimulationMsspatialOverlapPairs

+

新日志还包含 averageSpeed / averageAcceleration,可用于观察鱼饵聚集是否靠剧烈运动换效果。

+
+
+

环境

+

AndroidEditor

+

Android 是主依据;Editor 只看趋势和粗略上限。

+
+
+

限制

+

单次样本较多部分配置缺失

+

建议关键配置重复跑 3 次取中位数,尤其是 Android / BaitScramble / 300-800。

+
+
+
+ +
+
+
+

Next Actions

+

建议动作

+
+
+
+
+

不要直接替换

+

A Pro 先不要全局替换 RVO2。它当前在鱼饵场景的质量代价太明显,尤其 500 鱼以上。

+
+
+

优先修鱼饵聚集

+

给鱼饵周围加分布点、局部容量、停靠环、或重叠后处理。A Pro 的性能余量可以用来换更好的分散策略。

+
+
+

下一轮固定样本

+

Android / XZ2D / BaitScramble 跑 300、500、800,每个 RVO2 与 A Pro 各 3 次,加入到达鱼饵时间和鱼饵半径内拥挤度。

+
+
+

生成时间:2026-06-15。数据源目录:C:\Users\13999\Desktop\logs。

+
+
+
+ + diff --git a/FishROV/Docs/avoidance-core-comparison.html b/FishROV/Docs/avoidance-core-comparison.html deleted file mode 100644 index 262507c..0000000 --- a/FishROV/Docs/avoidance-core-comparison.html +++ /dev/null @@ -1,631 +0,0 @@ - - - - - - RVO2 避障压测方案收束 - - - -
-
FishROV Avoidance Benchmark
-

RVO2 基准与自研版压测收束

-

- 当前阶段先忽略复杂玩法状态,只压测局部避障算法在理想和极端条件下的表现。主线收束为两项:RVO2 原版作为成熟 ORCA 基准,基于 RVO2 思想的自研版作为项目可控方案。 -

-
- -
-
-
-

压测结论

-

只测两条线就够

-

- 如果当前目标是忽略实际玩法状态,只验证理想与极端条件下的局部避障性能,那么建议压测对象收束为: - RVO2 原版基于 RVO2 思想的自研版。RVO2 是稳定基准,自研版负责验证项目可控性、可调参空间和性能上限。 -

-
- -
-

推荐压测口径

-
    -
  1. 理想情况:空旷水域、均匀分布、目标方向一致或轻微交叉,验证基础吞吐和稳定收敛。
  2. -
  3. 中等冲突:双向穿流、圆阵向心、随机目标切换,验证重叠率、抖动和换边稳定性。
  4. -
  5. 极端情况:高密度聚集、初始重叠、狭窄通道、所有 agent 争同一目标点,验证退化行为。
  6. -
  7. 规模阶梯:100、500、1000、3000、5000 agent,记录每帧耗时、最大耗时、重叠对、平均速度损失。
  8. -
  9. 移动端口径:固定竖屏 1080x1920,编辑器和百元机使用相同分辨率、数量档、场景和采样窗口。
  10. -
  11. 公平条件:相同半径、最大速度、timeStep、目标生成、积分方式、邻居查询范围和日志采样策略。
  12. -
-
-
- -
-

日志与真实性要求

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
环境必须输出不要做推荐实现
Unity 编辑器算法、场景、数量、分辨率、平均 FPS、1% Low、模拟耗时、最大耗时、GC、XZ/3D 重叠对。不要每帧 `Debug.Log`,不要在热路径拼接字符串,不要每帧写文件。内存环形缓冲 + 固定间隔采样;单轮结束后一次性导出 CSV/JSON,并在 Console 只打印摘要。
百元机真机同一套指标,加设备型号、Unity 图形 API、目标帧率、分辨率 1080x1920、温度/降频观察备注。不要开详细 HUD、不要高频日志、不要连接调试器长期采样导致额外开销。Release/Development 分开测;真机默认静默聚合,测试结束或手动按钮导出日志文件。
性能公平性日志开销要对两个算法一致,最好能统计采样写入耗时。不要让某个算法额外输出调试明细,导致对比不公平。所有算法只写结构化数值样本,统一由 benchmark runner 收集和导出。
-
-
- -
-
-

一句话判断

-

- 如果当前阶段只做算法压测,RVO2 原版 + RVO2 思想自研版 就是最小有效对照组。 - 其他方案暂时不必进入压测主线,可以作为解释来源或后续扩展参考。 -

-
- -
-

共同点

-
    -
  • 大多数方案都需要上层提供目标或期望速度,然后再做局部修正。
  • -
  • 核心输入都离不开位置、速度、半径、邻居范围和预测时间。
  • -
  • 都在权衡两个目标:尽量接近期望运动,同时减少未来碰撞风险。
  • -
  • 当前压测只关心局部速度修正,不比较全局寻路、地图可达性或复杂玩法状态。
  • -
  • 在 FishROV 的 3D/2.5D 场景里,多数方案仍需要明确如何处理 XZ 投影、高度层和真实体积冲突。
  • -
-
-
- -
-

核心矩阵

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
方案核心抽象最终如何选速度安全性来源最擅长主要风险FishROV 定位
RVO2 / ORCA速度空间半平面约束
把每个邻居变成 ORCA 线性约束。
在所有安全半平面内,求最接近期望速度的新速度。显式几何约束和低维优化,理论性最强。大规模多动态 agent 的实时局部避障。不做全局规划,2D 投影,参数不当会保守、停滞或碰撞。最适合作为局部避障工程基准和对照组。
HRVO速度空间混合 VO/RVO
把 VO 与 RVO 的几何边界混合。
排除危险速度,再选择接近期望速度且更稳定的速度。速度障碍几何,加上减少 reciprocal 振荡的边界设计。对称对冲、互相避让时减少左右横跳。仍是局部 2D 方法,不处理地图和动力学约束。适合作为处理振荡问题的设计参考。
RVO_Py_MAS速度空间采样过滤
把动态体和静态障碍都转成速度禁区。
采样候选速度,先过滤禁区,有安全速度就选最接近期望速度的;没有安全速度则选折中。禁区过滤加碰撞时间折中,不是严格 ORCA 求解。学习 RVO 建模方式,快速改造原型。没有空间索引时规模变贵,采样可能错过好速度。适合作为可读的 RVO 原理参考,不建议直接当大规模生产方案。
采样避障学习版候选速度评分函数
候选速度由期望速度、偏转、减速、停止等组成。
对每个候选速度计算目标偏差、平滑、预测碰撞、当前重叠、边界等惩罚,选最低分。启发式风险函数和权重调参,不保证无碰撞。项目内自研、调参、验证新评分项。权重依赖场景,高密度可能局部最优或卡住。最适合作为 FishROV 自研避障算法的实验骨架。
rl_rvo_nav学习策略RVO 先验
把 VO/RVO 风险编码进状态和奖励。
PPO actor 根据自身状态和 VO/RVO 序列输出速度增量。训练经验、RVO 区域奖励、预计碰撞时间塑形。复杂多机器人场景下学习更灵活的风险权衡。训练成本高,泛化依赖仿真覆盖,工程可控性较弱。建议先吸收风险表达和 reward/score 思想,不急着引入完整 RL 链路。
-
-
- -
-

真正拉开差距的六个轴

-
-
-

1. 位置空间还是速度空间

-

RVO2、HRVO、RVO_Py_MAS、rl_rvo_nav 都把问题转成“哪些速度会导致未来碰撞”。采样学习版虽然是评分,但最关键的预测碰撞项也已经进入速度空间。

-
-
-

2. 约束、过滤还是惩罚

-

ORCA 是约束求解,危险速度直接被半平面排除。RVO_Py_MAS 是采样后过滤禁区。采样学习版是给风险加惩罚,危险速度未必完全禁止。RL 是把风险变成训练信号。

-
-
-

3. 谁承担避让责任

-

VO 偏“我躲别人”;RVO/ORCA 假设双方共同承担;HRVO 在 reciprocal 基础上减少来回换边。采样版需要靠评分项或通行偏置来塑造行为。

-
-
-

4. 是否进入主压测

-

当前主压测只保留 RVO2 原版和自研版。HRVO、RVO_Py_MAS、RL-RVO 只作为思想参考,不进入编辑器选择项。

-
-
-

5. 可解释性和可改性

-

RVO2 理论清晰但实现复杂。自研版最容易改权重和新增评分项。RVO_Py_MAS 最适合读懂速度障碍。RL 灵活但可解释性和复现实验成本更高。

-
-
-

6. 2D 与 3D 的边界

-

这些方案多数原生是 2D 或平面导航。FishROV 里如果只看 XZ,会把不同高度层的对象误判为冲突,也可能漏掉真实 3D 体积风险。核心决策必须明确水平避障、高度分层和体积碰撞的关系。

-
-
-
- -
-

工程选择倾向

-
-
理论安全约束
-
-
RVO2 / ORCA
-
HRVO
-
采样 / RL
-
-
-
-
可改可调
-
-
采样学习版
-
RVO_Py_MAS
-
RVO2
-
-
-
- -
-

推荐读法

-
-
-

先压测 RVO2

-

RVO2 / ORCA 当成标准基线,确认理想、拥挤、对冲、初始重叠等场景下的耗时和重叠率。

-
-
-

再压测自研版

-

自研版可以先吸收 RVO2 的速度空间思想,但保留可调评分、鱼群权重和性能优化入口。

-
-
-

暂缓其他方案

-

HRVO、RVO_Py_MAS、RL-RVO 暂时不用进编辑器主压测。先把 RVO2 和自研版两条主线测准。

-
-
-
- -
-

依据文档:rvo2-core-principles.md、unity-navmesh-core-principles.md、sampling-local-avoidance-core-principles.md、hrvo-core-principles.md、rvo-py-mas-core-principles.md、rl-rvo-nav-core-principles.md。

-
-
- - diff --git a/FishROV/Docs/local-avoidance-benchmark-notes.md b/FishROV/Docs/local-avoidance-benchmark-notes.md index f904333..3ded2de 100644 --- a/FishROV/Docs/local-avoidance-benchmark-notes.md +++ b/FishROV/Docs/local-avoidance-benchmark-notes.md @@ -2,19 +2,19 @@ ## 目标 -这个 Unity 工程用于压测本地避障方案。当前阶段先收束到两条主线: +这个 Unity 工程用于压测 FishROV 的本地避障方案。当前主线已经收束为: -- `Rvo2Adapter`:成熟 RVO2 / ORCA 基准。 -- 项目自研 RVO2 思想版本:用于验证可控性、可调参空间和性能上限。 +- `NoAvoidanceAdapter`:无避障对照基线。 +- `Rvo2Adapter`:成熟 RVO2 / ORCA 本地避障基准,也是当前可用主线。 -当前工程入口只保留 `NoAvoidance`、`Rvo2` 和自研版。不同方案之间唯一应该变化的变量,是 `AvoidanceFrameworkKind` 选择后由 `AvoidanceAdapterRegistry` 创建的 adapter。 +此前的独立启发式避障方案已经从工程入口移除。后续复杂鱼群行为不再通过重写局部避障求解器推进,而是在 RVO2 外层增加行为参数、preferred velocity 生成、RVO2 参数映射和运动后处理。 ## 共享运行入口 - 场景:`Assets/Scenes/AvoidancePerfDemo.unity` - 入口脚本:`AvoidancePerfBootstrap` -- Game 视口调试面板: - - 避障方案选择:无避障、RVO2、自研版 +- Game 视图调试面板: + - 避障方案选择:无避障、RVO2 - 测试场景选择 - 2D / 2.5D / 3D 模式 - 观察视角切换 @@ -22,35 +22,44 @@ - 暂停 / 继续 / 重置 - 平均 FPS、1% Low FPS、模拟耗时、GC 分配 - 碰撞调试:XZ 平面重叠对数、3D 空间重叠对数、最近清空间距 -- 表现对象:鱼为运行时圆锥,圆锥尖头表示朝向。 -- 共享场景: - - 自由巡游 - - 对向穿流 - - 狭窄通道 - - 圆阵高压 ## Adapter 边界 -框架接入应主要修改: +框架接入主要修改: ```text Assets/Scripts/Benchmark/FrameworkAdapters/ ``` -adapter 可以给运行时创建的对象追加框架自己的组件,但不应改变: +adapter 可以给运行时创建的对象追加框架自己的组件,但不应该改变: - 场景生成逻辑 - 鱼数量档 -- Game 视口调试控件 +- Game 视图调试控件 - 指标名称 -- 相机/视角模式 +- 相机 / 视角模式 - spawn 参数,除非框架确实需要很小且有文档说明的兼容调整 ## 当前基线 `NoAvoidanceAdapter` 是对照基线:只按期望速度移动,不做避障。 -当前压测结论主线只比较 RVO2 原版和基于 RVO2 思想的自研版本。编辑器压测入口已经只保留无避障、RVO2 和自研版,避免无关选项干扰当前结论。 +`Rvo2Adapter` 是当前主线: + +- 直接调用 `RVO.Simulator`,避免每个 agent 每帧多次反射调用污染性能对比。 +- 每帧设置期望速度前会同步 Unity Transform 位置,避免 RVO 内部位置和 benchmark 边界夹取结果漂移。 +- RVO2-CS 是 2D 算法,2D 和 2.5D 使用 Unity `x/z` 到 RVO `x/y` 的映射;3D 模式当前会投影到 XZ 平面。 + +## 后续扩展方向 + +复杂鱼群场景优先做在 RVO2 外层: + +- 行为层:胆小、攻击性、群体倾向、追逐、逃逸、领地、避障优先级。 +- 参数映射:按鱼种或状态调整 `radius`、`neighborDist`、`maxNeighbors`、`timeHorizon`、`maxSpeed`。 +- 期望速度生成:把目标、水流、群体队形、垂直偏好合成 preferred velocity。 +- 后处理:转向速度限制、加速度限制、动画姿态和视觉平滑。 + +只有当 RVO2 外层扩展无法覆盖真实需求时,才重新评估是否需要替换底层求解器。 ## 当前压测口径 @@ -63,21 +72,19 @@ adapter 可以给运行时创建的对象追加框架自己的组件,但不应 ## 日志策略 -编辑器和百元机都需要日志,但日志不能拖慢性能测试。 +编辑器和真机都需要日志,但日志不能拖慢性能测试。 - 日志目录:`Application.persistentDataPath/AvoidanceBenchmarkLogs/`。 - 编辑器下通常在用户目录的 `AppData/LocalLow/.../AvoidanceBenchmarkLogs/`。 -- 真机下在 App 的 persistent data path 里,使用面板的“导出日志”按钮或结束/重置测试自动写出。 +- 真机下在 App 的 persistent data path 里,使用面板的“导出日志”按钮或结束 / 重置测试自动写出。 - 不要每帧 `Debug.Log`。 - 不要在热路径里拼接字符串。 - 不要每帧写文件。 -- 不要让某个 adapter 单独输出额外明细,导致对比不公平。 - 建议只在内存中记录结构化数值样本。 - 当前实现固定每 `0.5s` 采样一次,先写入内存列表。 - 单轮重置、退出或点击“导出日志”时一次性导出 CSV。 -- Console 只打印一行摘要,例如算法、场景、数量、平均 FPS、1% Low、平均模拟耗时、最大耗时、重叠对峰值。 -百元机真机日志建议额外记录: +真机日志建议额外记录: - 设备型号。 - 目标帧率。 @@ -86,20 +93,3 @@ adapter 可以给运行时创建的对象追加框架自己的组件,但不应 - 是否 Development Build。 - 是否连接调试器。 - 是否出现明显发热、降频或后台干扰。 - -## RVO2 Adapter - -当前已在 `Assets/ThirdParty/RVO2-CS/` 导入 `snape/RVO2-CS` 的核心 `RVOCS` 源码,并保留 Apache-2.0 许可证说明。 - -- `Rvo2Adapter` 已从反射桥改为直接调用 `RVO.Simulator`,避免每个 agent 每帧多次反射调用污染性能对比。 -- 每帧设置期望速度前会同步 Unity Transform 位置,避免 RVO 内部位置和 benchmark 边界夹取结果漂移。 -- RVO2-CS 是 2D 算法,2D 和 2.5D 使用 Unity `x/z` 到 RVO `x/y` 的映射;3D 模式会投影到 XZ 平面。 - -## 采样避障学习版 - -`SamplingLocalAvoidanceAdapter` 是项目内自写的学习算法,用来和成熟框架做横向对比。 - -- 使用空间哈希查找近邻,避免全量 O(n²) 扫描。 -- 对每个 agent 采样多组候选速度,并用目标偏差、速度平滑、预测碰撞、当前重叠、边界越界惩罚打分。 -- 它不是完整 ORCA,也不保证无碰撞;当前只作为自研候选的弱基线,表现明显差于 RVO2 是预期内结果。 -- 如果它在某些场景比 RVO2 更稳,只能说明当前启发式适合该场景,不代表算法理论上更强。 diff --git a/FishROV/Docs/rvo-py-mas-core-principles.md b/FishROV/Docs/rvo-py-mas-core-principles.md index 7ca25ce..9fe6733 100644 --- a/FishROV/Docs/rvo-py-mas-core-principles.md +++ b/FishROV/Docs/rvo-py-mas-core-principles.md @@ -271,7 +271,7 @@ vA_post = min(suitable_V, key=lambda v: distance(v, vA)) 要在速度空间判断:哪些速度会导致未来碰撞。 ``` -它适合作为理解 RVO 思想和自研局部避障算法的入门参考;如果目标是直接上生产级大规模 agent 避障,RVO2 / ORCA 或项目内针对鱼群特征优化后的采样避障会更合适。 +它适合作为理解 RVO 思想和局部避障算法的入门参考;如果目标是直接上生产级大规模 agent 避障,当前 FishROV 主线应优先使用 RVO2 / ORCA,并把鱼群特征扩展放在 RVO2 外层行为与参数映射中。 ## 参考 diff --git a/FishROV/Docs/sampling-local-avoidance-core-principles.md b/FishROV/Docs/sampling-local-avoidance-core-principles.md deleted file mode 100644 index e4e5330..0000000 --- a/FishROV/Docs/sampling-local-avoidance-core-principles.md +++ /dev/null @@ -1,234 +0,0 @@ -# 采样避障学习版核心原理 - -## 一句话结论 - -采样避障学习版的核心是 **不直接求解析最优解,而是在一批候选速度中打分选择当前最合适的速度**。它是项目内自写的学习基线,目标是可读、可改、可调参,而不是证明无碰撞。 - -实现位置: - -```text -Assets/Scripts/Benchmark/FrameworkAdapters/SamplingLocalAvoidanceAdapter.cs -``` - -## 为什么写这个版本 - -RVO2/ORCA 很成熟,但内部数学和约束求解不容易快速改。 - -NavMeshAgent 工程成熟,但它是完整导航系统,不是纯局部避障算法。 - -学习版采样避障的目的不同: - -- 给你一个完全可控的 baseline。 -- 让每个评分项都能看懂、能改权重。 -- 方便测试“如果我改变风险函数,会发生什么”。 -- 为后续写更好的自研算法提供实验平台。 - -## 基本思路 - -每帧对每个 agent 做三步: - -```text -1. 找附近邻居 -2. 生成候选速度 -3. 给候选速度打分,选择分数最低的速度 -``` - -它不是像 RVO2 那样构造半平面约束,而是把避障变成一个启发式评分问题。 - -## 空间哈希找邻居 - -如果每个 agent 都和所有其他 agent 比较,复杂度是: - -```text -O(n²) -``` - -5000 条鱼时会非常贵。 - -所以学习版先把 agent 放进空间哈希格子: - -```text -世界坐标 x/z - -> 根据 cellSize 计算格子坐标 - -> 每个格子保存一批 agent - -> 查询时只看自己附近几个格子 -``` - -这样大多数情况下只需要检查附近 agent,而不是全量扫描。 - -## 候选速度 - -候选速度主要来自: - -- 当前期望速度。 -- 零速度。 -- 以期望方向为基准旋转出的多个方向。 -- 不同速度倍率,例如全速和半速。 - -可以理解为: - -```text -我想往目标方向走, -但也试试稍微偏左、偏右、减速、停下, -看哪个风险最低。 -``` - -当前版本每帧采样固定数量方向,优先保持实现简单和可读。 - -## 评分函数 - -每个候选速度都会计算一个分数: - -```text -总分 = - 目标偏差惩罚 - + 速度平滑惩罚 - + 预测碰撞惩罚 - + 当前重叠惩罚 - + 边界惩罚 -``` - -分数越低,速度越适合当前帧。 - -## 目标偏差惩罚 - -agent 不能为了避障完全忘记目标。 - -如果候选速度和期望速度差得很远,就加惩罚: - -```text -goalPenalty = |candidate - preferred|² -``` - -这个项越大,agent 越倾向继续朝目标走。 - -## 速度平滑惩罚 - -如果每帧都选一个差异很大的速度,视觉上会抖。 - -所以学习版会惩罚候选速度和上一帧速度的差异: - -```text -smoothPenalty = |candidate - lastVelocity|² -``` - -这个项越大,运动越平滑,但反应也可能变慢。 - -## 当前重叠惩罚 - -如果两个 agent 当前已经重叠,必须强烈惩罚继续贴近的速度。 - -学习版会计算: - -```text -当前距离 - 两者半径和 -``` - -如果小于 0,说明已经重叠。重叠越深,惩罚越高。 - -这个项主要用来处理初始生成过密、或上一帧已经挤在一起的情况。 - -## 预测碰撞惩罚 - -只看当前距离不够。两个 agent 现在很远,但如果高速对冲,很快也会撞。 - -学习版会估算候选速度下的最近接近时刻: - -```text -relativePosition = otherPosition - selfPosition -relativeVelocity = candidateVelocity - otherVelocity -closestTime = clamp(-dot(relativePosition, relativeVelocity) / |relativeVelocity|², 0, timeHorizon) -``` - -然后计算在 `closestTime` 时两者距离是否小于安全半径。 - -如果预测未来会撞,就加惩罚;越快撞、越深撞,惩罚越高。 - -这一步是学习版最接近 RVO 思想的地方:它不只问“现在近不近”,而是问“这个速度会不会导致未来碰撞”。 - -## 边界惩罚 - -benchmark 有 arena 半径。候选速度如果把 agent 推向边界外,就加惩罚。 - -这能减少 agent 因避障一路逃出场地边缘,然后被 `BenchmarkAgent.Integrate` 夹回边界导致的异常运动。 - -## 每帧流程 - -学习版每帧流程是: - -1. adapter 收集每个 agent 的期望速度。 -2. 构建空间哈希。 -3. 对每个 agent 生成候选速度。 -4. 对候选速度逐个打分。 -5. 选择最低分速度。 -6. 写回 `BenchmarkAgent.ApplyVelocity`。 -7. benchmark 统一积分移动。 - -## 优点 - -- 完全自写,许可证清晰。 -- 很容易读懂和调参。 -- 可以快速增加新的评分项。 -- 可以观察每个启发式对结果的影响。 -- 适合作为自研算法实验起点。 - -## 局限 - -- 不保证无碰撞。 -- 候选速度数量有限,可能错过真正好速度。 -- 权重依赖场景,换场景可能需要重新调。 -- 高密度时仍可能局部最优或互相卡住。 -- 当前实现是 CPU 主线程版本,还没有 Job/Burst 优化。 -- 它是启发式算法,不是完整 ORCA。 - -## 和 RVO2 的区别 - -| 维度 | RVO2 / ORCA | 采样避障学习版 | -| --- | --- | --- | -| 核心方法 | 速度空间半平面约束求解 | 候选速度打分 | -| 理论性 | 更强 | 较弱 | -| 可解释性 | 数学清晰,但实现复杂 | 每个评分项直接可读 | -| 可调性 | 参数较少但影响大 | 权重和候选集都可改 | -| 无碰撞保证 | 更接近可证明安全 | 不保证 | -| 学习价值 | 学习速度障碍理论 | 学习工程化评分和调参 | - -## 和 NavMesh 的区别 - -| 维度 | Unity NavMeshAgent | 采样避障学习版 | -| --- | --- | --- | -| 主要目标 | 地图导航和路径跟随 | 局部动态避障 | -| 地图理解 | 有 NavMesh 可达性 | 没有地图理解 | -| 动态群体 | 可做但不是最专门 | 专门面向局部邻居 | -| 3D 体积 | 主要平面 | 当前也主要平面,可扩展 | -| 可改性 | 内部黑盒较多 | 完全可改 | - -## 后续改进方向 - -这个学习版后续可以逐步升级: - -- 增加候选速度自适应采样,而不是固定 16 方向。 -- 把邻居风险按距离、相对速度、角色类型加权。 -- 引入最大加速度和最大转向角约束。 -- 增加“右侧通行/左侧通行”偏置,减少对称死锁。 -- 增加队形或鱼群凝聚项。 -- 使用 Job System / Burst 优化空间哈希和评分。 -- 把评分函数输出到调试 HUD,观察每项惩罚占比。 -- 借鉴 RVO2,把明显危险的速度直接剔除,再对剩余速度打分。 - -## 对自研算法的启发 - -如果要写更好的 FishROV 避障算法,可以把学习版当成骨架: - -```text -目标速度 - -> 候选速度生成 - -> 风险预测 - -> 多目标评分 - -> 速度平滑和运动约束 -``` - -后续真正要优化的,是三个核心点: - -- 更准确的风险函数。 -- 更聪明的候选速度生成。 -- 更稳定的冲突解除策略。