Compare commits
2 Commits
6e92c87d5a
...
e596d17866
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e596d17866 | ||
|
|
49acdf93cc |
@@ -165,4 +165,3 @@ MonoBehaviour:
|
|||||||
dimensionMode: 1
|
dimensionMode: 1
|
||||||
viewMode: 0
|
viewMode: 0
|
||||||
fishCount: 300
|
fishCount: 300
|
||||||
sharkCount: 1
|
|
||||||
|
|||||||
@@ -22,16 +22,12 @@ namespace FishROV.AvoidanceBenchmark
|
|||||||
|
|
||||||
public static BenchmarkAgent CreatePrimitive(BenchmarkAgentSpawnInfo spawnInfo, Transform parent)
|
public static BenchmarkAgent CreatePrimitive(BenchmarkAgentSpawnInfo spawnInfo, Transform parent)
|
||||||
{
|
{
|
||||||
var go = spawnInfo.Role == BenchmarkAgentRole.Shark
|
var go = CreateConeObject();
|
||||||
? GameObject.CreatePrimitive(PrimitiveType.Capsule)
|
|
||||||
: CreateConeObject();
|
|
||||||
go.name = $"{spawnInfo.Role}_{spawnInfo.GroupId:0000}";
|
go.name = $"{spawnInfo.Role}_{spawnInfo.GroupId:0000}";
|
||||||
go.transform.SetParent(parent, false);
|
go.transform.SetParent(parent, false);
|
||||||
go.transform.position = spawnInfo.Position;
|
go.transform.position = spawnInfo.Position;
|
||||||
|
|
||||||
var scale = spawnInfo.Role == BenchmarkAgentRole.Shark
|
var scale = new Vector3(
|
||||||
? new Vector3(spawnInfo.Radius, spawnInfo.Height, spawnInfo.Radius)
|
|
||||||
: new Vector3(
|
|
||||||
Mathf.Max(0.1f, spawnInfo.Radius * 1.4f),
|
Mathf.Max(0.1f, spawnInfo.Radius * 1.4f),
|
||||||
Mathf.Max(0.1f, spawnInfo.Radius * 1.4f),
|
Mathf.Max(0.1f, spawnInfo.Radius * 1.4f),
|
||||||
Mathf.Max(0.2f, spawnInfo.Radius * 3.2f));
|
Mathf.Max(0.2f, spawnInfo.Radius * 3.2f));
|
||||||
@@ -101,9 +97,6 @@ namespace FishROV.AvoidanceBenchmark
|
|||||||
case BenchmarkAgentRole.Fish:
|
case BenchmarkAgentRole.Fish:
|
||||||
color = new Color(0.10f, 0.72f, 0.95f);
|
color = new Color(0.10f, 0.72f, 0.95f);
|
||||||
break;
|
break;
|
||||||
case BenchmarkAgentRole.Shark:
|
|
||||||
color = new Color(0.95f, 0.22f, 0.16f);
|
|
||||||
break;
|
|
||||||
case BenchmarkAgentRole.SchoolProxy:
|
case BenchmarkAgentRole.SchoolProxy:
|
||||||
color = new Color(0.20f, 0.95f, 0.42f);
|
color = new Color(0.20f, 0.95f, 0.42f);
|
||||||
break;
|
break;
|
||||||
|
|||||||
156
FishROV/Assets/Scripts/Benchmark/AvoidanceBenchmarkLogWriter.cs
Normal file
156
FishROV/Assets/Scripts/Benchmark/AvoidanceBenchmarkLogWriter.cs
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace FishROV.AvoidanceBenchmark
|
||||||
|
{
|
||||||
|
public sealed class AvoidanceBenchmarkLogWriter
|
||||||
|
{
|
||||||
|
private const float SampleInterval = 0.5f;
|
||||||
|
private const int MaxSamples = 4096;
|
||||||
|
|
||||||
|
private readonly List<Sample> samples = new List<Sample>(1024);
|
||||||
|
private AvoidanceBenchmarkConfig config;
|
||||||
|
private string runName;
|
||||||
|
private float runStartTime;
|
||||||
|
private float nextSampleTime;
|
||||||
|
private bool hasRun;
|
||||||
|
|
||||||
|
public string LastExportPath { get; private set; } = "尚未导出";
|
||||||
|
public int SampleCount => samples.Count;
|
||||||
|
|
||||||
|
public void StartRun(AvoidanceBenchmarkConfig benchmarkConfig, string adapterName)
|
||||||
|
{
|
||||||
|
ExportIfNeeded();
|
||||||
|
|
||||||
|
config = benchmarkConfig.Clone();
|
||||||
|
runName = SanitizeFileName($"{DateTime.Now:yyyyMMdd_HHmmss}_{adapterName}_{config.Scenario}_{config.DimensionMode}_{config.FishCount}");
|
||||||
|
runStartTime = Time.unscaledTime;
|
||||||
|
nextSampleTime = runStartTime;
|
||||||
|
samples.Clear();
|
||||||
|
hasRun = true;
|
||||||
|
LastExportPath = "本轮尚未导出";
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Record(
|
||||||
|
AvoidanceMetricsRecorder metrics,
|
||||||
|
AvoidanceCollisionProbe collisionProbe,
|
||||||
|
int agentCount)
|
||||||
|
{
|
||||||
|
if (!hasRun || Time.unscaledTime < nextSampleTime || samples.Count >= MaxSamples)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextSampleTime += SampleInterval;
|
||||||
|
samples.Add(new Sample
|
||||||
|
{
|
||||||
|
TimeSeconds = Time.unscaledTime - runStartTime,
|
||||||
|
AgentCount = agentCount,
|
||||||
|
AverageFps = metrics.AverageFps,
|
||||||
|
OnePercentLowFps = metrics.OnePercentLowFps,
|
||||||
|
SimulationMs = metrics.SimulationMs,
|
||||||
|
GcAllocKb = metrics.GcAllocBytes / 1024.0,
|
||||||
|
HorizontalOverlapPairs = collisionProbe.HorizontalOverlapPairs,
|
||||||
|
SpatialOverlapPairs = collisionProbe.SpatialOverlapPairs,
|
||||||
|
MinimumClearance = collisionProbe.MinimumClearance
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ExportIfNeeded()
|
||||||
|
{
|
||||||
|
if (!hasRun || samples.Count == 0)
|
||||||
|
{
|
||||||
|
return LastExportPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
var directory = Path.Combine(Application.persistentDataPath, "AvoidanceBenchmarkLogs");
|
||||||
|
Directory.CreateDirectory(directory);
|
||||||
|
var path = Path.Combine(directory, runName + ".csv");
|
||||||
|
|
||||||
|
using (var writer = new StreamWriter(path, false))
|
||||||
|
{
|
||||||
|
writer.WriteLine("framework,scenario,dimension,fishCount,arenaRadius,screenWidth,screenHeight,targetFrameRate,timeSeconds,agentCount,averageFps,onePercentLowFps,simulationMs,gcAllocKb,horizontalOverlapPairs,spatialOverlapPairs,minimumClearance");
|
||||||
|
for (var i = 0; i < samples.Count; i++)
|
||||||
|
{
|
||||||
|
var sample = samples[i];
|
||||||
|
writer.Write(config.Framework);
|
||||||
|
writer.Write(',');
|
||||||
|
writer.Write(config.Scenario);
|
||||||
|
writer.Write(',');
|
||||||
|
writer.Write(config.DimensionMode);
|
||||||
|
writer.Write(',');
|
||||||
|
writer.Write(config.FishCount);
|
||||||
|
writer.Write(',');
|
||||||
|
WriteFloat(writer, config.ArenaRadius);
|
||||||
|
writer.Write(',');
|
||||||
|
writer.Write(Screen.width);
|
||||||
|
writer.Write(',');
|
||||||
|
writer.Write(Screen.height);
|
||||||
|
writer.Write(',');
|
||||||
|
writer.Write(Application.targetFrameRate);
|
||||||
|
writer.Write(',');
|
||||||
|
WriteFloat(writer, sample.TimeSeconds);
|
||||||
|
writer.Write(',');
|
||||||
|
writer.Write(sample.AgentCount);
|
||||||
|
writer.Write(',');
|
||||||
|
WriteFloat(writer, sample.AverageFps);
|
||||||
|
writer.Write(',');
|
||||||
|
WriteFloat(writer, sample.OnePercentLowFps);
|
||||||
|
writer.Write(',');
|
||||||
|
WriteDouble(writer, sample.SimulationMs);
|
||||||
|
writer.Write(',');
|
||||||
|
WriteDouble(writer, sample.GcAllocKb);
|
||||||
|
writer.Write(',');
|
||||||
|
writer.Write(sample.HorizontalOverlapPairs);
|
||||||
|
writer.Write(',');
|
||||||
|
writer.Write(sample.SpatialOverlapPairs);
|
||||||
|
writer.Write(',');
|
||||||
|
WriteFloat(writer, sample.MinimumClearance);
|
||||||
|
writer.WriteLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LastExportPath = path;
|
||||||
|
samples.Clear();
|
||||||
|
hasRun = false;
|
||||||
|
Debug.Log($"Avoidance benchmark log exported: {path}");
|
||||||
|
return LastExportPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteFloat(TextWriter writer, float value)
|
||||||
|
{
|
||||||
|
writer.Write(value.ToString("0.###", CultureInfo.InvariantCulture));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteDouble(TextWriter writer, double value)
|
||||||
|
{
|
||||||
|
writer.Write(value.ToString("0.###", CultureInfo.InvariantCulture));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string SanitizeFileName(string value)
|
||||||
|
{
|
||||||
|
foreach (var invalid in Path.GetInvalidFileNameChars())
|
||||||
|
{
|
||||||
|
value = value.Replace(invalid, '_');
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct Sample
|
||||||
|
{
|
||||||
|
public float TimeSeconds;
|
||||||
|
public int AgentCount;
|
||||||
|
public float AverageFps;
|
||||||
|
public float OnePercentLowFps;
|
||||||
|
public double SimulationMs;
|
||||||
|
public double GcAllocKb;
|
||||||
|
public int HorizontalOverlapPairs;
|
||||||
|
public int SpatialOverlapPairs;
|
||||||
|
public float MinimumClearance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 3f0e3f32e10e42eda4a2c6e58d0f9f8b
|
guid: 9c67563022844219b17fcf6a3630062c
|
||||||
MonoImporter:
|
MonoImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
@@ -6,19 +6,16 @@ namespace FishROV.AvoidanceBenchmark
|
|||||||
public enum AvoidanceFrameworkKind
|
public enum AvoidanceFrameworkKind
|
||||||
{
|
{
|
||||||
NoAvoidance = 0,
|
NoAvoidance = 0,
|
||||||
AstarRvo = 1,
|
Rvo2 = 1,
|
||||||
Rvo2 = 2,
|
SamplingLocal = 2
|
||||||
UnityNavMesh = 3,
|
|
||||||
SamplingLocal = 4
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum AvoidanceScenarioKind
|
public enum AvoidanceScenarioKind
|
||||||
{
|
{
|
||||||
FreeSwim = 0,
|
FreeSwim = 0,
|
||||||
CrossFlow = 1,
|
CrossFlow = 1,
|
||||||
SharkCharge = 2,
|
NarrowPassage = 2,
|
||||||
NarrowPassage = 3,
|
DenseCircleStress = 3
|
||||||
DenseCircleStress = 4
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum AvoidanceDimensionMode
|
public enum AvoidanceDimensionMode
|
||||||
@@ -33,16 +30,14 @@ namespace FishROV.AvoidanceBenchmark
|
|||||||
Overview = 0,
|
Overview = 0,
|
||||||
TopDown = 1,
|
TopDown = 1,
|
||||||
Side = 2,
|
Side = 2,
|
||||||
Shark = 3,
|
FollowSchool = 3
|
||||||
FollowSchool = 4
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum BenchmarkAgentRole
|
public enum BenchmarkAgentRole
|
||||||
{
|
{
|
||||||
Fish = 0,
|
Fish = 0,
|
||||||
Shark = 1,
|
SchoolProxy = 1,
|
||||||
SchoolProxy = 2,
|
Obstacle = 2
|
||||||
Obstacle = 3
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Serializable]
|
[Serializable]
|
||||||
@@ -67,13 +62,10 @@ namespace FishROV.AvoidanceBenchmark
|
|||||||
public AvoidanceScenarioKind Scenario = AvoidanceScenarioKind.FreeSwim;
|
public AvoidanceScenarioKind Scenario = AvoidanceScenarioKind.FreeSwim;
|
||||||
public AvoidanceDimensionMode DimensionMode = AvoidanceDimensionMode.XZ25D;
|
public AvoidanceDimensionMode DimensionMode = AvoidanceDimensionMode.XZ25D;
|
||||||
public int FishCount = 300;
|
public int FishCount = 300;
|
||||||
public int SharkCount = 1;
|
|
||||||
public int SchoolCount = 1;
|
public int SchoolCount = 1;
|
||||||
public float ArenaRadius = 28f;
|
public float ArenaRadius = 28f;
|
||||||
public float FishRadius = 0.35f;
|
public float FishRadius = 0.35f;
|
||||||
public float SharkRadius = 2.2f;
|
|
||||||
public float FishSpeed = 2.4f;
|
public float FishSpeed = 2.4f;
|
||||||
public float SharkSpeed = 3.4f;
|
|
||||||
public int MaxNeighbours = 8;
|
public int MaxNeighbours = 8;
|
||||||
public int RandomSeed = 13999;
|
public int RandomSeed = 13999;
|
||||||
public bool UseSchoolProxy = false;
|
public bool UseSchoolProxy = false;
|
||||||
|
|||||||
@@ -12,12 +12,12 @@ 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 int sharkCount = 1;
|
|
||||||
|
|
||||||
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();
|
||||||
private readonly AvoidanceMetricsRecorder metrics = new AvoidanceMetricsRecorder();
|
private readonly AvoidanceMetricsRecorder metrics = new AvoidanceMetricsRecorder();
|
||||||
private readonly AvoidanceCollisionProbe collisionProbe = new AvoidanceCollisionProbe();
|
private readonly AvoidanceCollisionProbe collisionProbe = new AvoidanceCollisionProbe();
|
||||||
|
private readonly AvoidanceBenchmarkLogWriter logWriter = new AvoidanceBenchmarkLogWriter();
|
||||||
private AvoidanceBenchmarkConfig config;
|
private AvoidanceBenchmarkConfig config;
|
||||||
private IAvoidanceAdapter adapter;
|
private IAvoidanceAdapter adapter;
|
||||||
private Transform agentRoot;
|
private Transform agentRoot;
|
||||||
@@ -60,6 +60,7 @@ namespace FishROV.AvoidanceBenchmark
|
|||||||
|
|
||||||
collisionProbe.Update(agents, config.DimensionMode, Time.unscaledDeltaTime);
|
collisionProbe.Update(agents, config.DimensionMode, Time.unscaledDeltaTime);
|
||||||
metrics.EndSimulation();
|
metrics.EndSimulation();
|
||||||
|
logWriter.Record(metrics, collisionProbe, agents.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnGUI()
|
private void OnGUI()
|
||||||
@@ -68,11 +69,12 @@ namespace FishROV.AvoidanceBenchmark
|
|||||||
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},鲨鱼 {sharkCount},总数 {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($"碰撞调试:XZ重叠 {collisionProbe.HorizontalOverlapPairs} 对 | 3D重叠 {collisionProbe.SpatialOverlapPairs} 对 | 最近间距 {collisionProbe.MinimumClearance:F2}");
|
||||||
GUILayout.Label($"帧率限制:不锁帧,vSync={QualitySettings.vSyncCount},targetFrameRate={Application.targetFrameRate}");
|
GUILayout.Label($"帧率限制:不锁帧,vSync={QualitySettings.vSyncCount},targetFrameRate={Application.targetFrameRate}");
|
||||||
|
GUILayout.Label($"日志样本:{logWriter.SampleCount} | {logWriter.LastExportPath}");
|
||||||
|
|
||||||
GUILayout.Space(8);
|
GUILayout.Space(8);
|
||||||
DrawEnumButtons("避障方案", framework, value =>
|
DrawEnumButtons("避障方案", framework, value =>
|
||||||
@@ -124,12 +126,18 @@ namespace FishROV.AvoidanceBenchmark
|
|||||||
{
|
{
|
||||||
ResetBenchmark();
|
ResetBenchmark();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (GUILayout.Button("导出日志"))
|
||||||
|
{
|
||||||
|
logWriter.ExportIfNeeded();
|
||||||
|
}
|
||||||
GUILayout.EndHorizontal();
|
GUILayout.EndHorizontal();
|
||||||
GUILayout.EndArea();
|
GUILayout.EndArea();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnDestroy()
|
private void OnDestroy()
|
||||||
{
|
{
|
||||||
|
logWriter.ExportIfNeeded();
|
||||||
adapter?.Dispose();
|
adapter?.Dispose();
|
||||||
metrics.Dispose();
|
metrics.Dispose();
|
||||||
}
|
}
|
||||||
@@ -138,6 +146,7 @@ namespace FishROV.AvoidanceBenchmark
|
|||||||
{
|
{
|
||||||
adapter?.Dispose();
|
adapter?.Dispose();
|
||||||
adapter = null;
|
adapter = null;
|
||||||
|
logWriter.ExportIfNeeded();
|
||||||
|
|
||||||
if (agentRoot != null)
|
if (agentRoot != null)
|
||||||
{
|
{
|
||||||
@@ -151,7 +160,6 @@ namespace FishROV.AvoidanceBenchmark
|
|||||||
Scenario = scenario,
|
Scenario = scenario,
|
||||||
DimensionMode = dimensionMode,
|
DimensionMode = dimensionMode,
|
||||||
FishCount = fishCount,
|
FishCount = fishCount,
|
||||||
SharkCount = sharkCount,
|
|
||||||
UseSchoolProxy = false
|
UseSchoolProxy = false
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -161,8 +169,8 @@ namespace FishROV.AvoidanceBenchmark
|
|||||||
adapter.Initialize(config);
|
adapter.Initialize(config);
|
||||||
|
|
||||||
SpawnFish();
|
SpawnFish();
|
||||||
SpawnSharks();
|
|
||||||
scenarioController.Reset(config, agents);
|
scenarioController.Reset(config, agents);
|
||||||
|
logWriter.StartRun(config, adapter.Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SpawnFish()
|
private void SpawnFish()
|
||||||
@@ -185,27 +193,6 @@ namespace FishROV.AvoidanceBenchmark
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SpawnSharks()
|
|
||||||
{
|
|
||||||
for (var i = 0; i < config.SharkCount; i++)
|
|
||||||
{
|
|
||||||
var angle = i * Mathf.PI * 2f / Mathf.Max(1, config.SharkCount);
|
|
||||||
var spawn = new BenchmarkAgentSpawnInfo
|
|
||||||
{
|
|
||||||
Position = new Vector3(Mathf.Cos(angle), 0f, Mathf.Sin(angle)) * config.ArenaRadius * 0.85f,
|
|
||||||
Radius = config.SharkRadius,
|
|
||||||
Height = config.SharkRadius * 2f,
|
|
||||||
MaxSpeed = config.SharkSpeed,
|
|
||||||
Layer = 2,
|
|
||||||
CollidesWith = 4,
|
|
||||||
Priority = 1f,
|
|
||||||
Role = BenchmarkAgentRole.Shark,
|
|
||||||
GroupId = i
|
|
||||||
};
|
|
||||||
agents.Add(adapter.CreateAgent(spawn, agentRoot));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Vector3 SpawnPosition(int index, int total, float radius)
|
private Vector3 SpawnPosition(int index, int total, float radius)
|
||||||
{
|
{
|
||||||
if (scenario == AvoidanceScenarioKind.CrossFlow)
|
if (scenario == AvoidanceScenarioKind.CrossFlow)
|
||||||
@@ -282,12 +269,6 @@ namespace FishROV.AvoidanceBenchmark
|
|||||||
position = new Vector3(0f, radius * 0.45f, -radius * 1.9f);
|
position = new Vector3(0f, radius * 0.45f, -radius * 1.9f);
|
||||||
rotation = Quaternion.Euler(12f, 0f, 0f);
|
rotation = Quaternion.Euler(12f, 0f, 0f);
|
||||||
break;
|
break;
|
||||||
case BenchmarkViewMode.Shark:
|
|
||||||
var shark = agents.Find(agent => agent.Role == BenchmarkAgentRole.Shark);
|
|
||||||
var sharkPos = shark != null ? shark.transform.position : Vector3.zero;
|
|
||||||
position = sharkPos + new Vector3(0f, radius * 0.35f, -radius * 0.75f);
|
|
||||||
rotation = Quaternion.LookRotation(sharkPos - position + Vector3.up * 2f, Vector3.up);
|
|
||||||
break;
|
|
||||||
case BenchmarkViewMode.FollowSchool:
|
case BenchmarkViewMode.FollowSchool:
|
||||||
var center = CalculateAgentCenter();
|
var center = CalculateAgentCenter();
|
||||||
position = center + new Vector3(0f, radius * 0.65f, -radius);
|
position = center + new Vector3(0f, radius * 0.65f, -radius);
|
||||||
@@ -342,20 +323,14 @@ namespace FishROV.AvoidanceBenchmark
|
|||||||
{
|
{
|
||||||
case AvoidanceFrameworkKind.NoAvoidance:
|
case AvoidanceFrameworkKind.NoAvoidance:
|
||||||
return "无避障";
|
return "无避障";
|
||||||
case AvoidanceFrameworkKind.AstarRvo:
|
|
||||||
return "A* RVO";
|
|
||||||
case AvoidanceFrameworkKind.Rvo2:
|
case AvoidanceFrameworkKind.Rvo2:
|
||||||
return "RVO2";
|
return "RVO2";
|
||||||
case AvoidanceFrameworkKind.UnityNavMesh:
|
|
||||||
return "NavMesh";
|
|
||||||
case AvoidanceFrameworkKind.SamplingLocal:
|
case AvoidanceFrameworkKind.SamplingLocal:
|
||||||
return "采样避障";
|
return "自研版";
|
||||||
case AvoidanceScenarioKind.FreeSwim:
|
case AvoidanceScenarioKind.FreeSwim:
|
||||||
return "自由巡游";
|
return "自由巡游";
|
||||||
case AvoidanceScenarioKind.CrossFlow:
|
case AvoidanceScenarioKind.CrossFlow:
|
||||||
return "对向穿流";
|
return "对向穿流";
|
||||||
case AvoidanceScenarioKind.SharkCharge:
|
|
||||||
return "鲨鱼冲群";
|
|
||||||
case AvoidanceScenarioKind.NarrowPassage:
|
case AvoidanceScenarioKind.NarrowPassage:
|
||||||
return "狭窄通道";
|
return "狭窄通道";
|
||||||
case AvoidanceScenarioKind.DenseCircleStress:
|
case AvoidanceScenarioKind.DenseCircleStress:
|
||||||
@@ -372,8 +347,6 @@ namespace FishROV.AvoidanceBenchmark
|
|||||||
return "俯视";
|
return "俯视";
|
||||||
case BenchmarkViewMode.Side:
|
case BenchmarkViewMode.Side:
|
||||||
return "侧视";
|
return "侧视";
|
||||||
case BenchmarkViewMode.Shark:
|
|
||||||
return "跟鲨鱼";
|
|
||||||
case BenchmarkViewMode.FollowSchool:
|
case BenchmarkViewMode.FollowSchool:
|
||||||
return "跟鱼群";
|
return "跟鱼群";
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -1,286 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Reflection;
|
|
||||||
using UnityEngine;
|
|
||||||
|
|
||||||
namespace FishROV.AvoidanceBenchmark
|
|
||||||
{
|
|
||||||
public sealed class AstarRvoAdapter : IAvoidanceAdapter
|
|
||||||
{
|
|
||||||
private const BindingFlags MemberFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
|
|
||||||
|
|
||||||
private readonly Dictionary<BenchmarkAgent, RvoAgentBinding> bindings =
|
|
||||||
new Dictionary<BenchmarkAgent, RvoAgentBinding>(512);
|
|
||||||
|
|
||||||
private Type rvoControllerType;
|
|
||||||
private Type rvoSimulatorType;
|
|
||||||
private MethodInfo setTargetMethod;
|
|
||||||
private MethodInfo calculateMovementDeltaMethod;
|
|
||||||
private GameObject simulatorObject;
|
|
||||||
private AvoidanceBenchmarkConfig config;
|
|
||||||
private string status = "Not initialized";
|
|
||||||
|
|
||||||
public string Name => "A* RVO 本地避障";
|
|
||||||
public bool IsAvailable => rvoControllerType != null && rvoSimulatorType != null;
|
|
||||||
public string Status => status;
|
|
||||||
|
|
||||||
public void Initialize(AvoidanceBenchmarkConfig config)
|
|
||||||
{
|
|
||||||
this.config = config;
|
|
||||||
ResolveAstarTypes();
|
|
||||||
|
|
||||||
if (!IsAvailable)
|
|
||||||
{
|
|
||||||
status = "未检测到 A* Pathfinding Project Pro 的 RVO 模块;请导入 A* Pro,确保存在 Pathfinding.RVO.RVOController 和 RVOSimulator。当前退回基线移动。";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
simulatorObject = new GameObject("A* RVO 模拟器");
|
|
||||||
var simulator = simulatorObject.AddComponent(rvoSimulatorType);
|
|
||||||
ConfigureSimulator(simulator);
|
|
||||||
|
|
||||||
var dimensionNote = config.DimensionMode == AvoidanceDimensionMode.XYZ3D
|
|
||||||
? "3D 压测会投影到 A* RVO 的 XZ 平面"
|
|
||||||
: "XZ 平面";
|
|
||||||
status = $"A* Pro RVO 已通过反射接入({dimensionNote})。";
|
|
||||||
}
|
|
||||||
|
|
||||||
public BenchmarkAgent CreateAgent(BenchmarkAgentSpawnInfo spawnInfo, Transform parent)
|
|
||||||
{
|
|
||||||
var agent = BenchmarkAgent.CreatePrimitive(spawnInfo, parent);
|
|
||||||
if (!IsAvailable)
|
|
||||||
{
|
|
||||||
return agent;
|
|
||||||
}
|
|
||||||
|
|
||||||
var controller = agent.gameObject.AddComponent(rvoControllerType);
|
|
||||||
ConfigureController(controller, spawnInfo);
|
|
||||||
bindings[agent] = new RvoAgentBinding(agent, controller);
|
|
||||||
return agent;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetPreferredVelocity(BenchmarkAgent agent, Vector3 preferredVelocity)
|
|
||||||
{
|
|
||||||
if (!IsAvailable || !bindings.TryGetValue(agent, out var binding))
|
|
||||||
{
|
|
||||||
agent.ApplyVelocity(preferredVelocity);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.PreferredVelocity = Vector3.ClampMagnitude(preferredVelocity, agent.MaxSpeed);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetTarget(BenchmarkAgent agent, Vector3 target)
|
|
||||||
{
|
|
||||||
agent.Target = target;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Tick(float deltaTime)
|
|
||||||
{
|
|
||||||
if (!IsAvailable || deltaTime <= 0f)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var binding in bindings.Values)
|
|
||||||
{
|
|
||||||
var velocity = CalculateAvoidedVelocity(binding, deltaTime);
|
|
||||||
binding.Agent.ApplyVelocity(velocity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
bindings.Clear();
|
|
||||||
if (simulatorObject != null)
|
|
||||||
{
|
|
||||||
UnityEngine.Object.Destroy(simulatorObject);
|
|
||||||
simulatorObject = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ResolveAstarTypes()
|
|
||||||
{
|
|
||||||
rvoControllerType = FindType("Pathfinding.RVO.RVOController");
|
|
||||||
rvoSimulatorType = FindType("Pathfinding.RVO.RVOSimulator");
|
|
||||||
|
|
||||||
if (rvoControllerType == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setTargetMethod = rvoControllerType.GetMethod(
|
|
||||||
"SetTarget",
|
|
||||||
MemberFlags,
|
|
||||||
null,
|
|
||||||
new[] { typeof(Vector3), typeof(float), typeof(float), typeof(Vector3) },
|
|
||||||
null);
|
|
||||||
calculateMovementDeltaMethod = rvoControllerType.GetMethod(
|
|
||||||
"CalculateMovementDelta",
|
|
||||||
MemberFlags,
|
|
||||||
null,
|
|
||||||
new[] { typeof(Vector3), typeof(float) },
|
|
||||||
null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ConfigureSimulator(object simulator)
|
|
||||||
{
|
|
||||||
SetMember(simulator, "desiredSimulationFPS", 60);
|
|
||||||
SetMember(simulator, "movementPlane", "XZ");
|
|
||||||
SetMember(simulator, "doubleBuffering", true);
|
|
||||||
SetMember(simulator, "workerThreads", "None");
|
|
||||||
SetMember(simulator, "symmetryBreakingBias", 0.1f);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ConfigureController(object controller, BenchmarkAgentSpawnInfo spawnInfo)
|
|
||||||
{
|
|
||||||
SetMember(controller, "radius", spawnInfo.Radius);
|
|
||||||
SetMember(controller, "height", Mathf.Max(spawnInfo.Height, spawnInfo.Radius * 2f));
|
|
||||||
SetMember(controller, "center", Vector3.up * spawnInfo.Height * 0.5f);
|
|
||||||
SetMember(controller, "maxSpeed", spawnInfo.MaxSpeed);
|
|
||||||
SetMember(controller, "priority", Mathf.Clamp01(spawnInfo.Priority));
|
|
||||||
SetMember(controller, "layer", spawnInfo.Layer);
|
|
||||||
SetMember(controller, "collidesWith", spawnInfo.CollidesWith);
|
|
||||||
SetMember(controller, "locked", spawnInfo.IsKinematicObstacle);
|
|
||||||
SetMember(controller, "lockWhenNotMoving", spawnInfo.IsKinematicObstacle);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Vector3 CalculateAvoidedVelocity(RvoAgentBinding binding, float deltaTime)
|
|
||||||
{
|
|
||||||
var agent = binding.Agent;
|
|
||||||
var preferredVelocity = binding.PreferredVelocity;
|
|
||||||
var position = agent.transform.position;
|
|
||||||
var target = position + preferredVelocity;
|
|
||||||
var speed = preferredVelocity.magnitude;
|
|
||||||
|
|
||||||
if (setTargetMethod != null)
|
|
||||||
{
|
|
||||||
TryInvoke(
|
|
||||||
setTargetMethod,
|
|
||||||
binding.Controller,
|
|
||||||
new object[] { target, speed, agent.MaxSpeed, target });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (calculateMovementDeltaMethod == null)
|
|
||||||
{
|
|
||||||
return preferredVelocity;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var delta = calculateMovementDeltaMethod.Invoke(
|
|
||||||
binding.Controller,
|
|
||||||
new object[] { position, deltaTime });
|
|
||||||
if (delta is Vector3 movementDelta)
|
|
||||||
{
|
|
||||||
return movementDelta / deltaTime;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception exception)
|
|
||||||
{
|
|
||||||
status = $"A* RVO 反射调用失败:{exception.GetBaseException().Message}";
|
|
||||||
}
|
|
||||||
|
|
||||||
return preferredVelocity;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void TryInvoke(MethodInfo method, object target, object[] parameters)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
method.Invoke(target, parameters);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// Keep the benchmark running even when an A* version changes a reflected member.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void SetMember(object target, string name, object value)
|
|
||||||
{
|
|
||||||
if (target == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var type = target.GetType();
|
|
||||||
var property = type.GetProperty(name, MemberFlags);
|
|
||||||
if (property != null && property.CanWrite)
|
|
||||||
{
|
|
||||||
TrySetValue(property.PropertyType, value, converted => property.SetValue(target, converted, null));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var field = type.GetField(name, MemberFlags);
|
|
||||||
if (field != null)
|
|
||||||
{
|
|
||||||
TrySetValue(field.FieldType, value, converted => field.SetValue(target, converted));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void TrySetValue(Type targetType, object value, Action<object> setter)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
setter(ConvertValue(targetType, value));
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// Optional reflected configuration only; defaults are acceptable across A* versions.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static object ConvertValue(Type targetType, object value)
|
|
||||||
{
|
|
||||||
if (value == null || targetType.IsInstanceOfType(value))
|
|
||||||
{
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetType.IsEnum)
|
|
||||||
{
|
|
||||||
if (value is string enumName)
|
|
||||||
{
|
|
||||||
return Enum.Parse(targetType, enumName);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Enum.ToObject(targetType, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Convert.ChangeType(value, targetType);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Type FindType(string fullName)
|
|
||||||
{
|
|
||||||
var direct = Type.GetType(fullName);
|
|
||||||
if (direct != null)
|
|
||||||
{
|
|
||||||
return direct;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
|
|
||||||
{
|
|
||||||
var type = assembly.GetType(fullName);
|
|
||||||
if (type != null)
|
|
||||||
{
|
|
||||||
return type;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class RvoAgentBinding
|
|
||||||
{
|
|
||||||
public RvoAgentBinding(BenchmarkAgent agent, object controller)
|
|
||||||
{
|
|
||||||
Agent = agent;
|
|
||||||
Controller = controller;
|
|
||||||
}
|
|
||||||
|
|
||||||
public BenchmarkAgent Agent { get; }
|
|
||||||
public object Controller { get; }
|
|
||||||
public Vector3 PreferredVelocity { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,12 +8,8 @@ namespace FishROV.AvoidanceBenchmark
|
|||||||
{
|
{
|
||||||
case AvoidanceFrameworkKind.NoAvoidance:
|
case AvoidanceFrameworkKind.NoAvoidance:
|
||||||
return new NoAvoidanceAdapter();
|
return new NoAvoidanceAdapter();
|
||||||
case AvoidanceFrameworkKind.AstarRvo:
|
|
||||||
return new AstarRvoAdapter();
|
|
||||||
case AvoidanceFrameworkKind.Rvo2:
|
case AvoidanceFrameworkKind.Rvo2:
|
||||||
return new Rvo2Adapter();
|
return new Rvo2Adapter();
|
||||||
case AvoidanceFrameworkKind.UnityNavMesh:
|
|
||||||
return new UnityNavMeshAdapter();
|
|
||||||
case AvoidanceFrameworkKind.SamplingLocal:
|
case AvoidanceFrameworkKind.SamplingLocal:
|
||||||
return new SamplingLocalAvoidanceAdapter();
|
return new SamplingLocalAvoidanceAdapter();
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ namespace FishROV.AvoidanceBenchmark
|
|||||||
states.Clear();
|
states.Clear();
|
||||||
orderedStates.Clear();
|
orderedStates.Clear();
|
||||||
buckets.Clear();
|
buckets.Clear();
|
||||||
cellSize = Mathf.Max(config.SharkRadius * 2.5f, config.FishRadius * NeighbourRangeMultiplier);
|
cellSize = config.FishRadius * NeighbourRangeMultiplier;
|
||||||
}
|
}
|
||||||
|
|
||||||
public BenchmarkAgent CreateAgent(BenchmarkAgentSpawnInfo spawnInfo, Transform parent)
|
public BenchmarkAgent CreateAgent(BenchmarkAgentSpawnInfo spawnInfo, Transform parent)
|
||||||
@@ -143,7 +143,7 @@ namespace FishROV.AvoidanceBenchmark
|
|||||||
var position = state.Agent.transform.position;
|
var position = state.Agent.transform.position;
|
||||||
var centerCell = ToCell(position);
|
var centerCell = ToCell(position);
|
||||||
var queryRange = Mathf.CeilToInt(
|
var queryRange = Mathf.CeilToInt(
|
||||||
Mathf.Max(state.Agent.Radius * NeighbourRangeMultiplier, state.Agent.Radius + config.SharkRadius) / cellSize);
|
state.Agent.Radius * NeighbourRangeMultiplier / cellSize);
|
||||||
queryRange = Mathf.Clamp(queryRange, 1, 4);
|
queryRange = Mathf.Clamp(queryRange, 1, 4);
|
||||||
|
|
||||||
for (var x = -queryRange; x <= queryRange; x++)
|
for (var x = -queryRange; x <= queryRange; x++)
|
||||||
|
|||||||
@@ -1,197 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using UnityEngine;
|
|
||||||
using UnityEngine.AI;
|
|
||||||
|
|
||||||
namespace FishROV.AvoidanceBenchmark
|
|
||||||
{
|
|
||||||
public sealed class UnityNavMeshAdapter : IAvoidanceAdapter
|
|
||||||
{
|
|
||||||
private readonly Dictionary<BenchmarkAgent, NavMeshAgent> navAgents = new Dictionary<BenchmarkAgent, NavMeshAgent>(512);
|
|
||||||
private AvoidanceBenchmarkConfig config;
|
|
||||||
private NavMeshData runtimeNavMeshData;
|
|
||||||
private NavMeshDataInstance runtimeNavMeshInstance;
|
|
||||||
private bool hasNavMeshData;
|
|
||||||
private int fallbackAgentCount;
|
|
||||||
|
|
||||||
public string Name => "Unity NavMeshAgent 对照组";
|
|
||||||
public bool IsAvailable => hasNavMeshData;
|
|
||||||
|
|
||||||
public string Status
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (!hasNavMeshData)
|
|
||||||
{
|
|
||||||
return "NavMesh 数据构建失败;当前退回基线移动。";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fallbackAgentCount > 0)
|
|
||||||
{
|
|
||||||
return $"NavMeshAgent 已接管 {navAgents.Count} 个对象;{fallbackAgentCount} 个对象无法放到 NavMesh 上,使用基线移动。";
|
|
||||||
}
|
|
||||||
|
|
||||||
return $"Unity NavMeshAgent 避障已接管 {navAgents.Count} 个对象。";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Initialize(AvoidanceBenchmarkConfig config)
|
|
||||||
{
|
|
||||||
this.config = config.Clone();
|
|
||||||
navAgents.Clear();
|
|
||||||
fallbackAgentCount = 0;
|
|
||||||
BuildRuntimeNavMesh();
|
|
||||||
|
|
||||||
var triangulation = NavMesh.CalculateTriangulation();
|
|
||||||
hasNavMeshData = triangulation.vertices != null && triangulation.vertices.Length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public BenchmarkAgent CreateAgent(BenchmarkAgentSpawnInfo spawnInfo, Transform parent)
|
|
||||||
{
|
|
||||||
var benchmarkAgent = BenchmarkAgent.CreatePrimitive(spawnInfo, parent);
|
|
||||||
if (!hasNavMeshData)
|
|
||||||
{
|
|
||||||
return benchmarkAgent;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TryPlaceOnNavMesh(benchmarkAgent, spawnInfo))
|
|
||||||
{
|
|
||||||
fallbackAgentCount++;
|
|
||||||
return benchmarkAgent;
|
|
||||||
}
|
|
||||||
|
|
||||||
var navAgent = benchmarkAgent.gameObject.AddComponent<NavMeshAgent>();
|
|
||||||
ConfigureNavMeshAgent(navAgent, spawnInfo);
|
|
||||||
navAgents[benchmarkAgent] = navAgent;
|
|
||||||
return benchmarkAgent;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetPreferredVelocity(BenchmarkAgent agent, Vector3 preferredVelocity)
|
|
||||||
{
|
|
||||||
if (!hasNavMeshData || !navAgents.TryGetValue(agent, out var navAgent) || !navAgent.isOnNavMesh)
|
|
||||||
{
|
|
||||||
agent.ApplyVelocity(preferredVelocity);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var navVelocity = preferredVelocity;
|
|
||||||
if (config.DimensionMode != AvoidanceDimensionMode.XYZ3D)
|
|
||||||
{
|
|
||||||
navVelocity.y = 0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
navAgent.nextPosition = agent.transform.position;
|
|
||||||
navAgent.velocity = Vector3.ClampMagnitude(navVelocity, agent.MaxSpeed);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetTarget(BenchmarkAgent agent, Vector3 target)
|
|
||||||
{
|
|
||||||
agent.Target = target;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Tick(float deltaTime)
|
|
||||||
{
|
|
||||||
if (!hasNavMeshData)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var entry in navAgents)
|
|
||||||
{
|
|
||||||
var benchmarkAgent = entry.Key;
|
|
||||||
var navAgent = entry.Value;
|
|
||||||
if (benchmarkAgent == null || navAgent == null || !navAgent.isOnNavMesh)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var smoothedVelocity = Vector3.MoveTowards(
|
|
||||||
benchmarkAgent.Velocity,
|
|
||||||
navAgent.velocity,
|
|
||||||
navAgent.acceleration * deltaTime);
|
|
||||||
benchmarkAgent.ApplyVelocity(smoothedVelocity);
|
|
||||||
navAgent.nextPosition = benchmarkAgent.transform.position;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
foreach (var navAgent in navAgents.Values)
|
|
||||||
{
|
|
||||||
if (navAgent != null)
|
|
||||||
{
|
|
||||||
Object.Destroy(navAgent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
navAgents.Clear();
|
|
||||||
fallbackAgentCount = 0;
|
|
||||||
|
|
||||||
if (runtimeNavMeshInstance.valid)
|
|
||||||
{
|
|
||||||
runtimeNavMeshInstance.Remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
runtimeNavMeshData = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void BuildRuntimeNavMesh()
|
|
||||||
{
|
|
||||||
if (runtimeNavMeshInstance.valid)
|
|
||||||
{
|
|
||||||
runtimeNavMeshInstance.Remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
var buildSettings = NavMesh.GetSettingsByID(0);
|
|
||||||
buildSettings.agentRadius = Mathf.Max(0.05f, config.FishRadius);
|
|
||||||
buildSettings.agentHeight = Mathf.Max(0.2f, config.FishRadius * 2f);
|
|
||||||
buildSettings.agentClimb = 0.4f;
|
|
||||||
buildSettings.agentSlope = 45f;
|
|
||||||
|
|
||||||
var arenaSize = Mathf.Max(8f, config.ArenaRadius * 2.4f);
|
|
||||||
var sources = new List<NavMeshBuildSource>
|
|
||||||
{
|
|
||||||
new NavMeshBuildSource
|
|
||||||
{
|
|
||||||
shape = NavMeshBuildSourceShape.Box,
|
|
||||||
transform = Matrix4x4.TRS(Vector3.zero, Quaternion.identity, Vector3.one),
|
|
||||||
size = new Vector3(arenaSize, 0.1f, arenaSize),
|
|
||||||
area = 0
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var bounds = new Bounds(Vector3.zero, new Vector3(arenaSize, 2f, arenaSize));
|
|
||||||
runtimeNavMeshData = NavMeshBuilder.BuildNavMeshData(buildSettings, sources, bounds, Vector3.zero, Quaternion.identity);
|
|
||||||
if (runtimeNavMeshData != null)
|
|
||||||
{
|
|
||||||
runtimeNavMeshInstance = NavMesh.AddNavMeshData(runtimeNavMeshData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryPlaceOnNavMesh(BenchmarkAgent benchmarkAgent, BenchmarkAgentSpawnInfo spawnInfo)
|
|
||||||
{
|
|
||||||
var searchDistance = Mathf.Max(2f, spawnInfo.Height + spawnInfo.Radius);
|
|
||||||
if (!NavMesh.SamplePosition(spawnInfo.Position, out var hit, searchDistance, NavMesh.AllAreas))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
benchmarkAgent.transform.position = hit.position;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ConfigureNavMeshAgent(NavMeshAgent navAgent, BenchmarkAgentSpawnInfo spawnInfo)
|
|
||||||
{
|
|
||||||
navAgent.radius = Mathf.Max(0.01f, spawnInfo.Radius);
|
|
||||||
navAgent.height = Mathf.Max(0.1f, spawnInfo.Height);
|
|
||||||
navAgent.speed = Mathf.Max(0.01f, spawnInfo.MaxSpeed);
|
|
||||||
navAgent.acceleration = navAgent.speed * 8f;
|
|
||||||
navAgent.angularSpeed = 720f;
|
|
||||||
navAgent.obstacleAvoidanceType = ObstacleAvoidanceType.HighQualityObstacleAvoidance;
|
|
||||||
navAgent.avoidancePriority = Mathf.Clamp(Mathf.RoundToInt((1f - spawnInfo.Priority) * 99f), 0, 99);
|
|
||||||
navAgent.autoBraking = false;
|
|
||||||
navAgent.autoRepath = false;
|
|
||||||
navAgent.updatePosition = false;
|
|
||||||
navAgent.updateRotation = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 7d3b299dd0504f849b83e4af9ed272a7
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@@ -43,11 +43,6 @@ namespace FishROV.AvoidanceBenchmark
|
|||||||
desired.y = 0f;
|
desired.y = 0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.Scenario == AvoidanceScenarioKind.SharkCharge && agent.Role == BenchmarkAgentRole.Shark)
|
|
||||||
{
|
|
||||||
desired = new Vector3(Mathf.Sin(elapsed * 0.35f) * 8f, 0f, -agent.transform.position.z);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (desired.sqrMagnitude < 0.0001f)
|
if (desired.sqrMagnitude < 0.0001f)
|
||||||
{
|
{
|
||||||
return Vector3.zero;
|
return Vector3.zero;
|
||||||
@@ -66,13 +61,6 @@ namespace FishROV.AvoidanceBenchmark
|
|||||||
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))
|
||||||
: new Vector3(-radius, HeightFor(agent), Random.Range(-radius, radius));
|
: new Vector3(-radius, HeightFor(agent), Random.Range(-radius, radius));
|
||||||
case AvoidanceScenarioKind.SharkCharge:
|
|
||||||
if (agent.Role == BenchmarkAgentRole.Shark)
|
|
||||||
{
|
|
||||||
return new Vector3(0f, HeightFor(agent), -radius);
|
|
||||||
}
|
|
||||||
var schoolPoint = Random.insideUnitCircle * radius * 0.45f;
|
|
||||||
return new Vector3(schoolPoint.x, HeightFor(agent), schoolPoint.y);
|
|
||||||
case AvoidanceScenarioKind.NarrowPassage:
|
case AvoidanceScenarioKind.NarrowPassage:
|
||||||
return new Vector3(agent.GroupId % 2 == 0 ? radius : -radius, HeightFor(agent), Random.Range(-3f, 3f));
|
return new Vector3(agent.GroupId % 2 == 0 ? radius : -radius, HeightFor(agent), Random.Range(-3f, 3f));
|
||||||
case AvoidanceScenarioKind.DenseCircleStress:
|
case AvoidanceScenarioKind.DenseCircleStress:
|
||||||
|
|||||||
@@ -483,7 +483,7 @@
|
|||||||
<li>大多数方案都需要上层提供目标或期望速度,然后再做局部修正。</li>
|
<li>大多数方案都需要上层提供目标或期望速度,然后再做局部修正。</li>
|
||||||
<li>核心输入都离不开位置、速度、半径、邻居范围和预测时间。</li>
|
<li>核心输入都离不开位置、速度、半径、邻居范围和预测时间。</li>
|
||||||
<li>都在权衡两个目标:尽量接近期望运动,同时减少未来碰撞风险。</li>
|
<li>都在权衡两个目标:尽量接近期望运动,同时减少未来碰撞风险。</li>
|
||||||
<li>除 NavMesh 外,大多数方案都不负责全局路径,不理解死胡同、任务顺序和长期收益。</li>
|
<li>当前压测只关心局部速度修正,不比较全局寻路、地图可达性或复杂玩法状态。</li>
|
||||||
<li>在 FishROV 的 3D/2.5D 场景里,多数方案仍需要明确如何处理 XZ 投影、高度层和真实体积冲突。</li>
|
<li>在 FishROV 的 3D/2.5D 场景里,多数方案仍需要明确如何处理 XZ 投影、高度层和真实体积冲突。</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -550,15 +550,6 @@
|
|||||||
<td>训练成本高,泛化依赖仿真覆盖,工程可控性较弱。</td>
|
<td>训练成本高,泛化依赖仿真覆盖,工程可控性较弱。</td>
|
||||||
<td>建议先吸收风险表达和 reward/score 思想,不急着引入完整 RL 链路。</td>
|
<td>建议先吸收风险表达和 reward/score 思想,不急着引入完整 RL 链路。</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<td class="method">Unity NavMeshAgent</td>
|
|
||||||
<td><span class="tag tag-blue">导航网格</span><span class="tag tag-gold">路径跟随</span><br>先理解哪里能走,再沿路径局部避让。</td>
|
|
||||||
<td>在 NavMesh 上规划路径,沿拐点移动,同时使用内置局部避让。</td>
|
|
||||||
<td>全局可达性、路径搜索、内置工程避让。</td>
|
|
||||||
<td>静态地图、走廊、房间、地形导航。</td>
|
|
||||||
<td>局部避障不是核心强项,高密度动态体易抖,主要是平面系统。</td>
|
|
||||||
<td>适合作为全局路径层或工程对照,不适合作为鱼群局部避障核心。</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -569,7 +560,7 @@
|
|||||||
<div class="axis-grid">
|
<div class="axis-grid">
|
||||||
<article class="axis">
|
<article class="axis">
|
||||||
<h3>1. 位置空间还是速度空间</h3>
|
<h3>1. 位置空间还是速度空间</h3>
|
||||||
<p>RVO2、HRVO、RVO_Py_MAS、rl_rvo_nav 都把问题转成“哪些速度会导致未来碰撞”。NavMesh 主要先回答“哪里能走”。采样学习版虽然是评分,但最关键的预测碰撞项也已经进入速度空间。</p>
|
<p>RVO2、HRVO、RVO_Py_MAS、rl_rvo_nav 都把问题转成“哪些速度会导致未来碰撞”。采样学习版虽然是评分,但最关键的预测碰撞项也已经进入速度空间。</p>
|
||||||
</article>
|
</article>
|
||||||
<article class="axis">
|
<article class="axis">
|
||||||
<h3>2. 约束、过滤还是惩罚</h3>
|
<h3>2. 约束、过滤还是惩罚</h3>
|
||||||
@@ -577,15 +568,15 @@
|
|||||||
</article>
|
</article>
|
||||||
<article class="axis">
|
<article class="axis">
|
||||||
<h3>3. 谁承担避让责任</h3>
|
<h3>3. 谁承担避让责任</h3>
|
||||||
<p>VO 偏“我躲别人”;RVO/ORCA 假设双方共同承担;HRVO 在 reciprocal 基础上减少来回换边。NavMesh 用优先级处理谁让路。采样版需要靠评分项或通行偏置来塑造行为。</p>
|
<p>VO 偏“我躲别人”;RVO/ORCA 假设双方共同承担;HRVO 在 reciprocal 基础上减少来回换边。采样版需要靠评分项或通行偏置来塑造行为。</p>
|
||||||
</article>
|
</article>
|
||||||
<article class="axis">
|
<article class="axis">
|
||||||
<h3>4. 是否理解地图</h3>
|
<h3>4. 是否进入主压测</h3>
|
||||||
<p>NavMesh 有全局可达性和路径图。其他方案基本只处理局部邻居,不知道死胡同、洞穴、任务顺序和远期收益。因此 FishROV 最可能需要“全局/中层路径 + 局部避障 + 运动控制”的分层结构。</p>
|
<p>当前主压测只保留 RVO2 原版和自研版。HRVO、RVO_Py_MAS、RL-RVO 只作为思想参考,不进入编辑器选择项。</p>
|
||||||
</article>
|
</article>
|
||||||
<article class="axis">
|
<article class="axis">
|
||||||
<h3>5. 可解释性和可改性</h3>
|
<h3>5. 可解释性和可改性</h3>
|
||||||
<p>RVO2 理论清晰但实现复杂。采样学习版最容易改权重和新增评分项。RVO_Py_MAS最适合读懂速度障碍。RL 灵活但可解释性和复现实验成本更高。NavMesh 是工程黑盒较多的成熟系统。</p>
|
<p>RVO2 理论清晰但实现复杂。自研版最容易改权重和新增评分项。RVO_Py_MAS 最适合读懂速度障碍。RL 灵活但可解释性和复现实验成本更高。</p>
|
||||||
</article>
|
</article>
|
||||||
<article class="axis">
|
<article class="axis">
|
||||||
<h3>6. 2D 与 3D 的边界</h3>
|
<h3>6. 2D 与 3D 的边界</h3>
|
||||||
@@ -609,15 +600,7 @@
|
|||||||
<div class="bars">
|
<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>采样学习版</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>RVO_Py_MAS</span><div class="bar-track"><div class="bar-fill" style="width: 78%"></div></div></div>
|
||||||
<div class="bar"><span>NavMesh / RVO2</span><div class="bar-track"><div class="bar-fill" style="width: 45%"></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>
|
|
||||||
<div class="lane">
|
|
||||||
<div class="lane-title">全局路径能力</div>
|
|
||||||
<div class="bars">
|
|
||||||
<div class="bar"><span>NavMeshAgent</span><div class="bar-track"><div class="bar-fill" style="width: 96%"></div></div></div>
|
|
||||||
<div class="bar"><span>RVO 系列</span><div class="bar-track"><div class="bar-fill" style="width: 18%"></div></div></div>
|
|
||||||
<div class="bar"><span>采样 / RL</span><div class="bar-track"><div class="bar-fill" style="width: 22%"></div></div></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -635,7 +618,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="choice">
|
<div class="choice">
|
||||||
<h3>暂缓其他方案</h3>
|
<h3>暂缓其他方案</h3>
|
||||||
<p>NavMesh、HRVO、RL-RVO 暂时不用进主压测。先把两条主线测准,再决定是否补防振荡或全局可达性。</p>
|
<p>HRVO、RVO_Py_MAS、RL-RVO 暂时不用进编辑器主压测。先把 RVO2 和自研版两条主线测准。</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -7,26 +7,25 @@
|
|||||||
- `Rvo2Adapter`:成熟 RVO2 / ORCA 基准。
|
- `Rvo2Adapter`:成熟 RVO2 / ORCA 基准。
|
||||||
- 项目自研 RVO2 思想版本:用于验证可控性、可调参空间和性能上限。
|
- 项目自研 RVO2 思想版本:用于验证可控性、可调参空间和性能上限。
|
||||||
|
|
||||||
其他方案可以保留为参考或历史对照,但不进入当前主压测结论。不同方案之间唯一应该变化的变量,是 `AvoidanceFrameworkKind` 选择后由 `AvoidanceAdapterRegistry` 创建的 adapter。
|
当前工程入口只保留 `NoAvoidance`、`Rvo2` 和自研版。不同方案之间唯一应该变化的变量,是 `AvoidanceFrameworkKind` 选择后由 `AvoidanceAdapterRegistry` 创建的 adapter。
|
||||||
|
|
||||||
## 共享运行入口
|
## 共享运行入口
|
||||||
|
|
||||||
- 场景:`Assets/Scenes/AvoidancePerfDemo.unity`
|
- 场景:`Assets/Scenes/AvoidancePerfDemo.unity`
|
||||||
- 入口脚本:`AvoidancePerfBootstrap`
|
- 入口脚本:`AvoidancePerfBootstrap`
|
||||||
- Game 视口调试面板:
|
- Game 视口调试面板:
|
||||||
- 避障方案选择
|
- 避障方案选择:无避障、RVO2、自研版
|
||||||
- 测试场景选择
|
- 测试场景选择
|
||||||
- 2D / 2.5D / 3D 模式
|
- 2D / 2.5D / 3D 模式
|
||||||
- 观察视角切换
|
- 观察视角切换
|
||||||
- 小鱼数量预设和滑条
|
- 鱼数量预设和滑条
|
||||||
- 暂停 / 继续 / 重置
|
- 暂停 / 继续 / 重置
|
||||||
- 平均 FPS、1% Low FPS、模拟耗时、GC 分配
|
- 平均 FPS、1% Low FPS、模拟耗时、GC 分配
|
||||||
- 碰撞调试:XZ 平面重叠对数、3D 空间重叠对数、最近清空间距
|
- 碰撞调试:XZ 平面重叠对数、3D 空间重叠对数、最近清空间距
|
||||||
- 表现对象:鱼群为运行时圆锥,圆锥尖头表示朝向;鲨鱼为 Capsule。
|
- 表现对象:鱼为运行时圆锥,圆锥尖头表示朝向。
|
||||||
- 共享场景:
|
- 共享场景:
|
||||||
- 自由巡游
|
- 自由巡游
|
||||||
- 对向穿流
|
- 对向穿流
|
||||||
- 鲨鱼冲群
|
|
||||||
- 狭窄通道
|
- 狭窄通道
|
||||||
- 圆阵高压
|
- 圆阵高压
|
||||||
|
|
||||||
@@ -41,7 +40,7 @@ Assets/Scripts/Benchmark/FrameworkAdapters/
|
|||||||
adapter 可以给运行时创建的对象追加框架自己的组件,但不应改变:
|
adapter 可以给运行时创建的对象追加框架自己的组件,但不应改变:
|
||||||
|
|
||||||
- 场景生成逻辑
|
- 场景生成逻辑
|
||||||
- 小鱼/鲨鱼数量档
|
- 鱼数量档
|
||||||
- Game 视口调试控件
|
- Game 视口调试控件
|
||||||
- 指标名称
|
- 指标名称
|
||||||
- 相机/视角模式
|
- 相机/视角模式
|
||||||
@@ -51,7 +50,7 @@ adapter 可以给运行时创建的对象追加框架自己的组件,但不应
|
|||||||
|
|
||||||
`NoAvoidanceAdapter` 是对照基线:只按期望速度移动,不做避障。
|
`NoAvoidanceAdapter` 是对照基线:只按期望速度移动,不做避障。
|
||||||
|
|
||||||
当前压测结论主线只比较 RVO2 原版和基于 RVO2 思想的自研版本。A* RVO、Unity NavMesh、采样避障学习版可以继续保留在调试面板里,但当前不作为主结论对象。
|
当前压测结论主线只比较 RVO2 原版和基于 RVO2 思想的自研版本。编辑器压测入口已经只保留无避障、RVO2 和自研版,避免无关选项干扰当前结论。
|
||||||
|
|
||||||
## 当前压测口径
|
## 当前压测口径
|
||||||
|
|
||||||
@@ -66,14 +65,16 @@ adapter 可以给运行时创建的对象追加框架自己的组件,但不应
|
|||||||
|
|
||||||
编辑器和百元机都需要日志,但日志不能拖慢性能测试。
|
编辑器和百元机都需要日志,但日志不能拖慢性能测试。
|
||||||
|
|
||||||
|
- 日志目录:`Application.persistentDataPath/AvoidanceBenchmarkLogs/`。
|
||||||
|
- 编辑器下通常在用户目录的 `AppData/LocalLow/.../AvoidanceBenchmarkLogs/`。
|
||||||
|
- 真机下在 App 的 persistent data path 里,使用面板的“导出日志”按钮或结束/重置测试自动写出。
|
||||||
- 不要每帧 `Debug.Log`。
|
- 不要每帧 `Debug.Log`。
|
||||||
- 不要在热路径里拼接字符串。
|
- 不要在热路径里拼接字符串。
|
||||||
- 不要每帧写文件。
|
- 不要每帧写文件。
|
||||||
- 不要让某个 adapter 单独输出额外明细,导致对比不公平。
|
- 不要让某个 adapter 单独输出额外明细,导致对比不公平。
|
||||||
- 建议只在内存中记录结构化数值样本。
|
- 建议只在内存中记录结构化数值样本。
|
||||||
- 建议固定间隔采样,例如每 `0.5s` 或每 `30` 帧聚合一次。
|
- 当前实现固定每 `0.5s` 采样一次,先写入内存列表。
|
||||||
- 建议使用环形缓冲或预分配数组,避免压测中产生 GC。
|
- 单轮重置、退出或点击“导出日志”时一次性导出 CSV。
|
||||||
- 单轮结束后一次性导出 CSV/JSON。
|
|
||||||
- Console 只打印一行摘要,例如算法、场景、数量、平均 FPS、1% Low、平均模拟耗时、最大耗时、重叠对峰值。
|
- Console 只打印一行摘要,例如算法、场景、数量、平均 FPS、1% Low、平均模拟耗时、最大耗时、重叠对峰值。
|
||||||
|
|
||||||
百元机真机日志建议额外记录:
|
百元机真机日志建议额外记录:
|
||||||
@@ -86,15 +87,6 @@ adapter 可以给运行时创建的对象追加框架自己的组件,但不应
|
|||||||
- 是否连接调试器。
|
- 是否连接调试器。
|
||||||
- 是否出现明显发热、降频或后台干扰。
|
- 是否出现明显发热、降频或后台干扰。
|
||||||
|
|
||||||
## A* Pathfinding Project Pro RVO Adapter
|
|
||||||
|
|
||||||
`AstarRvoAdapter` 位于 `Assets/Scripts/Benchmark/FrameworkAdapters/`,由 `AvoidanceFrameworkKind.AstarRvo` 选择。
|
|
||||||
|
|
||||||
- adapter 通过反射查找 `Pathfinding.RVO.RVOController` 和 `Pathfinding.RVO.RVOSimulator`,所以没有导入 A* Pro 时工程仍可编译。
|
|
||||||
- 如果缺少 A* Pro RVO 类型,Game 视口状态会提示缺包,并退回基线移动。
|
|
||||||
- 如果类型存在,adapter 会创建运行时 `RVOSimulator`,给 benchmark 对象添加 `RVOController`,把期望速度送入 A* RVO,再把反射得到的避障速度写回 `BenchmarkAgent.ApplyVelocity`。
|
|
||||||
- 共享场景、HUD、数量档、指标和相机逻辑不变。
|
|
||||||
|
|
||||||
## RVO2 Adapter
|
## RVO2 Adapter
|
||||||
|
|
||||||
当前已在 `Assets/ThirdParty/RVO2-CS/` 导入 `snape/RVO2-CS` 的核心 `RVOCS` 源码,并保留 Apache-2.0 许可证说明。
|
当前已在 `Assets/ThirdParty/RVO2-CS/` 导入 `snape/RVO2-CS` 的核心 `RVOCS` 源码,并保留 Apache-2.0 许可证说明。
|
||||||
@@ -103,20 +95,11 @@ adapter 可以给运行时创建的对象追加框架自己的组件,但不应
|
|||||||
- 每帧设置期望速度前会同步 Unity Transform 位置,避免 RVO 内部位置和 benchmark 边界夹取结果漂移。
|
- 每帧设置期望速度前会同步 Unity Transform 位置,避免 RVO 内部位置和 benchmark 边界夹取结果漂移。
|
||||||
- RVO2-CS 是 2D 算法,2D 和 2.5D 使用 Unity `x/z` 到 RVO `x/y` 的映射;3D 模式会投影到 XZ 平面。
|
- RVO2-CS 是 2D 算法,2D 和 2.5D 使用 Unity `x/z` 到 RVO `x/y` 的映射;3D 模式会投影到 XZ 平面。
|
||||||
|
|
||||||
## Unity NavMesh Adapter
|
|
||||||
|
|
||||||
`UnityNavMeshAdapter` 使用内置 `UnityEngine.AI.NavMeshAgent` 做对照组。
|
|
||||||
|
|
||||||
- adapter 会使用 `NavMeshBuilder.BuildNavMeshData` 为 benchmark 场景运行时生成一张平面 NavMesh,不需要手动烘焙场景。
|
|
||||||
- 如果运行时 NavMesh 构建失败,Game 视口状态会提示,并退回基线移动,不会添加无效 `NavMeshAgent`。
|
|
||||||
- NavMeshAgent 计算出的速度会按自身 `acceleration` 平滑后再交给 benchmark 可视对象,减少高密度场景的一帧速度尖峰抖动。
|
|
||||||
- 因为 NavMesh 是平面数据,3D 压测更适合作为 2D / 2.5D 对照;完整 3D 体积避障仍需要专门方案。
|
|
||||||
|
|
||||||
## 采样避障学习版
|
## 采样避障学习版
|
||||||
|
|
||||||
`SamplingLocalAvoidanceAdapter` 是项目内自写的学习算法,用来和成熟框架做横向对比。
|
`SamplingLocalAvoidanceAdapter` 是项目内自写的学习算法,用来和成熟框架做横向对比。
|
||||||
|
|
||||||
- 使用空间哈希查找近邻,避免全量 O(n²) 扫描。
|
- 使用空间哈希查找近邻,避免全量 O(n²) 扫描。
|
||||||
- 对每个 agent 采样多组候选速度,并用目标偏差、速度平滑、预测碰撞、当前重叠、边界越界惩罚打分。
|
- 对每个 agent 采样多组候选速度,并用目标偏差、速度平滑、预测碰撞、当前重叠、边界越界惩罚打分。
|
||||||
- 它不是完整 ORCA,也不保证无碰撞;目的在于提供一个可读、可改、可调参的学习基线。
|
- 它不是完整 ORCA,也不保证无碰撞;当前只作为自研候选的弱基线,表现明显差于 RVO2 是预期内结果。
|
||||||
- 如果它在某些场景比 RVO2 或 NavMesh 更稳,只能说明当前启发式适合该场景,不代表算法理论上更强。
|
- 如果它在某些场景比 RVO2 更稳,只能说明当前启发式适合该场景,不代表算法理论上更强。
|
||||||
|
|||||||
Reference in New Issue
Block a user