refactor(benchmark): 整理避障压测逻辑和报告

This commit is contained in:
JSD\13999
2026-06-15 16:19:48 +08:00
parent 6854cca5a6
commit dc8ad8bfe6
13 changed files with 1856 additions and 1256 deletions

View File

@@ -9,8 +9,10 @@ namespace FishROV.AvoidanceBenchmark
private static Mesh sharedConeMesh; private static Mesh sharedConeMesh;
private static Material sharedConeMaterial; private static Material sharedConeMaterial;
private static MaterialPropertyBlock colorPropertyBlock; private static MaterialPropertyBlock colorPropertyBlock;
private static bool useCustomConeMaterial;
private Vector3 velocity; private Vector3 velocity;
private Vector3 previousVelocity;
private float maxSpeed = 1f; private float maxSpeed = 1f;
public BenchmarkAgentRole Role { get; private set; } public BenchmarkAgentRole Role { get; private set; }
@@ -18,8 +20,18 @@ namespace FishROV.AvoidanceBenchmark
public float Radius { get; private set; } public float Radius { get; private set; }
public float MaxSpeed => maxSpeed; public float MaxSpeed => maxSpeed;
public Vector3 Velocity => velocity; 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 Vector3 Target { get; set; }
public static void SetSharedConeMaterial(Material material)
{
useCustomConeMaterial = material != null;
sharedConeMaterial = material;
}
public static BenchmarkAgent CreatePrimitive(BenchmarkAgentSpawnInfo spawnInfo, Transform parent) public static BenchmarkAgent CreatePrimitive(BenchmarkAgentSpawnInfo spawnInfo, Transform parent)
{ {
var go = CreateConeObject(); var go = CreateConeObject();
@@ -50,6 +62,12 @@ namespace FishROV.AvoidanceBenchmark
GroupId = spawnInfo.GroupId; GroupId = spawnInfo.GroupId;
Radius = spawnInfo.Radius; Radius = spawnInfo.Radius;
maxSpeed = Mathf.Max(0.01f, spawnInfo.MaxSpeed); maxSpeed = Mathf.Max(0.01f, spawnInfo.MaxSpeed);
velocity = Vector3.zero;
previousVelocity = Vector3.zero;
CurrentSpeed = 0f;
PeakSpeed = 0f;
CurrentAcceleration = 0f;
PeakAcceleration = 0f;
ApplyColor(); ApplyColor();
} }
@@ -58,8 +76,20 @@ namespace FishROV.AvoidanceBenchmark
velocity = Vector3.ClampMagnitude(preferredVelocity, maxSpeed); 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) 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; var next = transform.position + velocity * deltaTime;
if (dimensionMode != AvoidanceDimensionMode.XYZ3D) if (dimensionMode != AvoidanceDimensionMode.XYZ3D)
{ {
@@ -91,6 +121,11 @@ namespace FishROV.AvoidanceBenchmark
return; return;
} }
if (useCustomConeMaterial)
{
return;
}
var color = Color.white; var color = Color.white;
switch (Role) switch (Role)
{ {
@@ -107,6 +142,7 @@ namespace FishROV.AvoidanceBenchmark
var propertyBlock = GetColorPropertyBlock(); var propertyBlock = GetColorPropertyBlock();
propertyBlock.SetColor("_Color", color); propertyBlock.SetColor("_Color", color);
propertyBlock.SetColor("_BaseColor", color);
renderer.SetPropertyBlock(propertyBlock); renderer.SetPropertyBlock(propertyBlock);
} }
@@ -173,13 +209,64 @@ namespace FishROV.AvoidanceBenchmark
return sharedConeMaterial; 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" name = "Benchmark Agent Shared Material"
}; };
return sharedConeMaterial; return sharedConeMaterial;
} }
private static Material FindFirstBuiltinMaterial(params string[] materialNames)
{
for (var i = 0; i < materialNames.Length; i++)
{
var material = Resources.GetBuiltinResource<Material>(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() private static MaterialPropertyBlock GetColorPropertyBlock()
{ {
return colorPropertyBlock ??= new MaterialPropertyBlock(); return colorPropertyBlock ??= new MaterialPropertyBlock();

View File

@@ -37,6 +37,7 @@ namespace FishROV.AvoidanceBenchmark
public void Record( public void Record(
AvoidanceMetricsRecorder metrics, AvoidanceMetricsRecorder metrics,
AvoidanceCollisionProbe collisionProbe, AvoidanceCollisionProbe collisionProbe,
AvoidancePerfBootstrap.MotionStats motionStats,
int agentCount) int agentCount)
{ {
if (!hasRun || Time.unscaledTime < nextSampleTime || samples.Count >= MaxSamples) if (!hasRun || Time.unscaledTime < nextSampleTime || samples.Count >= MaxSamples)
@@ -55,7 +56,11 @@ namespace FishROV.AvoidanceBenchmark
GcAllocKb = metrics.GcAllocBytes / 1024.0, GcAllocKb = metrics.GcAllocBytes / 1024.0,
HorizontalOverlapPairs = collisionProbe.HorizontalOverlapPairs, HorizontalOverlapPairs = collisionProbe.HorizontalOverlapPairs,
SpatialOverlapPairs = collisionProbe.SpatialOverlapPairs, 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)) 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++) for (var i = 0; i < samples.Count; i++)
{ {
var sample = samples[i]; var sample = samples[i];
@@ -109,6 +114,14 @@ namespace FishROV.AvoidanceBenchmark
writer.Write(sample.SpatialOverlapPairs); writer.Write(sample.SpatialOverlapPairs);
writer.Write(','); writer.Write(',');
WriteFloat(writer, sample.MinimumClearance); 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(); writer.WriteLine();
} }
} }
@@ -151,6 +164,10 @@ namespace FishROV.AvoidanceBenchmark
public int HorizontalOverlapPairs; public int HorizontalOverlapPairs;
public int SpatialOverlapPairs; public int SpatialOverlapPairs;
public float MinimumClearance; public float MinimumClearance;
public float AverageSpeed;
public float MaxSpeed;
public float AverageAcceleration;
public float MaxAcceleration;
} }
} }
} }

View File

