From e596d178667d4646935f1d8ade60255d8cfe9469 Mon Sep 17 00:00:00 2001 From: "JSD\\13999" <1399945104@qq.com> Date: Sat, 13 Jun 2026 16:42:53 +0800 Subject: [PATCH] =?UTF-8?q?feat(avoidance):=20=E6=B7=BB=E5=8A=A0=E4=BD=8E?= =?UTF-8?q?=E5=BC=80=E9=94=80=E5=8E=8B=E6=B5=8B=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Benchmark/AvoidanceBenchmarkLogWriter.cs | 156 ++++++++++++++++++ .../AvoidanceBenchmarkLogWriter.cs.meta | 11 ++ .../Benchmark/AvoidancePerfBootstrap.cs | 11 ++ .../Docs/local-avoidance-benchmark-notes.md | 10 +- 4 files changed, 184 insertions(+), 4 deletions(-) create mode 100644 FishROV/Assets/Scripts/Benchmark/AvoidanceBenchmarkLogWriter.cs create mode 100644 FishROV/Assets/Scripts/Benchmark/AvoidanceBenchmarkLogWriter.cs.meta diff --git a/FishROV/Assets/Scripts/Benchmark/AvoidanceBenchmarkLogWriter.cs b/FishROV/Assets/Scripts/Benchmark/AvoidanceBenchmarkLogWriter.cs new file mode 100644 index 0000000..a953c5c --- /dev/null +++ b/FishROV/Assets/Scripts/Benchmark/AvoidanceBenchmarkLogWriter.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using UnityEngine; + +namespace FishROV.AvoidanceBenchmark +{ + public sealed class AvoidanceBenchmarkLogWriter + { + private const float SampleInterval = 0.5f; + private const int MaxSamples = 4096; + + private readonly List samples = new List(1024); + private AvoidanceBenchmarkConfig config; + private string runName; + private float runStartTime; + private float nextSampleTime; + private bool hasRun; + + public string LastExportPath { get; private set; } = "尚未导出"; + public int SampleCount => samples.Count; + + public void StartRun(AvoidanceBenchmarkConfig benchmarkConfig, string adapterName) + { + ExportIfNeeded(); + + config = benchmarkConfig.Clone(); + runName = SanitizeFileName($"{DateTime.Now:yyyyMMdd_HHmmss}_{adapterName}_{config.Scenario}_{config.DimensionMode}_{config.FishCount}"); + runStartTime = Time.unscaledTime; + nextSampleTime = runStartTime; + samples.Clear(); + hasRun = true; + LastExportPath = "本轮尚未导出"; + } + + public void Record( + AvoidanceMetricsRecorder metrics, + AvoidanceCollisionProbe collisionProbe, + int agentCount) + { + if (!hasRun || Time.unscaledTime < nextSampleTime || samples.Count >= MaxSamples) + { + return; + } + + nextSampleTime += SampleInterval; + samples.Add(new Sample + { + TimeSeconds = Time.unscaledTime - runStartTime, + AgentCount = agentCount, + AverageFps = metrics.AverageFps, + OnePercentLowFps = metrics.OnePercentLowFps, + SimulationMs = metrics.SimulationMs, + GcAllocKb = metrics.GcAllocBytes / 1024.0, + HorizontalOverlapPairs = collisionProbe.HorizontalOverlapPairs, + SpatialOverlapPairs = collisionProbe.SpatialOverlapPairs, + MinimumClearance = collisionProbe.MinimumClearance + }); + } + + public string ExportIfNeeded() + { + if (!hasRun || samples.Count == 0) + { + return LastExportPath; + } + + var directory = Path.Combine(Application.persistentDataPath, "AvoidanceBenchmarkLogs"); + Directory.CreateDirectory(directory); + var path = Path.Combine(directory, runName + ".csv"); + + 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"); + for (var i = 0; i < samples.Count; i++) + { + var sample = samples[i]; + writer.Write(config.Framework); + writer.Write(','); + writer.Write(config.Scenario); + writer.Write(','); + writer.Write(config.DimensionMode); + writer.Write(','); + writer.Write(config.FishCount); + writer.Write(','); + WriteFloat(writer, config.ArenaRadius); + writer.Write(','); + writer.Write(Screen.width); + writer.Write(','); + writer.Write(Screen.height); + writer.Write(','); + writer.Write(Application.targetFrameRate); + writer.Write(','); + WriteFloat(writer, sample.TimeSeconds); + writer.Write(','); + writer.Write(sample.AgentCount); + writer.Write(','); + WriteFloat(writer, sample.AverageFps); + writer.Write(','); + WriteFloat(writer, sample.OnePercentLowFps); + writer.Write(','); + WriteDouble(writer, sample.SimulationMs); + writer.Write(','); + WriteDouble(writer, sample.GcAllocKb); + writer.Write(','); + writer.Write(sample.HorizontalOverlapPairs); + writer.Write(','); + writer.Write(sample.SpatialOverlapPairs); + writer.Write(','); + WriteFloat(writer, sample.MinimumClearance); + writer.WriteLine(); + } + } + + LastExportPath = path; + samples.Clear(); + hasRun = false; + Debug.Log($"Avoidance benchmark log exported: {path}"); + return LastExportPath; + } + + private static void WriteFloat(TextWriter writer, float value) + { + writer.Write(value.ToString("0.###", CultureInfo.InvariantCulture)); + } + + private static void WriteDouble(TextWriter writer, double value) + { + writer.Write(value.ToString("0.###", CultureInfo.InvariantCulture)); + } + + private static string SanitizeFileName(string value) + { + foreach (var invalid in Path.GetInvalidFileNameChars()) + { + value = value.Replace(invalid, '_'); + } + + return value; + } + + private struct Sample + { + public float TimeSeconds; + public int AgentCount; + public float AverageFps; + public float OnePercentLowFps; + public double SimulationMs; + public double GcAllocKb; + public int HorizontalOverlapPairs; + public int SpatialOverlapPairs; + public float MinimumClearance; + } + } +} diff --git a/FishROV/Assets/Scripts/Benchmark/AvoidanceBenchmarkLogWriter.cs.meta b/FishROV/Assets/Scripts/Benchmark/AvoidanceBenchmarkLogWriter.cs.meta new file mode 100644 index 0000000..f1e81ae --- /dev/null +++ b/FishROV/Assets/Scripts/Benchmark/AvoidanceBenchmarkLogWriter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9c67563022844219b17fcf6a3630062c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/FishROV/Assets/Scripts/Benchmark/AvoidancePerfBootstrap.cs b/FishROV/Assets/Scripts/Benchmark/AvoidancePerfBootstrap.cs index fc311e3..2e3e6b9 100644 --- a/FishROV/Assets/Scripts/Benchmark/AvoidancePerfBootstrap.cs +++ b/FishROV/Assets/Scripts/Benchmark/AvoidancePerfBootstrap.cs @@ -17,6 +17,7 @@ namespace FishROV.AvoidanceBenchmark private readonly AvoidanceScenarioController scenarioController = new AvoidanceScenarioController(); private readonly AvoidanceMetricsRecorder metrics = new AvoidanceMetricsRecorder(); private readonly AvoidanceCollisionProbe collisionProbe = new AvoidanceCollisionProbe(); + private readonly AvoidanceBenchmarkLogWriter logWriter = new AvoidanceBenchmarkLogWriter(); private AvoidanceBenchmarkConfig config; private IAvoidanceAdapter adapter; private Transform agentRoot; @@ -59,6 +60,7 @@ namespace FishROV.AvoidanceBenchmark collisionProbe.Update(agents, config.DimensionMode, Time.unscaledDeltaTime); metrics.EndSimulation(); + logWriter.Record(metrics, collisionProbe, agents.Count); } private void OnGUI() @@ -72,6 +74,7 @@ namespace FishROV.AvoidanceBenchmark 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($"日志样本:{logWriter.SampleCount} | {logWriter.LastExportPath}"); GUILayout.Space(8); DrawEnumButtons("避障方案", framework, value => @@ -123,12 +126,18 @@ namespace FishROV.AvoidanceBenchmark { ResetBenchmark(); } + + if (GUILayout.Button("导出日志")) + { + logWriter.ExportIfNeeded(); + } GUILayout.EndHorizontal(); GUILayout.EndArea(); } private void OnDestroy() { + logWriter.ExportIfNeeded(); adapter?.Dispose(); metrics.Dispose(); } @@ -137,6 +146,7 @@ namespace FishROV.AvoidanceBenchmark { adapter?.Dispose(); adapter = null; + logWriter.ExportIfNeeded(); if (agentRoot != null) { @@ -160,6 +170,7 @@ namespace FishROV.AvoidanceBenchmark SpawnFish(); scenarioController.Reset(config, agents); + logWriter.StartRun(config, adapter.Name); } private void SpawnFish() diff --git a/FishROV/Docs/local-avoidance-benchmark-notes.md b/FishROV/Docs/local-avoidance-benchmark-notes.md index 5bbefc4..f904333 100644 --- a/FishROV/Docs/local-avoidance-benchmark-notes.md +++ b/FishROV/Docs/local-avoidance-benchmark-notes.md @@ -65,14 +65,16 @@ adapter 可以给运行时创建的对象追加框架自己的组件,但不应 编辑器和百元机都需要日志,但日志不能拖慢性能测试。 +- 日志目录:`Application.persistentDataPath/AvoidanceBenchmarkLogs/`。 +- 编辑器下通常在用户目录的 `AppData/LocalLow/.../AvoidanceBenchmarkLogs/`。 +- 真机下在 App 的 persistent data path 里,使用面板的“导出日志”按钮或结束/重置测试自动写出。 - 不要每帧 `Debug.Log`。 - 不要在热路径里拼接字符串。 - 不要每帧写文件。 - 不要让某个 adapter 单独输出额外明细,导致对比不公平。 - 建议只在内存中记录结构化数值样本。 -- 建议固定间隔采样,例如每 `0.5s` 或每 `30` 帧聚合一次。 -- 建议使用环形缓冲或预分配数组,避免压测中产生 GC。 -- 单轮结束后一次性导出 CSV/JSON。 +- 当前实现固定每 `0.5s` 采样一次,先写入内存列表。 +- 单轮重置、退出或点击“导出日志”时一次性导出 CSV。 - Console 只打印一行摘要,例如算法、场景、数量、平均 FPS、1% Low、平均模拟耗时、最大耗时、重叠对峰值。 百元机真机日志建议额外记录: @@ -99,5 +101,5 @@ adapter 可以给运行时创建的对象追加框架自己的组件,但不应 - 使用空间哈希查找近邻,避免全量 O(n²) 扫描。 - 对每个 agent 采样多组候选速度,并用目标偏差、速度平滑、预测碰撞、当前重叠、边界越界惩罚打分。 -- 它不是完整 ORCA,也不保证无碰撞;目的在于提供一个可读、可改、可调参的学习基线。 +- 它不是完整 ORCA,也不保证无碰撞;当前只作为自研候选的弱基线,表现明显差于 RVO2 是预期内结果。 - 如果它在某些场景比 RVO2 更稳,只能说明当前启发式适合该场景,不代表算法理论上更强。