@@ -12,6 +12,13 @@ namespace FishROV.AvoidanceBenchmark
[SerializeField] private AvoidanceDimensionMode dimensionMode = AvoidanceDimensionMode.XZ25D; [SerializeField] private AvoidanceDimensionMode dimensionMode = AvoidanceDimensionMode.XZ25D;
[SerializeField] private BenchmarkViewMode viewMode = BenchmarkViewMode.Overview; [SerializeField] private BenchmarkViewMode viewMode = BenchmarkViewMode.Overview;
[SerializeField] private int fishCount = 300; [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<BenchmarkAgent> agents = new List<BenchmarkAgent>(512); private readonly List<BenchmarkAgent> agents = new List<BenchmarkAgent>(512);
private readonly AvoidanceScenarioController scenarioController = new AvoidanceScenarioController(); private readonly AvoidanceScenarioController scenarioController = new AvoidanceScenarioController();
@@ -21,17 +28,25 @@ namespace FishROV.AvoidanceBenchmark
private AvoidanceBenchmarkConfig config; private AvoidanceBenchmarkConfig config;
private IAvoidanceAdapter adapter; private IAvoidanceAdapter adapter;
private Transform agentRoot; private Transform agentRoot;
private Transform baitMarker;
private Camera benchmarkCamera; private Camera benchmarkCamera;
private bool paused; private bool paused;
private int pendingFishCount; private int pendingFishCount;
private MotionStats motionStats;
private bool hookDropping;
private float hookDropElapsed;
private Vector3 hookDropStart;
private Vector3 hookDropTarget;
private void Start() private void Start()
{ {
QualitySettings.vSyncCount = 0; QualitySettings.vSyncCount = 0;
Application.targetFrameRate = -1; Application.targetFrameRate = -1;
ApplyTimeScale();
pendingFishCount = fishCount; pendingFishCount = fishCount;
metrics.Start(); metrics.Start();
EnsureCameraAndLight(); EnsureCameraAndLight();
EnsureBaitMarker();
ResetBenchmark(); ResetBenchmark();
} }
@@ -46,6 +61,9 @@ namespace FishROV.AvoidanceBenchmark
} }
metrics.BeginSimulation(); metrics.BeginSimulation();
UpdateHookDrop(Time.deltaTime);
ApplyRuntimeSpeed();
scenarioController.Tick(Time.deltaTime);
foreach (var agent in agents) foreach (var agent in agents)
{ {
var velocity = scenarioController.GetPreferredVelocity(agent, Time.deltaTime); var velocity = scenarioController.GetPreferredVelocity(agent, Time.deltaTime);
@@ -58,24 +76,40 @@ namespace FishROV.AvoidanceBenchmark
agent.Integrate(Time.deltaTime, config.DimensionMode, config.ArenaRadius); agent.Integrate(Time.deltaTime, config.DimensionMode, config.ArenaRadius);
} }
motionStats = CalculateMotionStats();
collisionProbe.Update(agents, config.DimensionMode, Time.unscaledDeltaTime); collisionProbe.Update(agents, config.DimensionMode, Time.unscaledDeltaTime);
metrics.EndSimulation(); metrics.EndSimulation();
logWriter.Record(metrics, collisionProbe, agents.Count); logWriter.Record(metrics, collisionProbe, motionStats, agents.Count);
} }
private void OnGUI() 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("FishROV 本地避障压测");
GUILayout.Label($"当前方案:{adapter?.Name ?? ""}"); GUILayout.Label($"当前方案:{adapter?.Name ?? ""}");
GUILayout.Label($"状态:{adapter?.Status ?? ""}"); 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.AverageFps:F1} | 1% Low {metrics.OnePercentLowFps:F1}");
GUILayout.Label($"模拟耗时:{metrics.SimulationMs:F2} ms | GC{metrics.GcAllocBytes / 1024f:F1} KB"); 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($"速度:极速设定 {(config?.FishSpeed ?? fishMaxSpeed):F1} | 实测峰值 {motionStats.MaxSpeed:F2} | 平均 {motionStats.AverageSpeed:F2}");
GUILayout.Label($"帧率限制不锁帧vSync={QualitySettings.vSyncCount}targetFrameRate={Application.targetFrameRate}"); 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.Label($"日志样本:{logWriter.SampleCount} | {logWriter.LastExportPath}");
GUILayout.Space(8);
if (GUILayout.Button("投放鱼钩:下落后惊扰争饵", GUILayout.Height(28)))
{
StartBaitScrambleDemo();
}
GUILayout.Space(8);
DrawRuntimeTuningControls();
GUILayout.Space(8); GUILayout.Space(8);
DrawEnumButtons("避障方案", framework, value => DrawEnumButtons("避障方案", framework, value =>
{ {
@@ -95,7 +129,7 @@ namespace FishROV.AvoidanceBenchmark
DrawEnumButtons("观察视角", viewMode, value => viewMode = value); DrawEnumButtons("观察视角", viewMode, value => viewMode = value);
GUILayout.Space(8); GUILayout.Space(8);
GUILayout.Label($"鱼数量:{pendingFishCount}"); GUILayout.Label($"鱼数量:{pendingFishCount}");
GUILayout.BeginHorizontal(); GUILayout.BeginHorizontal();
foreach (var preset in CountPresets) foreach (var preset in CountPresets)
{ {
@@ -137,6 +171,7 @@ namespace FishROV.AvoidanceBenchmark
private void OnDestroy() private void OnDestroy()
{ {
Time.timeScale = 1f;
logWriter.ExportIfNeeded(); logWriter.ExportIfNeeded();
adapter?.Dispose(); adapter?.Dispose();
metrics.Dispose(); metrics.Dispose();
@@ -160,6 +195,9 @@ namespace FishROV.AvoidanceBenchmark
Scenario = scenario, Scenario = scenario,
DimensionMode = dimensionMode, DimensionMode = dimensionMode,
FishCount = fishCount, FishCount = fishCount,
FishSpeed = scenario == AvoidanceScenarioKind.BaitScramble
? Mathf.Max(fishMaxSpeed, baitDemoMaxSpeed)
: Mathf.Max(0.1f, fishMaxSpeed),
UseSchoolProxy = false UseSchoolProxy = false
}; };
@@ -167,12 +205,110 @@ namespace FishROV.AvoidanceBenchmark
agents.Clear(); agents.Clear();
adapter = AvoidanceAdapterRegistry.Create(config.Framework); adapter = AvoidanceAdapterRegistry.Create(config.Framework);
adapter.Initialize(config); adapter.Initialize(config);
BenchmarkAgent.SetSharedConeMaterial(fishMaterial);
SpawnFish(); SpawnFish();
scenarioController.Reset(config, agents); 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); 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() private void SpawnFish()
{ {
for (var i = 0; i < config.FishCount; i++) 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); 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; var point = Random.insideUnitCircle * radius;
return new Vector3(point.x, LayerY(index), point.y); 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() private void EnsureCameraAndLight()
{ {
benchmarkCamera = Camera.main; 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<Renderer>();
if (renderer != null && fishMaterial != null)
{
renderer.sharedMaterial = fishMaterial;
}
var collider = marker.GetComponent<Collider>();
if (collider != null)
{
collider.enabled = false;
}
baitMarker = marker.transform;
marker.SetActive(false);
}
private void UpdateCamera() private void UpdateCamera()
{ {
if (benchmarkCamera == null || config == null) if (benchmarkCamera == null || config == null)
@@ -300,6 +544,40 @@ namespace FishROV.AvoidanceBenchmark
return sum / agents.Count; 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<T>(string label, T current, System.Action<T> onChanged) where T : System.Enum private static void DrawEnumButtons<T>(string label, T current, System.Action<T> onChanged) where T : System.Enum
{ {
GUILayout.Label(label); GUILayout.Label(label);
@@ -325,8 +603,10 @@ namespace FishROV.AvoidanceBenchmark
return "无避障"; return "无避障";
case AvoidanceFrameworkKind.Rvo2: case AvoidanceFrameworkKind.Rvo2:
return "RVO2"; return "RVO2";
case AvoidanceFrameworkKind.SamplingLocal: case AvoidanceFrameworkKind.AstarProRvo:
return "自研版"; return "A* Pro RVO";
case AvoidanceFrameworkKind.AstarProRvoVelocityLike:
return "A* Pro RVO Velocity";
case AvoidanceScenarioKind.FreeSwim: case AvoidanceScenarioKind.FreeSwim:
return "自由巡游"; return "自由巡游";
case AvoidanceScenarioKind.CrossFlow: case AvoidanceScenarioKind.CrossFlow:
@@ -335,6 +615,8 @@ namespace FishROV.AvoidanceBenchmark
return "狭窄通道"; return "狭窄通道";
case AvoidanceScenarioKind.DenseCircleStress: case AvoidanceScenarioKind.DenseCircleStress:
return "圆阵高压"; return "圆阵高压";
case AvoidanceScenarioKind.BaitScramble:
return "鱼钩争饵";
case AvoidanceDimensionMode.XZ2D: case AvoidanceDimensionMode.XZ2D:
return "2D/XZ"; return "2D/XZ";
case AvoidanceDimensionMode.XZ25D: case AvoidanceDimensionMode.XZ25D:
@@ -348,10 +630,18 @@ namespace FishROV.AvoidanceBenchmark
case BenchmarkViewMode.Side: case BenchmarkViewMode.Side:
return "侧视"; return "侧视";
case BenchmarkViewMode.FollowSchool: case BenchmarkViewMode.FollowSchool:
return "跟鱼群"; return "跟鱼群";
default: default:
return value.ToString(); return value.ToString();
} }
} }
public struct MotionStats
{
public float AverageSpeed;
public float MaxSpeed;
public float AverageAcceleration;
public float MaxAcceleration;
}
} }
} }

View File

@@ -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<BenchmarkAgent, AgentState> states =
new Dictionary<BenchmarkAgent, AgentState>(512);
private readonly List<AgentState> orderedStates = new List<AgentState>(512);
private readonly Dictionary<Vector2Int, List<AgentState>> buckets =
new Dictionary<Vector2Int, List<AgentState>>(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<AgentState>(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; }
}
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 41e3ccd900cb4a37bf60c67fa49c906c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -8,11 +8,15 @@ namespace FishROV.AvoidanceBenchmark
private readonly Dictionary<BenchmarkAgent, Vector3> targets = new Dictionary<BenchmarkAgent, Vector3>(); private readonly Dictionary<BenchmarkAgent, Vector3> targets = new Dictionary<BenchmarkAgent, Vector3>();
private AvoidanceBenchmarkConfig config; private AvoidanceBenchmarkConfig config;
private float elapsed; private float elapsed;
private float baitDropTime;
private Vector3 baitPosition;
public void Reset(AvoidanceBenchmarkConfig benchmarkConfig, IReadOnlyList<BenchmarkAgent> agents) public void Reset(AvoidanceBenchmarkConfig benchmarkConfig, IReadOnlyList<BenchmarkAgent> agents)
{ {
config = benchmarkConfig.Clone(); config = benchmarkConfig.Clone();
elapsed = 0f; elapsed = 0f;
baitDropTime = 0f;
baitPosition = Vector3.zero;
targets.Clear(); targets.Clear();
foreach (var agent in agents) 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; 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)) if (!targets.TryGetValue(agent, out var target))
{ {
target = CreateTarget(agent); target = CreateTarget(agent);
targets[agent] = target; targets[agent] = target;
} }
if (config.Scenario == AvoidanceScenarioKind.BaitScramble)
{
return GetBaitScrambleVelocity(agent);
}
if ((target - agent.transform.position).sqrMagnitude < 4f) if ((target - agent.transform.position).sqrMagnitude < 4f)
{ {
target = CreateTarget(agent); target = CreateTarget(agent);
@@ -57,6 +79,8 @@ namespace FishROV.AvoidanceBenchmark
switch (config.Scenario) switch (config.Scenario)
{ {
case AvoidanceScenarioKind.BaitScramble:
return baitPosition;
case AvoidanceScenarioKind.CrossFlow: case AvoidanceScenarioKind.CrossFlow:
return agent.GroupId % 2 == 0 return agent.GroupId % 2 == 0
? new Vector3(radius, HeightFor(agent), Random.Range(-radius, radius)) ? new Vector3(radius, HeightFor(agent), Random.Range(-radius, radius))
@@ -87,5 +111,47 @@ namespace FishROV.AvoidanceBenchmark
return Random.Range(-4f, 4f); 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;
}
} }
} }

View File

@@ -0,0 +1,637 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light">
<title>FishROV RVO2 本地避障 Benchmark 报告</title>
<style>
:root {
--ink: #172033;
--muted: #667085;
--paper: #ffffff;
--canvas: #eef2f8;
--line: #dce3ee;
--navy: #102a43;
--blue: #2f6fed;
--blue-soft: #eaf1ff;
--cyan: #13a8a8;
--cyan-soft: #e6fafa;
--green: #138a5b;
--green-soft: #e7f7ef;
--orange: #e77817;
--orange-soft: #fff2e5;
--red: #c73e4d;
--red-soft: #ffeaed;
--gold: #c88a10;
--gold-soft: #fff7d8;
--shadow: 0 14px 40px rgba(16, 42, 67, .09);
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
color: var(--ink);
background:
radial-gradient(circle at 8% 0%, rgba(47, 111, 237, .11), transparent 28rem),
linear-gradient(180deg, #f7f9fc 0, var(--canvas) 100%);
font: 14px/1.7 -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
}
a { color: inherit; }
.shell {
display: grid;
grid-template-columns: 248px minmax(0, 1180px);
gap: 26px;
width: min(1480px, calc(100% - 32px));
margin: 24px auto 64px;
align-items: start;
}
.side {
position: sticky;
top: 20px;
min-width: 0;
padding: 20px 16px;
border: 1px solid var(--line);
border-radius: 16px;
background: rgba(255, 255, 255, .94);
box-shadow: var(--shadow);
backdrop-filter: blur(10px);
}
.brand {
display: flex;
gap: 11px;
align-items: center;
padding: 2px 5px 17px;
border-bottom: 1px solid var(--line);
}
.brand-mark {
display: grid;
width: 38px;
height: 38px;
place-items: center;
border-radius: 11px;
color: #fff;
background: linear-gradient(145deg, var(--blue), #1744ad);
font-size: 18px;
font-weight: 800;
box-shadow: 0 8px 16px rgba(47, 111, 237, .28);
}
.brand strong { display: block; font-size: 15px; }
.brand span { display: block; color: var(--muted); font-size: 11px; }
.side nav { margin-top: 14px; }
.side nav a {
display: block;
padding: 7px 10px;
border-radius: 8px;
color: #46536a;
text-decoration: none;
font-size: 13px;
}
.side nav a:hover {
color: var(--blue);
background: var(--blue-soft);
}
.side-note {
margin: 15px 5px 0;
padding-top: 14px;
border-top: 1px solid var(--line);
color: var(--muted);
font-size: 11px;
}
main { min-width: 0; }
.hero, .section {
border: 1px solid var(--line);
border-radius: 18px;
background: var(--paper);
box-shadow: var(--shadow);
}
.hero {
position: relative;
overflow: hidden;
padding: 38px 40px 34px;
color: #fff;
background:
linear-gradient(115deg, rgba(16, 42, 67, .98), rgba(23, 64, 112, .96) 55%, rgba(47, 111, 237, .92)),
#102a43;
}
.hero::after {
position: absolute;
right: -55px;
bottom: -115px;
width: 330px;
height: 330px;
border: 45px solid rgba(255, 255, 255, .055);
border-radius: 50%;
content: "";
}
.eyebrow {
margin: 0 0 10px;
color: #a8c8ff;
font-size: 12px;
font-weight: 750;
letter-spacing: .13em;
text-transform: uppercase;
}
h1 {
max-width: 850px;
margin: 0;
font-size: clamp(30px, 5vw, 48px);
line-height: 1.18;
letter-spacing: -.025em;
}
.lead {
max-width: 850px;
margin: 16px 0 0;
color: #d8e6fb;
font-size: 16px;
}
.hero-meta, .summary-grid, .grid-2, .grid-3 {
display: grid;
gap: 14px;
}
.hero-meta {
grid-template-columns: repeat(4, minmax(0, 1fr));
margin-top: 28px;
}
.hero-meta div {
padding: 12px 14px;
border: 1px solid rgba(255, 255, 255, .13);
border-radius: 11px;
background: rgba(255, 255, 255, .07);
}
.hero-meta span { display: block; color: #a9bdd8; font-size: 11px; }
.hero-meta strong { display: block; margin-top: 2px; font-size: 14px; }
.section {
margin-top: 20px;
padding: 28px 30px;
scroll-margin-top: 24px;
}
.section-head {
display: flex;
gap: 20px;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 20px;
}
.kicker {
margin: 0 0 3px;
color: var(--blue);
font-size: 11px;
font-weight: 800;
letter-spacing: .12em;
text-transform: uppercase;
}
h2 { margin: 0; color: var(--navy); font-size: 24px; line-height: 1.3; }
h3 { margin: 0 0 10px; color: var(--navy); font-size: 16px; }
p { margin: 8px 0; }
.muted { color: var(--muted); }
.summary-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.metric {
padding: 17px;
border: 1px solid var(--line);
border-radius: 13px;
background: #fbfcfe;
}
.metric b { display: block; color: var(--blue); font-size: 27px; line-height: 1.2; }
.metric span { display: block; margin-top: 6px; color: var(--muted); font-size: 12px; }
.grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid-2 > *, .grid-3 > * { min-width: 0; }
.info-card {
padding: 17px 18px;
border: 1px solid var(--line);
border-radius: 13px;
background: #fbfcfe;
}
.info-card p:last-child { margin-bottom: 0; }
.tag {
display: inline-block;
margin: 2px 5px 2px 0;
padding: 3px 9px;
border-radius: 999px;
color: #31507d;
background: var(--blue-soft);
font-size: 11px;
font-weight: 700;
}
.tag.green { color: var(--green); background: var(--green-soft); }
.tag.orange { color: #9a4e05; background: var(--orange-soft); }
.tag.red { color: var(--red); background: var(--red-soft); }
.tag.cyan { color: #087273; background: var(--cyan-soft); }
.callout {
margin: 17px 0 0;
padding: 14px 16px;
border-left: 4px solid var(--blue);
border-radius: 9px;
background: var(--blue-soft);
}
.callout.warn { border-color: var(--orange); background: var(--orange-soft); }
.callout.good { border-color: var(--green); background: var(--green-soft); }
.compare-card {
padding: 17px;
border: 1px solid var(--line);
border-radius: 13px;
background: #fbfcfe;
}
.compare-card .rule {
margin-bottom: 13px;
padding-bottom: 11px;
border-bottom: 1px solid var(--line);
color: var(--muted);
font-size: 12px;
}
.compare-card .rule strong { color: var(--navy); }
table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
th, td {
padding: 10px 9px;
border-bottom: 1px solid var(--line);
text-align: right;
vertical-align: top;
white-space: nowrap;
font-variant-numeric: tabular-nums;
}
th {
color: #31507d;
background: #f3f7fd;
font-size: 12px;
font-weight: 800;
}
th:first-child, td:first-child,
th:nth-child(2), td:nth-child(2) { text-align: left; }
.wide-table { overflow-x: auto; }
.score {
display: inline-flex;
min-width: 68px;
justify-content: center;
padding: 2px 8px;
border-radius: 999px;
font-size: 12px;
font-weight: 800;
}
.score.good { color: var(--green); background: var(--green-soft); }
.score.warn { color: #9a4e05; background: var(--orange-soft); }
.score.bad { color: var(--red); background: var(--red-soft); }
.good-text { color: var(--green); font-weight: 800; }
.warn-text { color: var(--orange); font-weight: 800; }
.bad-text { color: var(--red); font-weight: 800; }
.bar-list {
display: grid;
gap: 10px;
margin-top: 12px;
}
.bar-row {
display: grid;
grid-template-columns: 106px 1fr 68px;
gap: 10px;
align-items: center;
font-size: 12px;
font-weight: 750;
font-variant-numeric: tabular-nums;
}
.track {
height: 14px;
border-radius: 999px;
background: #e8eef7;
overflow: hidden;
}
.fill {
height: 100%;
border-radius: 999px;
background: var(--blue);
}
.fill.green { background: var(--green); }
.fill.orange { background: var(--orange); }
.fill.red { background: var(--red); }
.foot {
margin-top: 18px;
color: var(--muted);
font-size: 12px;
}
@media (max-width: 1080px) {
.shell { grid-template-columns: 1fr; }
.side { position: static; }
.side nav { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); }
.hero-meta, .summary-grid, .grid-2, .grid-3 { grid-template-columns: 1fr; }
.section-head { display: block; }
}
@media (max-width: 640px) {
.shell { width: min(100% - 20px, 1480px); margin-top: 10px; }
.hero, .section { border-radius: 14px; }
.hero { padding: 28px 22px; }
.section { padding: 22px 18px; }
.side nav { grid-template-columns: 1fr; }
.bar-row { grid-template-columns: 86px 1fr 56px; }
}
</style>
</head>
<body>
<div class="shell">
<aside class="side">
<div class="brand">
<div class="brand-mark">FR</div>
<div>
<strong>FishROV Benchmark</strong>
<span>RVO2 local avoidance</span>
</div>
</div>
<nav>
<a href="#summary">结论摘要</a>
<a href="#phone">百元机内对比</a>
<a href="#editor">编辑器内对比</a>
<a href="#cross">编辑器 vs 百元机</a>
<a href="#method">控制变量口径</a>
<a href="#actions">建议动作</a>
</nav>
<div class="side-note">
统计口径:排除 timeSeconds = 0 的启动采样。比较表均标注“固定条件”和“唯一变量”。
</div>
</aside>
<main>
<section class="hero">
<p class="eyebrow">2026-06-13 / AvoidanceBenchmarkLogs</p>
<h1>RVO2 本地避障结果:按唯一变量拆开看</h1>
<p class="lead">这版报告不再把编辑器和百元机混成一张表。先在百元机内部比较算法、数量、维度;再在编辑器内部看同样趋势;最后才做同配置的跨环境差异。</p>
<div class="hero-meta">
<div><span>移动端主依据</span><strong>百元机 / 720x1600</strong></div>
<div><span>趋势参考</span><strong>编辑器 / 750x617</strong></div>
<div><span>推荐默认档</span><strong>300 鱼 + XYZ3D</strong></div>
<div><span>压测红线</span><strong>500 鱼开始警戒</strong></div>
</div>
</section>
<section id="summary" class="section">
<div class="section-head">
<div>
<p class="kicker">Executive Summary</p>
<h2>结论摘要</h2>
</div>
<p class="muted">先给结论,再看证据。所有跨表比较都避免同时改变多个核心变量。</p>
</div>
<div class="summary-grid">
<div class="metric"><b>300</b><span>百元机可用档。RVO2 / FreeSwim / XZ25D 第二次运行达到 30.1 FPS空间重叠均值 1.1。</span></div>
<div class="metric"><b>500</b><span>百元机压测线。XYZ3D 在 FreeSwim 仍有 26.2 FPS但 1% Low 已降到 15.8。</span></div>
<div class="metric"><b>XYZ3D</b><span>同场景同数量下空间重叠最低Dense/CrossFlow 1000 都明显优于 XZ2D/XZ25D。</span></div>
<div class="metric"><b>6-14x</b><span>同配置下,编辑器 FPS 约为百元机 6 倍左右;百元机 simulation ms 约为编辑器 10-14 倍。</span></div>
</div>
<div class="callout good">
<strong>推荐口径:</strong>玩法默认目标按百元机结果定,不按编辑器定。编辑器只用于观察算法随数量和维度变化的趋势。
</div>
</section>
<section id="phone" class="section">
<div class="section-head">
<div>
<p class="kicker">Phone Only / Controlled Variables</p>
<h2>百元机内对比</h2>
</div>
<p class="muted">同一设备环境内比较,避免把硬件差异混进算法结论。</p>
</div>
<div class="grid-2">
<article class="compare-card">
<h3>算法变量:无避障 vs RVO2</h3>
<div class="rule"><strong>固定:</strong>百元机 / FreeSwim / XZ25D / 300 鱼。<strong>唯一变量:</strong>避障策略。</div>
<table>
<thead><tr><th>策略</th><th>FPS</th><th>1% Low</th><th>空间重叠</th><th>最小间距</th><th>判断</th></tr></thead>
<tbody>
<tr><td>无避障</td><td>7.0</td><td>0.5</td><td>80.9</td><td>-0.674</td><td><span class="score bad">不可用</span></td></tr>
<tr><td>RVO2 第一次</td><td>26.4</td><td>14.4</td><td>1.6</td><td>-0.298</td><td><span class="score warn">有尖峰</span></td></tr>
<tr><td>RVO2 第二次</td><td>30.1</td><td>23.6</td><td>1.1</td><td>-0.017</td><td><span class="score good">可用</span></td></tr>
</tbody>
</table>
<p class="muted">RVO2 不是只减少重叠,也显著改善了百元机上的可运行性。第一次与第二次 RVO2 结果不同,后续建议保留重复运行取中位数。</p>
</article>
<article class="compare-card">
<h3>数量变量FreeSwim / XYZ3D</h3>
<div class="rule"><strong>固定:</strong>百元机 / RVO2 / FreeSwim / XYZ3D。<strong>唯一变量:</strong>鱼数量。</div>
<div class="bar-list">
<div class="bar-row"><span>100 鱼</span><div class="track"><div class="fill green" style="width:100%"></div></div><strong>30.2 FPS</strong></div>
<div class="bar-row"><span>500 鱼</span><div class="track"><div class="fill orange" style="width:86.8%"></div></div><strong>26.2 FPS</strong></div>
<div class="bar-row"><span>800 鱼</span><div class="track"><div class="fill red" style="width:39.1%"></div></div><strong>11.8 FPS</strong></div>
<div class="bar-row"><span>1500 鱼</span><div class="track"><div class="fill red" style="width:29.5%"></div></div><strong>8.9 FPS</strong></div>
</div>
<table>
<thead><tr><th>数量</th><th>FPS</th><th>1% Low</th><th>Sim ms</th><th>P95 ms</th><th>空间重叠</th></tr></thead>
<tbody>
<tr><td>100</td><td>30.2</td><td>28.6</td><td>4.43</td><td>7.82</td><td>0.0</td></tr>
<tr><td>500</td><td>26.2</td><td>15.8</td><td>22.70</td><td>37.08</td><td>0.7</td></tr>
<tr><td>800</td><td>11.8</td><td>5.2</td><td>54.07</td><td>104.84</td><td>7.7</td></tr>
<tr><td>1500</td><td>8.9</td><td>3.1</td><td>103.64</td><td>189.01</td><td>84.2</td></tr>
</tbody>
</table>
</article>
</div>
<div class="grid-2" style="margin-top:14px;">
<article class="compare-card">
<h3>维度变量Dense 1000</h3>
<div class="rule"><strong>固定:</strong>百元机 / RVO2 / DenseCircleStress / 1000 鱼。<strong>唯一变量:</strong>维度。</div>
<table>
<thead><tr><th>维度</th><th>FPS</th><th>Sim ms</th><th>P95 ms</th><th>空间重叠</th><th>判断</th></tr></thead>
<tbody>
<tr><td>XYZ3D</td><td>14.2</td><td>55.33</td><td>103.78</td><td class="good-text">87.1</td><td><span class="score warn">质量最好</span></td></tr>
<tr><td>XZ25D</td><td>12.1</td><td>69.90</td><td>144.30</td><td class="warn-text">474.4</td><td><span class="score bad">偏高</span></td></tr>
<tr><td>XZ2D</td><td>11.8</td><td>70.63</td><td>131.34</td><td class="bad-text">1190.3</td><td><span class="score bad">最差</span></td></tr>
</tbody>
</table>
</article>
<article class="compare-card">
<h3>维度变量CrossFlow 1000</h3>
<div class="rule"><strong>固定:</strong>百元机 / RVO2 / CrossFlow / 1000 鱼。<strong>唯一变量:</strong>维度。</div>
<table>
<thead><tr><th>维度</th><th>FPS</th><th>Sim ms</th><th>P95 ms</th><th>空间重叠</th><th>判断</th></tr></thead>
<tbody>
<tr><td>XYZ3D</td><td>13.1</td><td>60.92</td><td>117.81</td><td class="good-text">38.5</td><td><span class="score warn">质量最好</span></td></tr>
<tr><td>XZ25D</td><td>11.9</td><td>77.62</td><td>202.03</td><td class="warn-text">384.4</td><td><span class="score bad">尾延迟高</span></td></tr>
<tr><td>XZ2D</td><td>12.2</td><td>60.66</td><td>118.03</td><td class="bad-text">714.1</td><td><span class="score bad">重叠高</span></td></tr>
</tbody>
</table>
</article>
</div>
</section>
<section id="editor" class="section">
<div class="section-head">
<div>
<p class="kicker">Editor Only / Controlled Variables</p>
<h2>编辑器内对比</h2>
</div>
<p class="muted">编辑器不用于移动端性能结论,但可以看数量增长和维度变化的趋势是否一致。</p>
</div>
<div class="grid-2">
<article class="compare-card">
<h3>数量变量FreeSwim / XYZ3D</h3>
<div class="rule"><strong>固定:</strong>编辑器 / RVO2 / FreeSwim / XYZ3D。<strong>唯一变量:</strong>鱼数量。</div>
<table>
<thead><tr><th>数量</th><th>FPS</th><th>1% Low</th><th>Sim ms</th><th>P95 ms</th><th>空间重叠</th></tr></thead>
<tbody>
<tr><td>100</td><td>169.4</td><td>93.4</td><td>0.62</td><td>0.84</td><td>0.0</td></tr>
<tr><td>300</td><td>134.0</td><td>72.7</td><td>1.53</td><td>2.17</td><td>0.0</td></tr>
<tr><td>500</td><td>129.7</td><td>67.8</td><td>2.33</td><td>2.41</td><td>0.2</td></tr>
<tr><td>1000</td><td>71.5</td><td>33.0</td><td>4.73</td><td>4.98</td><td>5.1</td></tr>
<tr><td>2000</td><td>40.5</td><td>17.7</td><td>12.18</td><td>25.97</td><td>55.3</td></tr>
<tr><td>5000</td><td>18.2</td><td>5.7</td><td>36.22</td><td>86.95</td><td>774.7</td></tr>
</tbody>
</table>
</article>
<article class="compare-card">
<h3>维度变量Dense 1000 / 2000</h3>
<div class="rule"><strong>固定:</strong>编辑器 / RVO2 / DenseCircleStress / 同数量。<strong>唯一变量:</strong>维度。</div>
<table>
<thead><tr><th>数量</th><th>维度</th><th>FPS</th><th>Sim ms</th><th>P95 ms</th><th>空间重叠</th></tr></thead>
<tbody>
<tr><td>1000</td><td>XYZ3D</td><td>85.2</td><td>5.38</td><td>8.21</td><td class="good-text">29.5</td></tr>
<tr><td>1000</td><td>XZ25D</td><td>83.8</td><td>4.97</td><td>5.43</td><td class="warn-text">264.6</td></tr>
<tr><td>1000</td><td>XZ2D</td><td>79.8</td><td>5.41</td><td>7.25</td><td class="bad-text">851.9</td></tr>
<tr><td>2000</td><td>XYZ3D</td><td>55.1</td><td>11.14</td><td>26.22</td><td class="good-text">154.2</td></tr>
<tr><td>2000</td><td>XZ25D</td><td>50.2</td><td>12.17</td><td>26.58</td><td class="warn-text">765.8</td></tr>
<tr><td>2000</td><td>XZ2D</td><td>48.7</td><td>10.34</td><td>11.58</td><td class="bad-text">2093.6</td></tr>
</tbody>
</table>
</article>
</div>
<div class="callout">
<strong>编辑器趋势与百元机一致:</strong>XYZ3D 的空间重叠始终最低XZ2D 在 Dense 场景中最容易堆叠。不同的是,编辑器性能余量会掩盖百元机上的真实性能红线。
</div>
</section>
<section id="cross" class="section">
<div class="section-head">
<div>
<p class="kicker">Cross Environment / Same Config</p>
<h2>编辑器 vs 百元机</h2>
</div>
<p class="muted">同配置跨环境对比只能说明设备差异,不用于证明算法优劣。</p>
</div>
<div class="wide-table compare-card">
<div class="rule"><strong>固定:</strong>RVO2 / 同场景 / 同维度 / 同数量。<strong>唯一变量:</strong>运行环境。</div>
<table>
<thead>
<tr>
<th>配置</th>
<th>编辑器 FPS</th>
<th>百元机 FPS</th>
<th>FPS 倍率</th>
<th>编辑器 Sim ms</th>
<th>百元机 Sim ms</th>
<th>Sim 倍率</th>
<th>空间重叠:编辑器 / 百元机</th>
</tr>
</thead>
<tbody>
<tr><td>FreeSwim / XYZ3D / 100</td><td>169.4</td><td>30.2</td><td>5.6x</td><td>0.62</td><td>4.43</td><td>7.1x</td><td>0.0 / 0.0</td></tr>
<tr><td>FreeSwim / XYZ3D / 500</td><td>129.7</td><td>26.2</td><td>5.0x</td><td>2.33</td><td>22.70</td><td>9.7x</td><td>0.2 / 0.7</td></tr>
<tr><td>Dense / XYZ3D / 1000</td><td>85.2</td><td>14.2</td><td>6.0x</td><td>5.38</td><td>55.33</td><td>10.3x</td><td>29.5 / 87.1</td></tr>
<tr><td>Dense / XZ25D / 1000</td><td>83.8</td><td>12.1</td><td>6.9x</td><td>4.97</td><td>69.90</td><td>14.1x</td><td>264.6 / 474.4</td></tr>
<tr><td>Dense / XZ2D / 1000</td><td>79.8</td><td>11.8</td><td>6.8x</td><td>5.41</td><td>70.63</td><td>13.1x</td><td>851.9 / 1190.3</td></tr>
</tbody>
</table>
</div>
<div class="callout warn">
<strong>读法:</strong>跨环境对比的唯一变量是“运行环境”,因此它只回答“编辑器结果离百元机有多远”。它不能替代百元机内的算法、数量、维度对比。
</div>
</section>
<section id="method" class="section">
<div class="section-head">
<div>
<p class="kicker">Method</p>
<h2>控制变量口径</h2>
</div>
</div>
<div class="grid-3">
<div class="info-card">
<h3>百元机内对比</h3>
<p><span class="tag green">环境固定</span><span class="tag">百元机</span></p>
<p class="muted">用于回答移动端真实可用性:算法是否有效、数量红线在哪里、哪个维度质量最好。</p>
</div>
<div class="info-card">
<h3>编辑器内对比</h3>
<p><span class="tag cyan">环境固定</span><span class="tag">编辑器</span></p>
<p class="muted">用于观察趋势,不直接判断移动端上线规模。</p>
</div>
<div class="info-card">
<h3>跨环境对比</h3>
<p><span class="tag orange">配置固定</span><span class="tag">环境变化</span></p>
<p class="muted">用于量化编辑器到百元机的性能折损,避免把桌面余量误当移动端余量。</p>
</div>
</div>
</section>
<section id="actions" class="section">
<div class="section-head">
<div>
<p class="kicker">Next Actions</p>
<h2>建议动作</h2>
</div>
</div>
<div class="grid-3">
<div class="info-card">
<h3>玩法默认档</h3>
<p>按百元机结果,把默认目标定为 <strong>300 鱼 + XYZ3D</strong>。500 鱼作为压测档,不作为默认玩法规模。</p>
</div>
<div class="info-card">
<h3>优化方向</h3>
<p>500+ 鱼需要分级避障:远场降频或关闭 RVO只对近场、玩家交互区和高风险鱼群启用。</p>
</div>
<div class="info-card">
<h3>后续 Benchmark</h3>
<p>补齐设备、构建类型、VSync、targetFrameRate、Burst/Jobs 状态,并对同配置至少跑 3 次取中位数。</p>
</div>
</div>
<p class="foot">数据源:编辑器批次来自 C:\Users\13999\AppData\LocalLow\DefaultCompany\FishROV\AvoidanceBenchmarkLogs百元机批次来自 C:\Users\13999\AppData\Local\Temp。</p>
</section>
</main>
</div>
</body>
</html>

View File

@@ -0,0 +1,153 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>FishROV Android 2 避障复测报告</title>
<style>
:root{--ink:#172033;--muted:#667085;--paper:#fff;--canvas:#eef2f8;--line:#dce3ee;--blue:#2f6fed;--blue-soft:#eaf1ff;--green:#138a5b;--green-soft:#e7f7ef;--orange:#d96b11;--orange-soft:#fff2e5;--red:#c73e4d;--shadow:0 14px 40px rgba(16,42,67,.09)}
*{box-sizing:border-box}body{margin:0;color:var(--ink);background:linear-gradient(180deg,#f7f9fc 0,var(--canvas) 100%);font:14px/1.65 -apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif}.shell{display:grid;grid-template-columns:248px minmax(0,1180px);gap:26px;width:min(1480px,calc(100% - 32px));margin:24px auto 64px;align-items:start}.side{position:sticky;top:20px;padding:20px 16px;border:1px solid var(--line);border-radius:16px;background:rgba(255,255,255,.94);box-shadow:var(--shadow)}.brand{display:flex;gap:11px;align-items:center;padding:2px 5px 17px;border-bottom:1px solid var(--line)}.brand-mark{display:grid;width:38px;height:38px;place-items:center;border-radius:11px;color:#fff;background:linear-gradient(145deg,var(--blue),#1744ad);font-weight:800}.brand strong{display:block;font-size:15px}.brand span{color:var(--muted);font-size:11px}nav{margin-top:14px}nav a{display:block;padding:7px 10px;border-radius:8px;color:#46536a;text-decoration:none;font-size:13px}nav a:hover{color:var(--blue);background:var(--blue-soft)}main{min-width:0}.hero,.section{border:1px solid var(--line);border-radius:18px;background:var(--paper);box-shadow:var(--shadow)}.hero{padding:38px 40px 34px;color:#fff;background:linear-gradient(115deg,rgba(16,42,67,.98),rgba(23,64,112,.96) 56%,rgba(47,111,237,.92))}.eyebrow{margin:0 0 8px;color:#cfe1ff;font-weight:700;letter-spacing:.02em;text-transform:uppercase;font-size:12px}h1{margin:0;font-size:34px;line-height:1.15;letter-spacing:0}.hero p{max-width:840px;margin:16px 0 0;color:#e7efff;font-size:15px}.metrics{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin-top:22px}.metric{padding:14px 16px;border-radius:12px;background:rgba(255,255,255,.12);border:1px solid rgba(255,255,255,.18)}.metric span{display:block;color:#cfe1ff;font-size:12px}.metric b{display:block;font-size:22px;margin-top:2px}.section{margin-top:18px;padding:26px 28px}h2{margin:0 0 14px;font-size:22px}h3{margin:0;font-size:16px}.callout{padding:15px 16px;border-radius:12px;border:1px solid var(--line);background:#f8fbff}.callout.strong{border-color:#c9dcff;background:var(--blue-soft)}ul{margin:10px 0 0;padding-left:20px}li{margin:6px 0}.group-lists{display:grid;grid-template-columns:1fr 1fr;gap:14px}.compare-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}.compare{border:1px solid var(--line);border-radius:14px;padding:16px;background:#fff}.compare.ok{border-color:#bde5d0}.compare.warn{border-color:#ffd5a8}.compare-head{display:flex;justify-content:space-between;gap:12px;align-items:start}.chip{display:inline-flex;margin:0 0 4px 5px;padding:3px 8px;border-radius:999px;font-size:12px;border:1px solid var(--line);color:#46536a}.chip.ok{color:var(--green);background:var(--green-soft);border-color:#bde5d0}.chip.warn{color:var(--orange);background:var(--orange-soft);border-color:#ffd5a8}.delta-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px;margin:14px 0}.delta-grid div{padding:10px;border-radius:10px;background:#f7f9fc}.delta-grid span{display:block;color:var(--muted);font-size:11px}.delta-grid b{font-size:18px}.good{color:var(--green)}.bad{color:var(--red)}.flat{color:#5f6c80}table{width:100%;border-collapse:collapse}th,td{padding:9px 10px;border-bottom:1px solid var(--line);text-align:right;white-space:nowrap}th:first-child,td:first-child{text-align:left}th{color:#536074;font-size:12px;background:#f7f9fc}.mini th,.mini td{padding:7px 8px;font-size:12px}.table-wrap{overflow:auto;border:1px solid var(--line);border-radius:12px}@media(max-width:980px){.shell{display:block}.side{position:static;margin-bottom:16px}.metrics,.compare-grid,.group-lists,.delta-grid{grid-template-columns:1fr}h1{font-size:28px}}
</style>
</head>
<body>
<div class="shell">
<aside class="side">
<div class="brand"><div class="brand-mark">FR</div><div><strong>FishROV Benchmark</strong><span>Android_2 复测</span></div></div>
<nav><a href="#summary">结论</a><a href="#quality">样本质量</a><a href="#compare">控制变量对比</a><a href="#raw">完整数据</a><a href="#next">下一轮测试</a></nav>
</aside>
<main>
<section class="hero"><p class="eyebrow">2026-06-15 / 百元机 / XZ2D</p><h1>A Pro VelocityLike 复测报告</h1><p>本页分析 <code>C:/Users/13999/Desktop/logs/android_2</code> 下 27 个 CSV。统计时排除了 <code>timeSeconds=0</code> 的启动样本,避免初始化尖峰污染稳态数据。</p><div class="metrics"><div class="metric"><span>CSV</span><b>27</b></div><div class="metric"><span>场景</span><b>3</b></div><div class="metric"><span>鱼数范围</span><b>300-1000</b></div><div class="metric"><span>屏幕</span><b>720x1600</b></div></div></section>
<section class="section" id="summary"><h2>结论</h2><div class="callout strong"><ul><li>300 鱼鱼饵争抢是最干净的样本VelocityLike 平均 FPS 29.9,比 RVO2 高 +3.2%,比旧 A Pro 高 +2.1%;最小间距从旧 A Pro -0.625 改到 -0.141,重叠深度明显收敛。</li><li>300 鱼圆阵高压中A Pro 两种接法 FPS 接近满帧,但平均重叠约 337.5/340.0,仍高于 RVO2 的 159.0;这更像算法/场景拥挤上限,不只是远目标语义问题。</li><li>500 鱼鱼饵争抢三组速度一致但速度倍率已到 maxSpeed 16.8VelocityLike FPS 27.7,略好于 RVO2/旧 A Pro重叠与旧 A Pro 基本同级,说明高速度争抢下输入语义已不是主因。</li><li>CrossFlow 500 不能直接判 VelocityLikeVelocityLike 的 maxSpeed 是 9.6RVO2/旧 A Pro 是 2.4/2.4,速度变量被改变。</li></ul></div></section>
<section class="section" id="quality"><h2>样本质量</h2><div class="group-lists"><div class="callout"><strong>可以优先采信</strong><ul><li>鱼饵争抢 / 300 鱼</li><li>鱼饵争抢 / 500 鱼</li><li>鱼饵争抢 / 800 鱼</li><li>圆阵高压 / 1000 鱼</li></ul></div><div class="callout"><strong>需要谨慎</strong><ul><li>鱼饵争抢 / 1000 鱼:三后端齐全 不满足</li><li>对向穿流 / 300 鱼:三后端齐全 不满足</li><li>对向穿流 / 500 鱼:速度一致 不满足</li><li>圆阵高压 / 300 鱼:时长充足 不满足</li><li>圆阵高压 / 500 鱼:时长充足 不满足</li><li>圆阵高压 / 800 鱼:时长充足 不满足</li></ul></div></div></section>
<section class="section" id="compare"><h2>控制变量对比</h2><div class="compare-grid"><article class="compare warn">
<div class="compare-head"><h3>鱼饵争抢 / 1000 鱼</h3><div><span class="chip warn">后端不齐</span><span class="chip ok">速度一致</span><span class="chip ok">时长充足</span></div></div>
<div class="delta-grid">
<div><span>Velocity vs RVO2 FPS</span><b class=""></b></div>
<div><span>Velocity vs RVO2 重叠</span><b class=""></b></div>
<div><span>Velocity vs 旧 A Pro FPS</span><b class="flat">+2.2%</b></div>
<div><span>Velocity vs 旧 A Pro 重叠</span><b class="good">-3.8%</b></div>
</div>
<table class="mini"><thead><tr><th>后端</th><th>FPS</th><th>Sim ms</th><th>重叠</th><th>最小间距</th><th>MaxSpeed</th><th>时长</th></tr></thead><tbody><tr><td>A Pro RVO</td><td>13.4</td><td>53.3</td><td>1240.8</td><td>-0.699</td><td>16.8</td><td>13.6s</td></tr><tr><td>A Pro VelocityLike</td><td>13.7</td><td>55.0</td><td>1193.7</td><td>-0.697</td><td>16.8</td><td>20.5s</td></tr></tbody></table>
</article>
<article class="compare ok">
<div class="compare-head"><h3>鱼饵争抢 / 300 鱼</h3><div><span class="chip ok">三后端齐全</span><span class="chip ok">速度一致</span><span class="chip ok">时长充足</span></div></div>
<div class="delta-grid">
<div><span>Velocity vs RVO2 FPS</span><b class="good">+3.2%</b></div>
<div><span>Velocity vs RVO2 重叠</span><b class="bad">+9.9%</b></div>
<div><span>Velocity vs 旧 A Pro FPS</span><b class="flat">+2.1%</b></div>
<div><span>Velocity vs 旧 A Pro 重叠</span><b class="good">-3.2%</b></div>
</div>
<table class="mini"><thead><tr><th>后端</th><th>FPS</th><th>Sim ms</th><th>重叠</th><th>最小间距</th><th>MaxSpeed</th><th>时长</th></tr></thead><tbody><tr><td>RVO2</td><td>29.0</td><td>16.8</td><td>125.3</td><td>-0.624</td><td>4.2</td><td>25.0s</td></tr><tr><td>A Pro RVO</td><td>29.3</td><td>13.9</td><td>142.1</td><td>-0.625</td><td>4.2</td><td>20.5s</td></tr><tr><td>A Pro VelocityLike</td><td>29.9</td><td>13.2</td><td>137.7</td><td>-0.141</td><td>4.2</td><td>21.0s</td></tr></tbody></table>
</article>
<article class="compare ok">
<div class="compare-head"><h3>鱼饵争抢 / 500 鱼</h3><div><span class="chip ok">三后端齐全</span><span class="chip ok">速度一致</span><span class="chip ok">时长充足</span></div></div>
<div class="delta-grid">
<div><span>Velocity vs RVO2 FPS</span><b class="good">+3.2%</b></div>
<div><span>Velocity vs RVO2 重叠</span><b class="bad">+93.8%</b></div>
<div><span>Velocity vs 旧 A Pro FPS</span><b class="flat">+2.9%</b></div>
<div><span>Velocity vs 旧 A Pro 重叠</span><b class="flat">-1.0%</b></div>
</div>
<table class="mini"><thead><tr><th>后端</th><th>FPS</th><th>Sim ms</th><th>重叠</th><th>最小间距</th><th>MaxSpeed</th><th>时长</th></tr></thead><tbody><tr><td>RVO2</td><td>26.8</td><td>27.3</td><td>277.1</td><td>-0.682</td><td>16.8</td><td>8.0s</td></tr><tr><td>A Pro RVO</td><td>26.9</td><td>24.6</td><td>542.2</td><td>-0.684</td><td>16.8</td><td>13.5s</td></tr><tr><td>A Pro VelocityLike</td><td>27.7</td><td>23.6</td><td>536.9</td><td>-0.678</td><td>16.8</td><td>27.5s</td></tr></tbody></table>
</article>
<article class="compare ok">
<div class="compare-head"><h3>鱼饵争抢 / 800 鱼</h3><div><span class="chip ok">三后端齐全</span><span class="chip ok">速度一致</span><span class="chip ok">时长充足</span></div></div>
<div class="delta-grid">
<div><span>Velocity vs RVO2 FPS</span><b class="good">+22.8%</b></div>
<div><span>Velocity vs RVO2 重叠</span><b class="bad">+67.6%</b></div>
<div><span>Velocity vs 旧 A Pro FPS</span><b class="good">+11.6%</b></div>
<div><span>Velocity vs 旧 A Pro 重叠</span><b class="bad">+3.2%</b></div>
</div>
<table class="mini"><thead><tr><th>后端</th><th>FPS</th><th>Sim ms</th><th>重叠</th><th>最小间距</th><th>MaxSpeed</th><th>时长</th></tr></thead><tbody><tr><td>RVO2</td><td>16.8</td><td>47.5</td><td>640.1</td><td>-0.694</td><td>16.8</td><td>10.5s</td></tr><tr><td>A Pro RVO</td><td>18.5</td><td>39.9</td><td>1040.4</td><td>-0.697</td><td>16.8</td><td>15.0s</td></tr><tr><td>A Pro VelocityLike</td><td>20.6</td><td>39.9</td><td>1073.2</td><td>-0.694</td><td>16.8</td><td>17.5s</td></tr></tbody></table>
</article>
<article class="compare warn">
<div class="compare-head"><h3>对向穿流 / 300 鱼</h3><div><span class="chip warn">后端不齐</span><span class="chip ok">速度一致</span><span class="chip ok">时长充足</span></div></div>
<div class="delta-grid">
<div><span>Velocity vs RVO2 FPS</span><b class=""></b></div>
<div><span>Velocity vs RVO2 重叠</span><b class=""></b></div>
<div><span>Velocity vs 旧 A Pro FPS</span><b class=""></b></div>
<div><span>Velocity vs 旧 A Pro 重叠</span><b class=""></b></div>
</div>
<table class="mini"><thead><tr><th>后端</th><th>FPS</th><th>Sim ms</th><th>重叠</th><th>最小间距</th><th>MaxSpeed</th><th>时长</th></tr></thead><tbody><tr><td>RVO2</td><td>30.0</td><td>16.1</td><td>82.2</td><td>-0.268</td><td>2.4</td><td>9.5s</td></tr></tbody></table>
</article>
<article class="compare warn">
<div class="compare-head"><h3>对向穿流 / 500 鱼</h3><div><span class="chip ok">三后端齐全</span><span class="chip warn">速度不一致</span><span class="chip ok">时长充足</span></div></div>
<div class="delta-grid">
<div><span>Velocity vs RVO2 FPS</span><b class="good">+7.1%</b></div>
<div><span>Velocity vs RVO2 重叠</span><b class="bad">+58.4%</b></div>
<div><span>Velocity vs 旧 A Pro FPS</span><b class="flat">+0.8%</b></div>
<div><span>Velocity vs 旧 A Pro 重叠</span><b class="bad">+22.9%</b></div>
</div>
<table class="mini"><thead><tr><th>后端</th><th>FPS</th><th>Sim ms</th><th>重叠</th><th>最小间距</th><th>MaxSpeed</th><th>时长</th></tr></thead><tbody><tr><td>RVO2</td><td>26.4</td><td>26.8</td><td>171.2</td><td>-0.398</td><td>2.4</td><td>20.5s</td></tr><tr><td>A Pro RVO</td><td>28.1</td><td>22.1</td><td>220.8</td><td>-0.459</td><td>2.4</td><td>28.0s</td></tr><tr><td>A Pro VelocityLike</td><td>28.3</td><td>23.4</td><td>271.3</td><td>-0.618</td><td>9.6</td><td>25.0s</td></tr></tbody></table>
</article>
<article class="compare ok">
<div class="compare-head"><h3>圆阵高压 / 1000 鱼</h3><div><span class="chip ok">三后端齐全</span><span class="chip ok">速度一致</span><span class="chip ok">时长充足</span></div></div>
<div class="delta-grid">
<div><span>Velocity vs RVO2 FPS</span><b class="good">+5.5%</b></div>
<div><span>Velocity vs RVO2 重叠</span><b class="bad">+30.1%</b></div>
<div><span>Velocity vs 旧 A Pro FPS</span><b class="flat">+1.6%</b></div>
<div><span>Velocity vs 旧 A Pro 重叠</span><b class="flat">-0.4%</b></div>
</div>
<table class="mini"><thead><tr><th>后端</th><th>FPS</th><th>Sim ms</th><th>重叠</th><th>最小间距</th><th>MaxSpeed</th><th>时长</th></tr></thead><tbody><tr><td>RVO2</td><td>13.4</td><td>56.7</td><td>1002.1</td><td>-0.688</td><td>9.6</td><td>9.5s</td></tr><tr><td>A Pro RVO</td><td>13.9</td><td>56.1</td><td>1308.8</td><td>-0.697</td><td>9.6</td><td>25.1s</td></tr><tr><td>A Pro VelocityLike</td><td>14.1</td><td>53.4</td><td>1304.0</td><td>-0.692</td><td>9.6</td><td>22.5s</td></tr></tbody></table>
</article>
<article class="compare warn">
<div class="compare-head"><h3>圆阵高压 / 300 鱼</h3><div><span class="chip ok">三后端齐全</span><span class="chip ok">速度一致</span><span class="chip warn">时长偏短</span></div></div>
<div class="delta-grid">
<div><span>Velocity vs RVO2 FPS</span><b class="flat">-0.7%</b></div>
<div><span>Velocity vs RVO2 重叠</span><b class="bad">+113.8%</b></div>
<div><span>Velocity vs 旧 A Pro FPS</span><b class="flat">-0.5%</b></div>
<div><span>Velocity vs 旧 A Pro 重叠</span><b class="flat">+0.7%</b></div>
</div>
<table class="mini"><thead><tr><th>后端</th><th>FPS</th><th>Sim ms</th><th>重叠</th><th>最小间距</th><th>MaxSpeed</th><th>时长</th></tr></thead><tbody><tr><td>RVO2</td><td>30.1</td><td>16.7</td><td>159.0</td><td>-0.379</td><td>2.4</td><td>14.5s</td></tr><tr><td>A Pro RVO</td><td>30.0</td><td>12.5</td><td>337.5</td><td>-0.383</td><td>2.4</td><td>8.0s</td></tr><tr><td>A Pro VelocityLike</td><td>29.9</td><td>12.9</td><td>340.0</td><td>-0.382</td><td>2.4</td><td>7.5s</td></tr></tbody></table>
</article>
<article class="compare warn">
<div class="compare-head"><h3>圆阵高压 / 500 鱼</h3><div><span class="chip ok">三后端齐全</span><span class="chip ok">速度一致</span><span class="chip warn">时长偏短</span></div></div>
<div class="delta-grid">
<div><span>Velocity vs RVO2 FPS</span><b class="good">+3.6%</b></div>
<div><span>Velocity vs RVO2 重叠</span><b class="bad">+4.2%</b></div>
<div><span>Velocity vs 旧 A Pro FPS</span><b class="flat">+1.4%</b></div>
<div><span>Velocity vs 旧 A Pro 重叠</span><b class="good">-16.7%</b></div>
</div>
<table class="mini"><thead><tr><th>后端</th><th>FPS</th><th>Sim ms</th><th>重叠</th><th>最小间距</th><th>MaxSpeed</th><th>时长</th></tr></thead><tbody><tr><td>RVO2</td><td>28.3</td><td>22.3</td><td>600.0</td><td>-0.416</td><td>9.6</td><td>2.5s</td></tr><tr><td>A Pro RVO</td><td>28.9</td><td>19.1</td><td>750.0</td><td>-0.416</td><td>9.6</td><td>1.0s</td></tr><tr><td>A Pro VelocityLike</td><td>29.3</td><td>22.8</td><td>625.0</td><td>-0.416</td><td>9.6</td><td>2.0s</td></tr></tbody></table>
</article>
<article class="compare warn">
<div class="compare-head"><h3>圆阵高压 / 800 鱼</h3><div><span class="chip ok">三后端齐全</span><span class="chip ok">速度一致</span><span class="chip warn">时长偏短</span></div></div>
<div class="delta-grid">
<div><span>Velocity vs RVO2 FPS</span><b class="good">+17.6%</b></div>
<div><span>Velocity vs RVO2 重叠</span><b class="bad">+327.0%</b></div>
<div><span>Velocity vs 旧 A Pro FPS</span><b class="bad">-4.3%</b></div>
<div><span>Velocity vs 旧 A Pro 重叠</span><b class="flat">+0.0%</b></div>
</div>
<table class="mini"><thead><tr><th>后端</th><th>FPS</th><th>Sim ms</th><th>重叠</th><th>最小间距</th><th>MaxSpeed</th><th>时长</th></tr></thead><tbody><tr><td>RVO2</td><td>15.1</td><td>77.9</td><td>562.1</td><td>-0.697</td><td>9.6</td><td>9.5s</td></tr><tr><td>A Pro RVO</td><td>18.6</td><td>39.8</td><td>2400.0</td><td>-0.511</td><td>9.6</td><td>2.5s</td></tr><tr><td>A Pro VelocityLike</td><td>17.8</td><td>35.8</td><td>2400.0</td><td>-0.521</td><td>9.6</td><td>4.5s</td></tr></tbody></table>
</article></div></section>
<section class="section" id="raw"><h2>完整汇总表</h2><div class="table-wrap"><table><thead><tr><th>场景</th><th>鱼数</th><th>后端</th><th>MaxSpeed</th><th>样本</th><th>时长</th><th>FPS</th><th>1% Low</th><th>Sim ms</th><th>重叠</th><th>最小间距</th><th>均速</th><th>均加速</th></tr></thead><tbody><tr><td>鱼饵争抢</td><td>300</td><td>RVO2</td><td>4.2</td><td>50</td><td>25.0s</td><td>29.0</td><td>17.6</td><td>16.8</td><td>125.3</td><td>-0.624</td><td>1.9</td><td>4.0</td></tr>
<tr><td>鱼饵争抢</td><td>300</td><td>A Pro RVO</td><td>4.2</td><td>41</td><td>20.5s</td><td>29.3</td><td>13.9</td><td>13.9</td><td>142.1</td><td>-0.625</td><td>1.8</td><td>3.9</td></tr>
<tr><td>鱼饵争抢</td><td>300</td><td>A Pro VelocityLike</td><td>4.2</td><td>42</td><td>21.0s</td><td>29.9</td><td>20.5</td><td>13.2</td><td>137.7</td><td>-0.141</td><td>1.8</td><td>3.4</td></tr>
<tr><td>鱼饵争抢</td><td>500</td><td>RVO2</td><td>16.8</td><td>16</td><td>8.0s</td><td>26.8</td><td>8.5</td><td>27.3</td><td>277.1</td><td>-0.682</td><td>10.5</td><td>54.1</td></tr>
<tr><td>鱼饵争抢</td><td>500</td><td>A Pro RVO</td><td>16.8</td><td>27</td><td>13.5s</td><td>26.9</td><td>11.3</td><td>24.6</td><td>542.2</td><td>-0.684</td><td>5.3</td><td>23.0</td></tr>
<tr><td>鱼饵争抢</td><td>500</td><td>A Pro VelocityLike</td><td>16.8</td><td>55</td><td>27.5s</td><td>27.7</td><td>13.7</td><td>23.6</td><td>536.9</td><td>-0.678</td><td>5.1</td><td>21.2</td></tr>
<tr><td>鱼饵争抢</td><td>800</td><td>RVO2</td><td>16.8</td><td>21</td><td>10.5s</td><td>16.8</td><td>6.1</td><td>47.5</td><td>640.1</td><td>-0.694</td><td>9.4</td><td>35.3</td></tr>
<tr><td>鱼饵争抢</td><td>800</td><td>A Pro RVO</td><td>16.8</td><td>30</td><td>15.0s</td><td>18.5</td><td>5.8</td><td>39.9</td><td>1040.4</td><td>-0.697</td><td>5.6</td><td>26.7</td></tr>
<tr><td>鱼饵争抢</td><td>800</td><td>A Pro VelocityLike</td><td>16.8</td><td>35</td><td>17.5s</td><td>20.6</td><td>6.5</td><td>39.9</td><td>1073.2</td><td>-0.694</td><td>5.4</td><td>24.6</td></tr>
<tr><td>鱼饵争抢</td><td>1000</td><td>A Pro RVO</td><td>16.8</td><td>27</td><td>13.6s</td><td>13.4</td><td>4.7</td><td>53.3</td><td>1240.8</td><td>-0.699</td><td>6.0</td><td>26.9</td></tr>
<tr><td>鱼饵争抢</td><td>1000</td><td>A Pro VelocityLike</td><td>16.8</td><td>41</td><td>20.5s</td><td>13.7</td><td>5.4</td><td>55.0</td><td>1193.7</td><td>-0.697</td><td>6.0</td><td>26.9</td></tr>
<tr><td>对向穿流</td><td>300</td><td>RVO2</td><td>2.4</td><td>19</td><td>9.5s</td><td>30.0</td><td>21.2</td><td>16.1</td><td>82.2</td><td>-0.268</td><td>1.8</td><td>1.9</td></tr>
<tr><td>对向穿流</td><td>500</td><td>RVO2</td><td>2.4</td><td>41</td><td>20.5s</td><td>26.4</td><td>11.8</td><td>26.8</td><td>171.2</td><td>-0.398</td><td>1.5</td><td>1.3</td></tr>
<tr><td>对向穿流</td><td>500</td><td>A Pro RVO</td><td>2.4</td><td>56</td><td>28.0s</td><td>28.1</td><td>14.2</td><td>22.1</td><td>220.8</td><td>-0.459</td><td>1.4</td><td>1.1</td></tr>
<tr><td>对向穿流</td><td>500</td><td>A Pro VelocityLike</td><td>9.6</td><td>50</td><td>25.0s</td><td>28.3</td><td>14.1</td><td>23.4</td><td>271.3</td><td>-0.618</td><td>2.4</td><td>5.7</td></tr>
<tr><td>圆阵高压</td><td>300</td><td>RVO2</td><td>2.4</td><td>29</td><td>14.5s</td><td>30.1</td><td>24.1</td><td>16.7</td><td>159.0</td><td>-0.379</td><td>1.5</td><td>1.7</td></tr>
<tr><td>圆阵高压</td><td>300</td><td>A Pro RVO</td><td>2.4</td><td>16</td><td>8.0s</td><td>30.0</td><td>20.7</td><td>12.5</td><td>337.5</td><td>-0.383</td><td>1.6</td><td>0.0</td></tr>
<tr><td>圆阵高压</td><td>300</td><td>A Pro VelocityLike</td><td>2.4</td><td>15</td><td>7.5s</td><td>29.9</td><td>16.9</td><td>12.9</td><td>340.0</td><td>-0.382</td><td>1.8</td><td>0.0</td></tr>
<tr><td>圆阵高压</td><td>500</td><td>RVO2</td><td>9.6</td><td>5</td><td>2.5s</td><td>28.3</td><td>7.5</td><td>22.3</td><td>600.0</td><td>-0.416</td><td>9.6</td><td>0.0</td></tr>
<tr><td>圆阵高压</td><td>500</td><td>A Pro RVO</td><td>9.6</td><td>2</td><td>1.0s</td><td>28.9</td><td>9.5</td><td>19.1</td><td>750.0</td><td>-0.416</td><td>4.8</td><td>0.0</td></tr>
<tr><td>圆阵高压</td><td>500</td><td>A Pro VelocityLike</td><td>9.6</td><td>4</td><td>2.0s</td><td>29.3</td><td>12.9</td><td>22.8</td><td>625.0</td><td>-0.416</td><td>2.4</td><td>0.0</td></tr>
<tr><td>圆阵高压</td><td>800</td><td>RVO2</td><td>9.6</td><td>19</td><td>9.5s</td><td>15.1</td><td>4.4</td><td>77.9</td><td>562.1</td><td>-0.697</td><td>6.9</td><td>13.8</td></tr>
<tr><td>圆阵高压</td><td>800</td><td>A Pro RVO</td><td>9.6</td><td>5</td><td>2.5s</td><td>18.6</td><td>5.8</td><td>39.8</td><td>2400.0</td><td>-0.511</td><td>1.9</td><td>0.1</td></tr>
<tr><td>圆阵高压</td><td>800</td><td>A Pro VelocityLike</td><td>9.6</td><td>9</td><td>4.5s</td><td>17.8</td><td>6.7</td><td>35.8</td><td>2400.0</td><td>-0.521</td><td>1.1</td><td>0.0</td></tr>
<tr><td>圆阵高压</td><td>1000</td><td>RVO2</td><td>9.6</td><td>19</td><td>9.5s</td><td>13.4</td><td>3.2</td><td>56.7</td><td>1002.1</td><td>-0.688</td><td>7.2</td><td>11.5</td></tr>
<tr><td>圆阵高压</td><td>1000</td><td>A Pro RVO</td><td>9.6</td><td>50</td><td>25.1s</td><td>13.9</td><td>5.5</td><td>56.1</td><td>1308.8</td><td>-0.697</td><td>2.8</td><td>7.6</td></tr>
<tr><td>圆阵高压</td><td>1000</td><td>A Pro VelocityLike</td><td>9.6</td><td>45</td><td>22.5s</td><td>14.1</td><td>4.6</td><td>53.4</td><td>1304.0</td><td>-0.692</td><td>2.9</td><td>8.2</td></tr></tbody></table></div></section>
<section class="section" id="next"><h2>下一轮测试建议</h2><div class="callout"><ul><li>固定速度倍率为 1x 后重新跑 CrossFlow 500因为这一组 VelocityLike 的 MaxSpeed 与另外两个后端不一致。</li><li>Dense 500 / Dense 800 每个后端至少跑到 20 秒;当前几组只有 1-5 秒,重叠和 1% Low 都容易被启动阶段支配。</li><li>BaitScramble 建议保留 300、500、800、1000 四档,但同一档必须连续跑完三后端,不要中途改速度倍率。</li><li>如果目标是判断 A Pro 能不能用,首要看 VelocityLike 在 300/500 鱼下是否同时满足 FPS 更高、最小间距不恶化、重叠不显著高于 RVO2。</li></ul></div></section>
</main>
</div>
</body>
</html>

View File

@@ -0,0 +1,562 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light">
<title>FishROV A Pro RVO vs RVO2 Benchmark 报告</title>
<style>
:root {
--ink: #172033;
--muted: #667085;
--paper: #ffffff;
--canvas: #eef2f8;
--line: #dce3ee;
--navy: #102a43;
--blue: #2f6fed;
--blue-soft: #eaf1ff;
--cyan: #13a8a8;
--cyan-soft: #e6fafa;
--green: #138a5b;
--green-soft: #e7f7ef;
--orange: #e77817;
--orange-soft: #fff2e5;
--red: #c73e4d;
--red-soft: #ffeaed;
--shadow: 0 14px 40px rgba(16, 42, 67, .09);
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
color: var(--ink);
background:
radial-gradient(circle at 8% 0%, rgba(47, 111, 237, .10), transparent 28rem),
linear-gradient(180deg, #f7f9fc 0, var(--canvas) 100%);
font: 14px/1.7 -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
}
a { color: inherit; }
.shell {
display: grid;
grid-template-columns: 248px minmax(0, 1180px);
gap: 26px;
width: min(1480px, calc(100% - 32px));
margin: 24px auto 64px;
align-items: start;
}
.side {
position: sticky;
top: 20px;
min-width: 0;
padding: 20px 16px;
border: 1px solid var(--line);
border-radius: 16px;
background: rgba(255, 255, 255, .94);
box-shadow: var(--shadow);
backdrop-filter: blur(10px);
}
.brand {
display: flex;
gap: 11px;
align-items: center;
padding: 2px 5px 17px;
border-bottom: 1px solid var(--line);
}
.brand-mark {
display: grid;
width: 38px;
height: 38px;
place-items: center;
border-radius: 11px;
color: #fff;
background: linear-gradient(145deg, var(--blue), #1744ad);
font-size: 18px;
font-weight: 800;
box-shadow: 0 8px 16px rgba(47, 111, 237, .28);
}
.brand strong { display: block; font-size: 15px; }
.brand span { display: block; color: var(--muted); font-size: 11px; }
.side nav { margin-top: 14px; }
.side nav a {
display: block;
padding: 7px 10px;
border-radius: 8px;
color: #46536a;
text-decoration: none;
font-size: 13px;
}
.side nav a:hover { color: var(--blue); background: var(--blue-soft); }
.side-note {
margin: 15px 5px 0;
padding-top: 14px;
border-top: 1px solid var(--line);
color: var(--muted);
font-size: 11px;
}
main { min-width: 0; }
.hero, .section {
border: 1px solid var(--line);
border-radius: 18px;
background: var(--paper);
box-shadow: var(--shadow);
}
.hero {
position: relative;
overflow: hidden;
padding: 38px 40px 34px;
color: #fff;
background:
linear-gradient(115deg, rgba(16, 42, 67, .98), rgba(23, 64, 112, .96) 56%, rgba(47, 111, 237, .92)),
#102a43;
}
.hero::after {
position: absolute;
right: -55px;
bottom: -115px;
width: 330px;
height: 330px;
border: 45px solid rgba(255, 255, 255, .055);
border-radius: 50%;
content: "";
}
.eyebrow {
margin: 0 0 10px;
color: #a8c8ff;
font-size: 12px;
font-weight: 750;
letter-spacing: .13em;
text-transform: uppercase;
}
h1 {
max-width: 860px;
margin: 0;
font-size: clamp(30px, 5vw, 48px);
line-height: 1.18;
letter-spacing: -.025em;
}
.lead {
max-width: 860px;
margin: 16px 0 0;
color: #d8e6fb;
font-size: 16px;
}
.hero-meta, .summary-grid, .grid-2, .grid-3 {
display: grid;
gap: 14px;
}
.hero-meta {
grid-template-columns: repeat(4, minmax(0, 1fr));
margin-top: 28px;
}
.hero-meta div {
padding: 12px 14px;
border: 1px solid rgba(255, 255, 255, .13);
border-radius: 11px;
background: rgba(255, 255, 255, .07);
}
.hero-meta span { display: block; color: #a9bdd8; font-size: 11px; }
.hero-meta strong { display: block; margin-top: 2px; font-size: 14px; }
.section {
margin-top: 20px;
padding: 28px 30px;
scroll-margin-top: 24px;
}
.section-head {
display: flex;
gap: 20px;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 20px;
}
.kicker {
margin: 0 0 3px;
color: var(--blue);
font-size: 11px;
font-weight: 800;
letter-spacing: .12em;
text-transform: uppercase;
}
h2 { margin: 0; color: var(--navy); font-size: 24px; line-height: 1.3; }
h3 { margin: 0 0 10px; color: var(--navy); font-size: 16px; }
p { margin: 8px 0; }
.muted { color: var(--muted); }
.summary-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.metric {
padding: 17px;
border: 1px solid var(--line);
border-radius: 13px;
background: #fbfcfe;
}
.metric b { display: block; color: var(--blue); font-size: 27px; line-height: 1.2; }
.metric span { display: block; margin-top: 6px; color: var(--muted); font-size: 12px; }
.grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid-2 > *, .grid-3 > * { min-width: 0; }
.info-card, .compare-card {
padding: 17px 18px;
border: 1px solid var(--line);
border-radius: 13px;
background: #fbfcfe;
}
.compare-card .rule {
margin-bottom: 13px;
padding-bottom: 11px;
border-bottom: 1px solid var(--line);
color: var(--muted);
font-size: 12px;
}
.compare-card .rule strong { color: var(--navy); }
.tag {
display: inline-block;
margin: 2px 5px 2px 0;
padding: 3px 9px;
border-radius: 999px;
color: #31507d;
background: var(--blue-soft);
font-size: 11px;
font-weight: 700;
}
.tag.green { color: var(--green); background: var(--green-soft); }
.tag.orange { color: #9a4e05; background: var(--orange-soft); }
.tag.red { color: var(--red); background: var(--red-soft); }
.tag.cyan { color: #087273; background: var(--cyan-soft); }
.callout {
margin: 17px 0 0;
padding: 14px 16px;
border-left: 4px solid var(--blue);
border-radius: 9px;
background: var(--blue-soft);
}
.callout.warn { border-color: var(--orange); background: var(--orange-soft); }
.callout.bad { border-color: var(--red); background: var(--red-soft); }
.callout.good { border-color: var(--green); background: var(--green-soft); }
table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
th, td {
padding: 10px 9px;
border-bottom: 1px solid var(--line);
text-align: right;
vertical-align: top;
white-space: nowrap;
font-variant-numeric: tabular-nums;
}
th {
color: #31507d;
background: #f3f7fd;
font-size: 12px;
font-weight: 800;
}
th:first-child, td:first-child,
th:nth-child(2), td:nth-child(2) { text-align: left; }
.wide-table { overflow-x: auto; }
.score {
display: inline-flex;
min-width: 68px;
justify-content: center;
padding: 2px 8px;
border-radius: 999px;
font-size: 12px;
font-weight: 800;
}
.score.good { color: var(--green); background: var(--green-soft); }
.score.warn { color: #9a4e05; background: var(--orange-soft); }
.score.bad { color: var(--red); background: var(--red-soft); }
.good-text { color: var(--green); font-weight: 800; }
.warn-text { color: var(--orange); font-weight: 800; }
.bad-text { color: var(--red); font-weight: 800; }
.bar-list {
display: grid;
gap: 10px;
margin-top: 12px;
}
.bar-row {
display: grid;
grid-template-columns: 110px 1fr 78px;
gap: 10px;
align-items: center;
font-size: 12px;
font-weight: 750;
font-variant-numeric: tabular-nums;
}
.track {
height: 14px;
border-radius: 999px;
background: #e8eef7;
overflow: hidden;
}
.fill {
height: 100%;
border-radius: 999px;
background: var(--blue);
}
.fill.green { background: var(--green); }
.fill.orange { background: var(--orange); }
.fill.red { background: var(--red); }
.foot {
margin-top: 18px;
color: var(--muted);
font-size: 12px;
}
@media (max-width: 1080px) {
.shell { grid-template-columns: 1fr; }
.side { position: static; }
.side nav { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); }
.hero-meta, .summary-grid, .grid-2, .grid-3 { grid-template-columns: 1fr; }
.section-head { display: block; }
}
@media (max-width: 640px) {
.shell { width: min(100% - 20px, 1480px); margin-top: 10px; }
.hero, .section { border-radius: 14px; }
.hero { padding: 28px 22px; }
.section { padding: 22px 18px; }
.side nav { grid-template-columns: 1fr; }
.bar-row { grid-template-columns: 90px 1fr 64px; }
}
</style>
</head>
<body>
<div class="shell">
<aside class="side">
<div class="brand">
<div class="brand-mark">AP</div>
<div>
<strong>A Pro RVO 报告</strong>
<span>固定 XZ2D / 鱼饵场景纳入</span>
</div>
</div>
<nav>
<a href="#summary">结论摘要</a>
<a href="#android">百元机A Pro vs RVO2</a>
<a href="#bait">鱼饵场景</a>
<a href="#editor">编辑器趋势</a>
<a href="#env">跨环境差异</a>
<a href="#method">样本与口径</a>
<a href="#actions">建议动作</a>
</nav>
<div class="side-note">
数据源C:\Users\13999\Desktop\logs。统计稳定段排除 timeSeconds = 0 的启动采样。
</div>
</aside>
<main>
<section class="hero">
<p class="eyebrow">2026-06-15 / Android + Editor / XZ2D mainline</p>
<h1>A Pro RVO 提升性能,但在 2D 下常用重叠换速度</h1>
<p class="lead">这批新日志固定了大部分 2D 主线,新增了 BaitScramble 鱼饵场景。结论不是“A Pro 全面替代 RVO2”它在百元机 500 鱼附近明显改善 FPS 和 simulation ms但鱼饵、FreeSwim、Dense 的空间重叠通常更高。</p>
<div class="hero-meta">
<div><span>日志数量</span><strong>69 个 CSV</strong></div>
<div><span>主环境</span><strong>Android 百元机</strong></div>
<div><span>主维度</span><strong>XZ2D</strong></div>
<div><span>新增场景</span><strong>BaitScramble</strong></div>
</div>
</section>
<section id="summary" class="section">
<div class="section-head">
<div>
<p class="kicker">Executive Summary</p>
<h2>结论摘要</h2>
</div>
<p class="muted">这次应该把“帧率胜利”和“避障质量胜利”分开看。</p>
</div>
<div class="summary-grid">
<div class="metric"><b>500</b><span>A Pro 的价值最明显:百元机 500 鱼在 FreeSwim / CrossFlow / Dense / Bait 都显著提升 FPS。</span></div>
<div class="metric"><b>+306</b><span>鱼饵 500 鱼 A Pro 比 RVO2 多 306.3 个平均空间重叠对,性能提升伴随拥挤代价。</span></div>
<div class="metric"><b>Cross 500</b><span>百元机 CrossFlow 500 是 A Pro 最像“真胜利”的样本FPS 28.8 vs 11.4,空间重叠 288.7 vs 512.0。</span></div>
<div class="metric"><b>800+</b><span>A Pro 到 800/1000 后重叠经常爆高,不能直接作为高数量默认方案。</span></div>
</div>
<div class="callout warn">
<strong>推荐判断:</strong>A Pro 可以继续作为性能候选,但现在还不建议无条件替代 RVO2。鱼饵场景要先解决聚集重叠或增加后处理分离再谈默认启用。
</div>
</section>
<section id="android" class="section">
<div class="section-head">
<div>
<p class="kicker">Android / Same Config</p>
<h2>百元机A Pro vs RVO2</h2>
</div>
<p class="muted">固定环境、场景、维度和数量,只比较模式。负数 Sim Δ 表示 A Pro 更快;正数重叠 Δ 表示 A Pro 更拥挤。</p>
</div>
<div class="wide-table compare-card">
<div class="rule"><strong>固定:</strong>Android / XZ2D / 同场景 / 同数量。<strong>唯一变量:</strong>RVO2 vs A Pro RVO。</div>
<table>
<thead>
<tr>
<th>场景</th>
<th>数量</th>
<th>FPS RVO2</th>
<th>FPS A Pro</th>
<th>FPS Δ</th>
<th>Sim Δ ms</th>
<th>空间重叠 RVO2</th>
<th>空间重叠 A Pro</th>
<th>重叠 Δ</th>
<th>结论</th>
</tr>
</thead>
<tbody>
<tr><td>FreeSwim</td><td>100</td><td>22.0</td><td>30.2</td><td class="good-text">+8.2</td><td class="warn-text">+0.26</td><td>0.4</td><td>0.4</td><td>0.0</td><td><span class="score good">可用</span></td></tr>
<tr><td>FreeSwim</td><td>300</td><td>22.5</td><td>30.0</td><td class="good-text">+7.5</td><td class="good-text">-7.08</td><td>16.1</td><td>17.9</td><td class="warn-text">+1.8</td><td><span class="score good">小赢</span></td></tr>
<tr><td>FreeSwim</td><td>500</td><td>11.9</td><td>28.3</td><td class="good-text">+16.4</td><td class="good-text">-22.89</td><td>89.1</td><td>268.0</td><td class="bad-text">+178.9</td><td><span class="score warn">性能赢</span></td></tr>
<tr><td>FreeSwim</td><td>800</td><td>19.0</td><td>21.7</td><td class="good-text">+2.7</td><td class="good-text">-17.21</td><td>293.1</td><td>844.3</td><td class="bad-text">+551.2</td><td><span class="score bad">质量差</span></td></tr>
<tr><td>CrossFlow</td><td>500</td><td>11.4</td><td>28.8</td><td class="good-text">+17.4</td><td class="good-text">-50.76</td><td>512.0</td><td>288.7</td><td class="good-text">-223.3</td><td><span class="score good">真胜利</span></td></tr>
<tr><td>Dense</td><td>500</td><td>13.0</td><td>27.9</td><td class="good-text">+14.9</td><td class="good-text">-37.78</td><td>625.0</td><td>545.5</td><td class="good-text">-79.5</td><td><span class="score good">可试</span></td></tr>
<tr><td>Dense</td><td>800</td><td>13.9</td><td>19.1</td><td class="good-text">+5.2</td><td class="good-text">-35.02</td><td>525.7</td><td>2400.0</td><td class="bad-text">+1874.3</td><td><span class="score bad">重叠爆</span></td></tr>
<tr><td>BaitScramble</td><td>500</td><td>16.5</td><td>28.2</td><td class="good-text">+11.7</td><td class="good-text">-41.27</td><td>209.9</td><td>516.2</td><td class="bad-text">+306.3</td><td><span class="score warn">性能赢</span></td></tr>
<tr><td>BaitScramble</td><td>800</td><td>12.9</td><td>18.8</td><td class="good-text">+5.9</td><td class="good-text">-8.02</td><td>616.0</td><td>1031.9</td><td class="bad-text">+415.9</td><td><span class="score bad">质量差</span></td></tr>
<tr><td>BaitScramble</td><td>1000</td><td>14.0</td><td>15.5</td><td class="good-text">+1.5</td><td class="good-text">-9.80</td><td>866.9</td><td>1208.8</td><td class="bad-text">+341.9</td><td><span class="score bad">不建议</span></td></tr>
</tbody>
</table>
</div>
</section>
<section id="bait" class="section">
<div class="section-head">
<div>
<p class="kicker">BaitScramble</p>
<h2>鱼饵场景是关键A Pro 帧率更好,但聚集更挤</h2>
</div>
<p class="muted">鱼饵场景更接近真实玩法,所以它的质量风险比 FreeSwim 更值得优先处理。</p>
</div>
<div class="grid-2">
<article class="compare-card">
<h3>Android / BaitScramble / XZ2D</h3>
<div class="rule"><strong>固定:</strong>Android / BaitScramble / XZ2D。<strong>变量:</strong>数量与模式。</div>
<table>
<thead><tr><th>数量</th><th>模式</th><th>FPS</th><th>1% Low</th><th>Sim ms</th><th>空间重叠</th><th>最小间距</th></tr></thead>
<tbody>
<tr><td>100</td><td>RVO2</td><td>30.2</td><td>29.1</td><td>7.50</td><td>12.8</td><td>-0.610</td></tr>
<tr><td>100</td><td>A Pro</td><td>29.1</td><td>7.5</td><td>6.15</td><td>18.2</td><td>-0.649</td></tr>
<tr><td>300</td><td>RVO2</td><td>26.5</td><td>8.8</td><td>33.41</td><td>122.2</td><td>-0.670</td></tr>
<tr><td>300</td><td>A Pro</td><td>30.1</td><td>24.8</td><td>15.62</td><td>235.9</td><td>-0.674</td></tr>
<tr><td>500</td><td>RVO2</td><td>16.5</td><td>3.7</td><td>65.92</td><td>209.9</td><td>-0.694</td></tr>
<tr><td>500</td><td>A Pro</td><td>28.2</td><td>12.8</td><td>24.65</td><td>516.2</td><td>-0.686</td></tr>
<tr><td>800</td><td>RVO2</td><td>12.9</td><td>3.4</td><td>52.27</td><td>616.0</td><td>-0.696</td></tr>
<tr><td>800</td><td>A Pro</td><td>18.8</td><td>6.0</td><td>44.25</td><td>1031.9</td><td>-0.692</td></tr>
<tr><td>1000</td><td>RVO2</td><td>14.0</td><td>4.6</td><td>72.77</td><td>866.9</td><td>-0.697</td></tr>
<tr><td>1000</td><td>A Pro</td><td>15.5</td><td>5.4</td><td>62.97</td><td>1208.8</td><td>-0.692</td></tr>
</tbody>
</table>
</article>
<article class="compare-card">
<h3>鱼饵场景读法</h3>
<div class="rule"><strong>业务解释:</strong>鱼饵会制造局部聚集,空间重叠比单纯 FPS 更关键。</div>
<div class="bar-list">
<div class="bar-row"><span>300 FPS</span><div class="track"><div class="fill green" style="width:100%"></div></div><strong>A Pro</strong></div>
<div class="bar-row"><span>300 重叠</span><div class="track"><div class="fill orange" style="width:52%"></div></div><strong>RVO2</strong></div>
<div class="bar-row"><span>500 FPS</span><div class="track"><div class="fill green" style="width:100%"></div></div><strong>A Pro</strong></div>
<div class="bar-row"><span>500 重叠</span><div class="track"><div class="fill red" style="width:100%"></div></div><strong>RVO2</strong></div>
<div class="bar-row"><span>800 FPS</span><div class="track"><div class="fill orange" style="width:67%"></div></div><strong>A Pro</strong></div>
<div class="bar-row"><span>800 重叠</span><div class="track"><div class="fill red" style="width:100%"></div></div><strong>RVO2</strong></div>
</div>
<div class="callout bad">
<strong>当前结论:</strong>A Pro 更能把鱼饵场景跑起来,但 300/500/800/1000 全部比 RVO2 更拥挤。它需要局部密度限制、到达鱼饵后的分层/环绕点、或重叠后处理。
</div>
</article>
</div>
</section>
<section id="editor" class="section">
<div class="section-head">
<div>
<p class="kicker">Editor Trend</p>
<h2>编辑器趋势A Pro 在大数量下也不是质量胜利</h2>
</div>
<p class="muted">编辑器只看趋势,不当移动端上线依据。</p>
</div>
<div class="wide-table compare-card">
<div class="rule"><strong>固定:</strong>Editor / XZ2D / 同场景 / 同数量。<strong>唯一变量:</strong>RVO2 vs A Pro。</div>
<table>
<thead><tr><th>场景</th><th>数量</th><th>FPS RVO2</th><th>FPS A Pro</th><th>Sim Δ ms</th><th>空间重叠 RVO2</th><th>空间重叠 A Pro</th><th>结论</th></tr></thead>
<tbody>
<tr><td>FreeSwim</td><td>300</td><td>99.3</td><td>100.9</td><td class="good-text">-0.22</td><td>6.0</td><td>5.8</td><td><span class="score good">基本持平</span></td></tr>
<tr><td>FreeSwim</td><td>3000</td><td>28.4</td><td>26.7</td><td class="good-text">-0.29</td><td>4222.1</td><td>4898.8</td><td><span class="score bad">更拥挤</span></td></tr>
<tr><td>BaitScramble</td><td>300</td><td>98.5</td><td>105.6</td><td class="good-text">-0.24</td><td>150.9</td><td>166.1</td><td><span class="score warn">小幅更挤</span></td></tr>
<tr><td>BaitScramble</td><td>3000</td><td>30.3</td><td>24.8</td><td class="bad-text">+2.25</td><td>4955.2</td><td>7677.1</td><td><span class="score bad">双输</span></td></tr>
<tr><td>CrossFlow</td><td>3000</td><td>28.0</td><td>26.9</td><td class="bad-text">+0.77</td><td>3906.3</td><td>6149.5</td><td><span class="score bad">更拥挤</span></td></tr>
<tr><td>Dense</td><td>3000</td><td>25.8</td><td>26.0</td><td class="bad-text">+3.30</td><td>4598.3</td><td>6223.0</td><td><span class="score bad">质量差</span></td></tr>
</tbody>
</table>
</div>
</section>
<section id="env" class="section">
<div class="section-head">
<div>
<p class="kicker">Environment Gap</p>
<h2>跨环境差异:编辑器仍会掩盖百元机红线</h2>
</div>
<p class="muted">固定同模式 / 同场景 / XZ2D / 同数量,只改变运行环境。</p>
</div>
<div class="wide-table compare-card">
<div class="rule"><strong>固定:</strong>RVO2 或 A Pro / XZ2D / 同场景 / 同数量。<strong>唯一变量:</strong>Editor vs Android。</div>
<table>
<thead><tr><th>配置</th><th>Editor FPS</th><th>Android FPS</th><th>Editor Sim</th><th>Android Sim</th><th>空间重叠 Editor / Android</th><th>读法</th></tr></thead>
<tbody>
<tr><td>RVO2 / FreeSwim / 300</td><td>99.3</td><td>22.5</td><td>2.08</td><td>21.36</td><td>6.0 / 16.1</td><td>Android 才暴露性能压力</td></tr>
<tr><td>RVO2 / BaitScramble / 300</td><td>98.5</td><td>26.5</td><td>2.25</td><td>33.41</td><td>150.9 / 122.2</td><td>鱼饵在 Android 上 sim 成本高</td></tr>
<tr><td>A Pro / FreeSwim / 300</td><td>100.9</td><td>30.0</td><td>1.86</td><td>14.28</td><td>5.8 / 17.9</td><td>帧率达标但质量变差</td></tr>
<tr><td>A Pro / BaitScramble / 300</td><td>105.6</td><td>30.1</td><td>2.01</td><td>15.62</td><td>166.1 / 235.9</td><td>鱼饵聚集在 Android 上更挤</td></tr>
</tbody>
</table>
</div>
</section>
<section id="method" class="section">
<div class="section-head">
<div>
<p class="kicker">Method</p>
<h2>样本与统计口径</h2>
</div>
</div>
<div class="grid-3">
<div class="info-card">
<h3>字段</h3>
<p><span class="tag">averageFps</span><span class="tag">onePercentLowFps</span><span class="tag">simulationMs</span><span class="tag">spatialOverlapPairs</span></p>
<p class="muted">新日志还包含 averageSpeed / averageAcceleration可用于观察鱼饵聚集是否靠剧烈运动换效果。</p>
</div>
<div class="info-card">
<h3>环境</h3>
<p><span class="tag green">Android</span><span class="tag cyan">Editor</span></p>
<p class="muted">Android 是主依据Editor 只看趋势和粗略上限。</p>
</div>
<div class="info-card">
<h3>限制</h3>
<p><span class="tag orange">单次样本较多</span><span class="tag red">部分配置缺失</span></p>
<p class="muted">建议关键配置重复跑 3 次取中位数,尤其是 Android / BaitScramble / 300-800。</p>
</div>
</div>
</section>
<section id="actions" class="section">
<div class="section-head">
<div>
<p class="kicker">Next Actions</p>
<h2>建议动作</h2>
</div>
</div>
<div class="grid-3">
<div class="info-card">
<h3>不要直接替换</h3>
<p>A Pro 先不要全局替换 RVO2。它当前在鱼饵场景的质量代价太明显尤其 500 鱼以上。</p>
</div>
<div class="info-card">
<h3>优先修鱼饵聚集</h3>
<p>给鱼饵周围加分布点、局部容量、停靠环、或重叠后处理。A Pro 的性能余量可以用来换更好的分散策略。</p>
</div>
<div class="info-card">
<h3>下一轮固定样本</h3>
<p>Android / XZ2D / BaitScramble 跑 300、500、800每个 RVO2 与 A Pro 各 3 次,加入到达鱼饵时间和鱼饵半径内拥挤度。</p>
</div>
</div>
<p class="foot">生成时间2026-06-15。数据源目录C:\Users\13999\Desktop\logs。</p>
</section>
</main>
</div>
</body>
</html>

View File

@@ -1,631 +0,0 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>RVO2 避障压测方案收束</title>
<style>
:root {
--ink: #1d2528;
--muted: #667175;
--paper: #f5f1e8;
--line: #d8d0c1;
--green: #1f6f62;
--blue: #255f85;
--red: #a64138;
--gold: #a36d18;
--dark: #233033;
--soft: rgba(255, 255, 255, 0.64);
--shadow: 0 18px 60px rgba(35, 48, 51, 0.14);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
color: var(--ink);
background:
linear-gradient(90deg, rgba(31, 111, 98, 0.08) 1px, transparent 1px),
linear-gradient(180deg, rgba(31, 111, 98, 0.06) 1px, transparent 1px),
var(--paper);
background-size: 34px 34px;
font-family: "Microsoft YaHei", "Noto Sans CJK SC", "PingFang SC", sans-serif;
line-height: 1.65;
}
header {
padding: 56px clamp(20px, 5vw, 72px) 30px;
border-bottom: 1px solid var(--line);
background: linear-gradient(135deg, rgba(255,255,255,0.72), rgba(221,232,221,0.34));
}
.eyebrow {
display: inline-flex;
align-items: center;
gap: 10px;
color: var(--green);
font-size: 13px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.eyebrow::before {
content: "";
width: 32px;
height: 2px;
background: var(--green);
}
h1 {
max-width: 1060px;
margin: 18px 0 16px;
font-family: Georgia, "Times New Roman", serif;
font-size: clamp(34px, 5vw, 72px);
line-height: 1.02;
letter-spacing: 0;
}
.lead {
max-width: 980px;
margin: 0;
color: var(--dark);
font-size: clamp(17px, 2vw, 22px);
}
main {
padding: 34px clamp(18px, 4vw, 64px) 72px;
}
section {
max-width: 1280px;
margin: 0 auto 42px;
}
h2 {
margin: 0 0 18px;
font-size: clamp(24px, 2.8vw, 38px);
line-height: 1.16;
letter-spacing: 0;
}
h3 {
margin: 0 0 10px;
font-size: 18px;
letter-spacing: 0;
}
.thesis {
display: grid;
grid-template-columns: minmax(240px, 0.86fr) minmax(280px, 1.14fr);
gap: 22px;
align-items: stretch;
}
.verdict {
padding: 28px;
background: var(--dark);
color: #f8f4eb;
box-shadow: var(--shadow);
}
.verdict strong {
color: #f1c66d;
}
.shared {
padding: 26px;
background: rgba(255,255,255,0.72);
border: 1px solid var(--line);
}
.shared ul,
.axis-list {
margin: 0;
padding-left: 20px;
}
.shared li,
.axis-list li {
margin: 8px 0;
}
.matrix-wrap {
overflow-x: auto;
background: rgba(255,255,255,0.7);
border: 1px solid var(--line);
box-shadow: var(--shadow);
}
table {
width: 100%;
min-width: 1080px;
border-collapse: collapse;
background: rgba(255,255,255,0.8);
}
th,
td {
padding: 16px 18px;
border-bottom: 1px solid var(--line);
vertical-align: top;
text-align: left;
font-size: 15px;
}
th {
position: sticky;
top: 0;
z-index: 1;
color: #f8f4eb;
background: var(--dark);
font-size: 14px;
white-space: nowrap;
}
tr:last-child td {
border-bottom: 0;
}
.method {
font-weight: 800;
color: var(--dark);
white-space: nowrap;
}
.tag {
display: inline-block;
margin: 0 6px 6px 0;
padding: 4px 8px;
border: 1px solid currentColor;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
}
.tag-green { color: var(--green); }
.tag-blue { color: var(--blue); }
.tag-red { color: var(--red); }
.tag-gold { color: var(--gold); }
.axis-grid {
display: grid;
grid-template-columns: repeat(3, minmax(240px, 1fr));
gap: 18px;
}
.axis {
padding: 22px;
background: rgba(255,255,255,0.72);
border-top: 6px solid var(--green);
box-shadow: 0 10px 30px rgba(35, 48, 51, 0.09);
}
.axis:nth-child(2) { border-top-color: var(--blue); }
.axis:nth-child(3) { border-top-color: var(--red); }
.axis:nth-child(4) { border-top-color: var(--gold); }
.axis:nth-child(5) { border-top-color: #6e5a93; }
.axis:nth-child(6) { border-top-color: #3f6b2f; }
.axis p {
margin: 0;
color: var(--muted);
}
.lane {
display: grid;
grid-template-columns: 180px 1fr;
gap: 14px;
align-items: start;
padding: 18px 0;
border-bottom: 1px solid var(--line);
}
.lane:last-child {
border-bottom: 0;
}
.lane-title {
font-weight: 800;
color: var(--dark);
}
.bars {
display: grid;
gap: 10px;
}
.bar {
display: grid;
grid-template-columns: 150px 1fr;
gap: 12px;
align-items: center;
font-size: 14px;
}
.bar-track {
height: 12px;
background: rgba(35, 48, 51, 0.12);
border-radius: 999px;
overflow: hidden;
}
.bar-fill {
height: 100%;
background: linear-gradient(90deg, var(--green), var(--gold));
border-radius: inherit;
}
.recommend {
display: grid;
grid-template-columns: repeat(3, minmax(260px, 1fr));
gap: 18px;
}
.scenario {
display: grid;
grid-template-columns: 1.02fr 0.98fr;
gap: 20px;
}
.scenario-card {
padding: 24px;
background: rgba(255,255,255,0.76);
border: 1px solid var(--line);
box-shadow: 0 10px 32px rgba(35, 48, 51, 0.08);
}
.flow {
display: grid;
gap: 12px;
}
.flow-step {
display: grid;
grid-template-columns: 34px 1fr;
gap: 12px;
align-items: start;
}
.num {
display: grid;
place-items: center;
width: 34px;
height: 34px;
color: #f8f4eb;
background: var(--green);
border-radius: 50%;
font-weight: 800;
line-height: 1;
}
.fit-grid {
display: grid;
grid-template-columns: repeat(4, minmax(190px, 1fr));
gap: 14px;
}
.benchmark-grid {
display: grid;
grid-template-columns: 0.9fr 1.1fr;
gap: 20px;
}
.benchmark-card {
padding: 24px;
color: #f8f4eb;
background: var(--dark);
box-shadow: var(--shadow);
}
.benchmark-card h3 {
color: #f1c66d;
}
.benchmark-card p {
margin: 0;
}
.test-list {
padding: 24px;
background: rgba(255,255,255,0.76);
border: 1px solid var(--line);
}
.test-list ol {
margin: 0;
padding-left: 22px;
}
.test-list li {
margin: 8px 0;
}
.fit {
padding: 18px;
min-height: 156px;
background: rgba(255,255,255,0.72);
border-left: 5px solid var(--blue);
}
.fit strong {
display: block;
margin-bottom: 6px;
color: var(--dark);
}
.fit small {
color: var(--muted);
font-size: 13px;
}
.choice {
padding: 22px;
background: var(--soft);
border: 1px solid var(--line);
}
.choice b {
color: var(--red);
}
.footnote {
color: var(--muted);
font-size: 13px;
}
@media (max-width: 900px) {
.thesis,
.scenario,
.benchmark-grid,
.axis-grid,
.recommend {
grid-template-columns: 1fr;
}
.fit-grid {
grid-template-columns: 1fr;
}
.lane {
grid-template-columns: 1fr;
}
.bar {
grid-template-columns: 120px 1fr;
}
}
</style>
</head>
<body>
<header>
<div class="eyebrow">FishROV Avoidance Benchmark</div>
<h1>RVO2 基准与自研版压测收束</h1>
<p class="lead">
当前阶段先忽略复杂玩法状态只压测局部避障算法在理想和极端条件下的表现。主线收束为两项RVO2 原版作为成熟 ORCA 基准,基于 RVO2 思想的自研版作为项目可控方案。
</p>
</header>
<main>
<section class="benchmark-grid" aria-labelledby="benchmark-title">
<div class="benchmark-card">
<h2 id="benchmark-title">压测结论</h2>
<h3>只测两条线就够</h3>
<p>
如果当前目标是忽略实际玩法状态,只验证理想与极端条件下的局部避障性能,那么建议压测对象收束为:
<strong>RVO2 原版</strong><strong>基于 RVO2 思想的自研版</strong>。RVO2 是稳定基准,自研版负责验证项目可控性、可调参空间和性能上限。
</p>
</div>
<div class="test-list">
<h2>推荐压测口径</h2>
<ol>
<li><strong>理想情况:</strong>空旷水域、均匀分布、目标方向一致或轻微交叉,验证基础吞吐和稳定收敛。</li>
<li><strong>中等冲突:</strong>双向穿流、圆阵向心、随机目标切换,验证重叠率、抖动和换边稳定性。</li>
<li><strong>极端情况:</strong>高密度聚集、初始重叠、狭窄通道、所有 agent 争同一目标点,验证退化行为。</li>
<li><strong>规模阶梯:</strong>100、500、1000、3000、5000 agent记录每帧耗时、最大耗时、重叠对、平均速度损失。</li>
<li><strong>移动端口径:</strong>固定竖屏 1080x1920编辑器和百元机使用相同分辨率、数量档、场景和采样窗口。</li>
<li><strong>公平条件:</strong>相同半径、最大速度、timeStep、目标生成、积分方式、邻居查询范围和日志采样策略。</li>
</ol>
</div>
</section>
<section aria-labelledby="log-title">
<h2 id="log-title">日志与真实性要求</h2>
<div class="matrix-wrap">
<table>
<thead>
<tr>
<th>环境</th>
<th>必须输出</th>
<th>不要做</th>
<th>推荐实现</th>
</tr>
</thead>
<tbody>
<tr>
<td class="method">Unity 编辑器</td>
<td>算法、场景、数量、分辨率、平均 FPS、1% Low、模拟耗时、最大耗时、GC、XZ/3D 重叠对。</td>
<td>不要每帧 `Debug.Log`,不要在热路径拼接字符串,不要每帧写文件。</td>
<td>内存环形缓冲 + 固定间隔采样;单轮结束后一次性导出 CSV/JSON并在 Console 只打印摘要。</td>
</tr>
<tr>
<td class="method">百元机真机</td>
<td>同一套指标加设备型号、Unity 图形 API、目标帧率、分辨率 1080x1920、温度/降频观察备注。</td>
<td>不要开详细 HUD、不要高频日志、不要连接调试器长期采样导致额外开销。</td>
<td>Release/Development 分开测;真机默认静默聚合,测试结束或手动按钮导出日志文件。</td>
</tr>
<tr>
<td class="method">性能公平性</td>
<td>日志开销要对两个算法一致,最好能统计采样写入耗时。</td>
<td>不要让某个算法额外输出调试明细,导致对比不公平。</td>
<td>所有算法只写结构化数值样本,统一由 benchmark runner 收集和导出。</td>
</tr>
</tbody>
</table>
</div>
</section>
<section class="thesis" aria-labelledby="summary-title">
<div class="verdict">
<h2 id="summary-title">一句话判断</h2>
<p>
如果当前阶段只做算法压测,<strong>RVO2 原版 + RVO2 思想自研版</strong> 就是最小有效对照组。
其他方案暂时不必进入压测主线,可以作为解释来源或后续扩展参考。
</p>
</div>
<div class="shared">
<h2>共同点</h2>
<ul>
<li>大多数方案都需要上层提供目标或期望速度,然后再做局部修正。</li>
<li>核心输入都离不开位置、速度、半径、邻居范围和预测时间。</li>
<li>都在权衡两个目标:尽量接近期望运动,同时减少未来碰撞风险。</li>
<li>当前压测只关心局部速度修正,不比较全局寻路、地图可达性或复杂玩法状态。</li>
<li>在 FishROV 的 3D/2.5D 场景里,多数方案仍需要明确如何处理 XZ 投影、高度层和真实体积冲突。</li>
</ul>
</div>
</section>
<section aria-labelledby="matrix-title">
<h2 id="matrix-title">核心矩阵</h2>
<div class="matrix-wrap">
<table>
<thead>
<tr>
<th>方案</th>
<th>核心抽象</th>
<th>最终如何选速度</th>
<th>安全性来源</th>
<th>最擅长</th>
<th>主要风险</th>
<th>FishROV 定位</th>
</tr>
</thead>
<tbody>
<tr>
<td class="method">RVO2 / ORCA</td>
<td><span class="tag tag-green">速度空间</span><span class="tag tag-blue">半平面约束</span><br>把每个邻居变成 ORCA 线性约束。</td>
<td>在所有安全半平面内,求最接近期望速度的新速度。</td>
<td>显式几何约束和低维优化,理论性最强。</td>
<td>大规模多动态 agent 的实时局部避障。</td>
<td>不做全局规划2D 投影,参数不当会保守、停滞或碰撞。</td>
<td>最适合作为局部避障工程基准和对照组。</td>
</tr>
<tr>
<td class="method">HRVO</td>
<td><span class="tag tag-green">速度空间</span><span class="tag tag-gold">混合 VO/RVO</span><br>把 VO 与 RVO 的几何边界混合。</td>
<td>排除危险速度,再选择接近期望速度且更稳定的速度。</td>
<td>速度障碍几何,加上减少 reciprocal 振荡的边界设计。</td>
<td>对称对冲、互相避让时减少左右横跳。</td>
<td>仍是局部 2D 方法,不处理地图和动力学约束。</td>
<td>适合作为处理振荡问题的设计参考。</td>
</tr>
<tr>
<td class="method">RVO_Py_MAS</td>
<td><span class="tag tag-green">速度空间</span><span class="tag tag-gold">采样过滤</span><br>把动态体和静态障碍都转成速度禁区。</td>
<td>采样候选速度,先过滤禁区,有安全速度就选最接近期望速度的;没有安全速度则选折中。</td>
<td>禁区过滤加碰撞时间折中,不是严格 ORCA 求解。</td>
<td>学习 RVO 建模方式,快速改造原型。</td>
<td>没有空间索引时规模变贵,采样可能错过好速度。</td>
<td>适合作为可读的 RVO 原理参考,不建议直接当大规模生产方案。</td>
</tr>
<tr>
<td class="method">采样避障学习版</td>
<td><span class="tag tag-gold">候选速度</span><span class="tag tag-red">评分函数</span><br>候选速度由期望速度、偏转、减速、停止等组成。</td>
<td>对每个候选速度计算目标偏差、平滑、预测碰撞、当前重叠、边界等惩罚,选最低分。</td>
<td>启发式风险函数和权重调参,不保证无碰撞。</td>
<td>项目内自研、调参、验证新评分项。</td>
<td>权重依赖场景,高密度可能局部最优或卡住。</td>
<td>最适合作为 FishROV 自研避障算法的实验骨架。</td>
</tr>
<tr>
<td class="method">rl_rvo_nav</td>
<td><span class="tag tag-red">学习策略</span><span class="tag tag-green">RVO 先验</span><br>把 VO/RVO 风险编码进状态和奖励。</td>
<td>PPO actor 根据自身状态和 VO/RVO 序列输出速度增量。</td>
<td>训练经验、RVO 区域奖励、预计碰撞时间塑形。</td>
<td>复杂多机器人场景下学习更灵活的风险权衡。</td>
<td>训练成本高,泛化依赖仿真覆盖,工程可控性较弱。</td>
<td>建议先吸收风险表达和 reward/score 思想,不急着引入完整 RL 链路。</td>
</tr>
</tbody>
</table>
</div>
</section>
<section aria-labelledby="axes-title">
<h2 id="axes-title">真正拉开差距的六个轴</h2>
<div class="axis-grid">
<article class="axis">
<h3>1. 位置空间还是速度空间</h3>
<p>RVO2、HRVO、RVO_Py_MAS、rl_rvo_nav 都把问题转成“哪些速度会导致未来碰撞”。采样学习版虽然是评分,但最关键的预测碰撞项也已经进入速度空间。</p>
</article>
<article class="axis">
<h3>2. 约束、过滤还是惩罚</h3>
<p>ORCA 是约束求解危险速度直接被半平面排除。RVO_Py_MAS 是采样后过滤禁区。采样学习版是给风险加惩罚危险速度未必完全禁止。RL 是把风险变成训练信号。</p>
</article>
<article class="axis">
<h3>3. 谁承担避让责任</h3>
<p>VO 偏“我躲别人”RVO/ORCA 假设双方共同承担HRVO 在 reciprocal 基础上减少来回换边。采样版需要靠评分项或通行偏置来塑造行为。</p>
</article>
<article class="axis">
<h3>4. 是否进入主压测</h3>
<p>当前主压测只保留 RVO2 原版和自研版。HRVO、RVO_Py_MAS、RL-RVO 只作为思想参考,不进入编辑器选择项。</p>
</article>
<article class="axis">
<h3>5. 可解释性和可改性</h3>
<p>RVO2 理论清晰但实现复杂。自研版最容易改权重和新增评分项。RVO_Py_MAS 最适合读懂速度障碍。RL 灵活但可解释性和复现实验成本更高。</p>
</article>
<article class="axis">
<h3>6. 2D 与 3D 的边界</h3>
<p>这些方案多数原生是 2D 或平面导航。FishROV 里如果只看 XZ会把不同高度层的对象误判为冲突也可能漏掉真实 3D 体积风险。核心决策必须明确水平避障、高度分层和体积碰撞的关系。</p>
</article>
</div>
</section>
<section aria-labelledby="score-title">
<h2 id="score-title">工程选择倾向</h2>
<div class="lane">
<div class="lane-title">理论安全约束</div>
<div class="bars">
<div class="bar"><span>RVO2 / ORCA</span><div class="bar-track"><div class="bar-fill" style="width: 92%"></div></div></div>
<div class="bar"><span>HRVO</span><div class="bar-track"><div class="bar-fill" style="width: 82%"></div></div></div>
<div class="bar"><span>采样 / RL</span><div class="bar-track"><div class="bar-fill" style="width: 48%"></div></div></div>
</div>
</div>
<div class="lane">
<div class="lane-title">可改可调</div>
<div class="bars">
<div class="bar"><span>采样学习版</span><div class="bar-track"><div class="bar-fill" style="width: 94%"></div></div></div>
<div class="bar"><span>RVO_Py_MAS</span><div class="bar-track"><div class="bar-fill" style="width: 78%"></div></div></div>
<div class="bar"><span>RVO2</span><div class="bar-track"><div class="bar-fill" style="width: 45%"></div></div></div>
</div>
</div>
</section>
<section aria-labelledby="recommend-title">
<h2 id="recommend-title">推荐读法</h2>
<div class="recommend">
<div class="choice">
<h3>先压测 RVO2</h3>
<p><b>RVO2 / ORCA</b> 当成标准基线,确认理想、拥挤、对冲、初始重叠等场景下的耗时和重叠率。</p>
</div>
<div class="choice">
<h3>再压测自研版</h3>
<p>自研版可以先吸收 RVO2 的速度空间思想,但保留可调评分、鱼群权重和性能优化入口。</p>
</div>
<div class="choice">
<h3>暂缓其他方案</h3>
<p>HRVO、RVO_Py_MAS、RL-RVO 暂时不用进编辑器主压测。先把 RVO2 和自研版两条主线测准。</p>
</div>
</div>
</section>
<section class="footnote">
<p>依据文档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。</p>
</section>
</main>
</body>
</html>

View File

@@ -2,19 +2,19 @@
## 目标 ## 目标
这个 Unity 工程用于压测本地避障方案。当前阶段先收束到两条主线 这个 Unity 工程用于压测 FishROV 的本地避障方案。当前主线已经收束为
- `Rvo2Adapter`:成熟 RVO2 / ORCA 基准 - `NoAvoidanceAdapter`:无避障对照基线
- 项目自研 RVO2 思想版本:用于验证可控性、可调参空间和性能上限 - `Rvo2Adapter`:成熟 RVO2 / ORCA 本地避障基准,也是当前可用主线
当前工程入口只保留 `NoAvoidance``Rvo2` 和自研版。不同方案之间唯一应该变化的变量,是 `AvoidanceFrameworkKind` 选择后由 `AvoidanceAdapterRegistry` 创建的 adapter 此前的独立启发式避障方案已经从工程入口移除。后续复杂鱼群行为不再通过重写局部避障求解器推进,而是在 RVO2 外层增加行为参数、preferred velocity 生成、RVO2 参数映射和运动后处理
## 共享运行入口 ## 共享运行入口
- 场景:`Assets/Scenes/AvoidancePerfDemo.unity` - 场景:`Assets/Scenes/AvoidancePerfDemo.unity`
- 入口脚本:`AvoidancePerfBootstrap` - 入口脚本:`AvoidancePerfBootstrap`
- Game 视调试面板: - Game 视调试面板:
- 避障方案选择无避障、RVO2、自研版 - 避障方案选择无避障、RVO2
- 测试场景选择 - 测试场景选择
- 2D / 2.5D / 3D 模式 - 2D / 2.5D / 3D 模式
- 观察视角切换 - 观察视角切换
@@ -22,26 +22,20 @@
- 暂停 / 继续 / 重置 - 暂停 / 继续 / 重置
- 平均 FPS、1% Low FPS、模拟耗时、GC 分配 - 平均 FPS、1% Low FPS、模拟耗时、GC 分配
- 碰撞调试XZ 平面重叠对数、3D 空间重叠对数、最近清空间距 - 碰撞调试XZ 平面重叠对数、3D 空间重叠对数、最近清空间距
- 表现对象:鱼为运行时圆锥,圆锥尖头表示朝向。
- 共享场景:
- 自由巡游
- 对向穿流
- 狭窄通道
- 圆阵高压
## Adapter 边界 ## Adapter 边界
框架接入主要修改: 框架接入主要修改:
```text ```text
Assets/Scripts/Benchmark/FrameworkAdapters/ Assets/Scripts/Benchmark/FrameworkAdapters/
``` ```
adapter 可以给运行时创建的对象追加框架自己的组件,但不应改变: adapter 可以给运行时创建的对象追加框架自己的组件,但不应改变:
- 场景生成逻辑 - 场景生成逻辑
- 鱼数量档 - 鱼数量档
- Game 视调试控件 - Game 视调试控件
- 指标名称 - 指标名称
- 相机 / 视角模式 - 相机 / 视角模式
- spawn 参数,除非框架确实需要很小且有文档说明的兼容调整 - spawn 参数,除非框架确实需要很小且有文档说明的兼容调整
@@ -50,7 +44,22 @@ adapter 可以给运行时创建的对象追加框架自己的组件,但不应
`NoAvoidanceAdapter` 是对照基线:只按期望速度移动,不做避障。 `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,7 +72,7 @@ adapter 可以给运行时创建的对象追加框架自己的组件,但不应
## 日志策略 ## 日志策略
编辑器和百元机都需要日志,但日志不能拖慢性能测试。 编辑器和机都需要日志,但日志不能拖慢性能测试。
- 日志目录:`Application.persistentDataPath/AvoidanceBenchmarkLogs/` - 日志目录:`Application.persistentDataPath/AvoidanceBenchmarkLogs/`
- 编辑器下通常在用户目录的 `AppData/LocalLow/.../AvoidanceBenchmarkLogs/` - 编辑器下通常在用户目录的 `AppData/LocalLow/.../AvoidanceBenchmarkLogs/`
@@ -71,13 +80,11 @@ adapter 可以给运行时创建的对象追加框架自己的组件,但不应
- 不要每帧 `Debug.Log` - 不要每帧 `Debug.Log`
- 不要在热路径里拼接字符串。 - 不要在热路径里拼接字符串。
- 不要每帧写文件。 - 不要每帧写文件。
- 不要让某个 adapter 单独输出额外明细,导致对比不公平。
- 建议只在内存中记录结构化数值样本。 - 建议只在内存中记录结构化数值样本。
- 当前实现固定每 `0.5s` 采样一次,先写入内存列表。 - 当前实现固定每 `0.5s` 采样一次,先写入内存列表。
- 单轮重置、退出或点击“导出日志”时一次性导出 CSV。 - 单轮重置、退出或点击“导出日志”时一次性导出 CSV。
- Console 只打印一行摘要,例如算法、场景、数量、平均 FPS、1% Low、平均模拟耗时、最大耗时、重叠对峰值。
百元机真机日志建议额外记录: 真机日志建议额外记录:
- 设备型号。 - 设备型号。
- 目标帧率。 - 目标帧率。
@@ -86,20 +93,3 @@ adapter 可以给运行时创建的对象追加框架自己的组件,但不应
- 是否 Development Build。 - 是否 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 更稳,只能说明当前启发式适合该场景,不代表算法理论上更强。

View File

@@ -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 外层行为与参数映射中
## 参考 ## 参考

View File

@@ -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
目标速度
-> 候选速度生成
-> 风险预测
-> 多目标评分
-> 速度平滑和运动约束
```
后续真正要优化的,是三个核心点:
- 更准确的风险函数。
- 更聪明的候选速度生成。
- 更稳定的冲突解除策略。