Compare commits
4 Commits
main
...
e596d17866
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e596d17866 | ||
|
|
49acdf93cc | ||
|
|
6e92c87d5a | ||
|
|
31408c3500 |
@@ -165,4 +165,3 @@ MonoBehaviour:
|
||||
dimensionMode: 1
|
||||
viewMode: 0
|
||||
fishCount: 300
|
||||
sharkCount: 1
|
||||
|
||||
@@ -22,16 +22,12 @@ namespace FishROV.AvoidanceBenchmark
|
||||
|
||||
public static BenchmarkAgent CreatePrimitive(BenchmarkAgentSpawnInfo spawnInfo, Transform parent)
|
||||
{
|
||||
var go = spawnInfo.Role == BenchmarkAgentRole.Shark
|
||||
? GameObject.CreatePrimitive(PrimitiveType.Capsule)
|
||||
: CreateConeObject();
|
||||
var go = CreateConeObject();
|
||||
go.name = $"{spawnInfo.Role}_{spawnInfo.GroupId:0000}";
|
||||
go.transform.SetParent(parent, false);
|
||||
go.transform.position = spawnInfo.Position;
|
||||
|
||||
var scale = spawnInfo.Role == BenchmarkAgentRole.Shark
|
||||
? new Vector3(spawnInfo.Radius, spawnInfo.Height, spawnInfo.Radius)
|
||||
: new Vector3(
|
||||
var scale = new Vector3(
|
||||
Mathf.Max(0.1f, spawnInfo.Radius * 1.4f),
|
||||
Mathf.Max(0.1f, spawnInfo.Radius * 1.4f),
|
||||
Mathf.Max(0.2f, spawnInfo.Radius * 3.2f));
|
||||
@@ -101,9 +97,6 @@ namespace FishROV.AvoidanceBenchmark
|
||||
case BenchmarkAgentRole.Fish:
|
||||
color = new Color(0.10f, 0.72f, 0.95f);
|
||||
break;
|
||||
case BenchmarkAgentRole.Shark:
|
||||
color = new Color(0.95f, 0.22f, 0.16f);
|
||||
break;
|
||||
case BenchmarkAgentRole.SchoolProxy:
|
||||
color = new Color(0.20f, 0.95f, 0.42f);
|
||||
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
|
||||
guid: 3f0e3f32e10e42eda4a2c6e58d0f9f8b
|
||||
guid: 9c67563022844219b17fcf6a3630062c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
@@ -6,19 +6,16 @@ namespace FishROV.AvoidanceBenchmark
|
||||
public enum AvoidanceFrameworkKind
|
||||
{
|
||||
NoAvoidance = 0,
|
||||
AstarRvo = 1,
|
||||
Rvo2 = 2,
|
||||
UnityNavMesh = 3,
|
||||
SamplingLocal = 4
|
||||
Rvo2 = 1,
|
||||
SamplingLocal = 2
|
||||
}
|
||||
|
||||
public enum AvoidanceScenarioKind
|
||||
{
|
||||
FreeSwim = 0,
|
||||
CrossFlow = 1,
|
||||
SharkCharge = 2,
|
||||
NarrowPassage = 3,
|
||||
DenseCircleStress = 4
|
||||
NarrowPassage = 2,
|
||||
DenseCircleStress = 3
|
||||
}
|
||||
|
||||
public enum AvoidanceDimensionMode
|
||||
@@ -33,16 +30,14 @@ namespace FishROV.AvoidanceBenchmark
|
||||
Overview = 0,
|
||||
TopDown = 1,
|
||||
Side = 2,
|
||||
Shark = 3,
|
||||
FollowSchool = 4
|
||||
FollowSchool = 3
|
||||
}
|
||||
|
||||
public enum BenchmarkAgentRole
|
||||
{
|
||||
Fish = 0,
|
||||
Shark = 1,
|
||||
SchoolProxy = 2,
|
||||
Obstacle = 3
|
||||
SchoolProxy = 1,
|
||||
Obstacle = 2
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
@@ -67,13 +62,10 @@ namespace FishROV.AvoidanceBenchmark
|
||||
public AvoidanceScenarioKind Scenario = AvoidanceScenarioKind.FreeSwim;
|
||||
public AvoidanceDimensionMode DimensionMode = AvoidanceDimensionMode.XZ25D;
|
||||
public int FishCount = 300;
|
||||
public int SharkCount = 1;
|
||||
public int SchoolCount = 1;
|
||||
public float ArenaRadius = 28f;
|
||||
public float FishRadius = 0.35f;
|
||||
public float SharkRadius = 2.2f;
|
||||
public float FishSpeed = 2.4f;
|
||||
public float SharkSpeed = 3.4f;
|
||||
public int MaxNeighbours = 8;
|
||||
public int RandomSeed = 13999;
|
||||
public bool UseSchoolProxy = false;
|
||||
|
||||
@@ -12,12 +12,12 @@ namespace FishROV.AvoidanceBenchmark
|
||||
[SerializeField] private AvoidanceDimensionMode dimensionMode = AvoidanceDimensionMode.XZ25D;
|
||||
[SerializeField] private BenchmarkViewMode viewMode = BenchmarkViewMode.Overview;
|
||||
[SerializeField] private int fishCount = 300;
|
||||
[SerializeField] private int sharkCount = 1;
|
||||
|
||||
private readonly List<BenchmarkAgent> agents = new List<BenchmarkAgent>(512);
|
||||
private readonly AvoidanceScenarioController scenarioController = new AvoidanceScenarioController();
|
||||
private readonly AvoidanceMetricsRecorder metrics = new AvoidanceMetricsRecorder();
|
||||
private readonly AvoidanceCollisionProbe collisionProbe = new AvoidanceCollisionProbe();
|
||||
private readonly AvoidanceBenchmarkLogWriter logWriter = new AvoidanceBenchmarkLogWriter();
|
||||
private AvoidanceBenchmarkConfig config;
|
||||
private IAvoidanceAdapter adapter;
|
||||
private Transform agentRoot;
|
||||
@@ -60,6 +60,7 @@ namespace FishROV.AvoidanceBenchmark
|
||||
|
||||
collisionProbe.Update(agents, config.DimensionMode, Time.unscaledDeltaTime);
|
||||
metrics.EndSimulation();
|
||||
logWriter.Record(metrics, collisionProbe, agents.Count);
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
@@ -68,11 +69,12 @@ namespace FishROV.AvoidanceBenchmark
|
||||
GUILayout.Label("FishROV 本地避障压测");
|
||||
GUILayout.Label($"当前方案:{adapter?.Name ?? "未初始化"}");
|
||||
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.SimulationMs:F2} ms | GC:{metrics.GcAllocBytes / 1024f:F1} KB");
|
||||
GUILayout.Label($"碰撞调试:XZ重叠 {collisionProbe.HorizontalOverlapPairs} 对 | 3D重叠 {collisionProbe.SpatialOverlapPairs} 对 | 最近间距 {collisionProbe.MinimumClearance:F2}");
|
||||
GUILayout.Label($"帧率限制:不锁帧,vSync={QualitySettings.vSyncCount},targetFrameRate={Application.targetFrameRate}");
|
||||
GUILayout.Label($"日志样本:{logWriter.SampleCount} | {logWriter.LastExportPath}");
|
||||
|
||||
GUILayout.Space(8);
|
||||
DrawEnumButtons("避障方案", framework, value =>
|
||||
@@ -124,12 +126,18 @@ namespace FishROV.AvoidanceBenchmark
|
||||
{
|
||||
ResetBenchmark();
|
||||
}
|
||||
|
||||
if (GUILayout.Button("导出日志"))
|
||||
{
|
||||
logWriter.ExportIfNeeded();
|
||||
}
|
||||
GUILayout.EndHorizontal();
|
||||
GUILayout.EndArea();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
logWriter.ExportIfNeeded();
|
||||
adapter?.Dispose();
|
||||
metrics.Dispose();
|
||||
}
|
||||
@@ -138,6 +146,7 @@ namespace FishROV.AvoidanceBenchmark
|
||||
{
|
||||
adapter?.Dispose();
|
||||
adapter = null;
|
||||
logWriter.ExportIfNeeded();
|
||||
|
||||
if (agentRoot != null)
|
||||
{
|
||||
@@ -151,7 +160,6 @@ namespace FishROV.AvoidanceBenchmark
|
||||
Scenario = scenario,
|
||||
DimensionMode = dimensionMode,
|
||||
FishCount = fishCount,
|
||||
SharkCount = sharkCount,
|
||||
UseSchoolProxy = false
|
||||
};
|
||||
|
||||
@@ -161,8 +169,8 @@ namespace FishROV.AvoidanceBenchmark
|
||||
adapter.Initialize(config);
|
||||
|
||||
SpawnFish();
|
||||
SpawnSharks();
|
||||
scenarioController.Reset(config, agents);
|
||||
logWriter.StartRun(config, adapter.Name);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (scenario == AvoidanceScenarioKind.CrossFlow)
|
||||
@@ -282,12 +269,6 @@ namespace FishROV.AvoidanceBenchmark
|
||||
position = new Vector3(0f, radius * 0.45f, -radius * 1.9f);
|
||||
rotation = Quaternion.Euler(12f, 0f, 0f);
|
||||
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:
|
||||
var center = CalculateAgentCenter();
|
||||
position = center + new Vector3(0f, radius * 0.65f, -radius);
|
||||
@@ -342,20 +323,14 @@ namespace FishROV.AvoidanceBenchmark
|
||||
{
|
||||
case AvoidanceFrameworkKind.NoAvoidance:
|
||||
return "无避障";
|
||||
case AvoidanceFrameworkKind.AstarRvo:
|
||||
return "A* RVO";
|
||||
case AvoidanceFrameworkKind.Rvo2:
|
||||
return "RVO2";
|
||||
case AvoidanceFrameworkKind.UnityNavMesh:
|
||||
return "NavMesh";
|
||||
case AvoidanceFrameworkKind.SamplingLocal:
|
||||
return "采样避障";
|
||||
return "自研版";
|
||||
case AvoidanceScenarioKind.FreeSwim:
|
||||
return "自由巡游";
|
||||
case AvoidanceScenarioKind.CrossFlow:
|
||||
return "对向穿流";
|
||||
case AvoidanceScenarioKind.SharkCharge:
|
||||
return "鲨鱼冲群";
|
||||
case AvoidanceScenarioKind.NarrowPassage:
|
||||
return "狭窄通道";
|
||||
case AvoidanceScenarioKind.DenseCircleStress:
|
||||
@@ -372,8 +347,6 @@ namespace FishROV.AvoidanceBenchmark
|
||||
return "俯视";
|
||||
case BenchmarkViewMode.Side:
|
||||
return "侧视";
|
||||
case BenchmarkViewMode.Shark:
|
||||
return "跟鲨鱼";
|
||||
case BenchmarkViewMode.FollowSchool:
|
||||
return "跟鱼群";
|
||||
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:
|
||||
return new NoAvoidanceAdapter();
|
||||
case AvoidanceFrameworkKind.AstarRvo:
|
||||
return new AstarRvoAdapter();
|
||||
case AvoidanceFrameworkKind.Rvo2:
|
||||
return new Rvo2Adapter();
|
||||
case AvoidanceFrameworkKind.UnityNavMesh:
|
||||
return new UnityNavMeshAdapter();
|
||||
case AvoidanceFrameworkKind.SamplingLocal:
|
||||
return new SamplingLocalAvoidanceAdapter();
|
||||
default:
|
||||
|
||||
@@ -34,7 +34,7 @@ namespace FishROV.AvoidanceBenchmark
|
||||
states.Clear();
|
||||
orderedStates.Clear();
|
||||
buckets.Clear();
|
||||
cellSize = Mathf.Max(config.SharkRadius * 2.5f, config.FishRadius * NeighbourRangeMultiplier);
|
||||
cellSize = config.FishRadius * NeighbourRangeMultiplier;
|
||||
}
|
||||
|
||||
public BenchmarkAgent CreateAgent(BenchmarkAgentSpawnInfo spawnInfo, Transform parent)
|
||||
@@ -143,7 +143,7 @@ namespace FishROV.AvoidanceBenchmark
|
||||
var position = state.Agent.transform.position;
|
||||
var centerCell = ToCell(position);
|
||||
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);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
return Vector3.zero;
|
||||
@@ -66,13 +61,6 @@ namespace FishROV.AvoidanceBenchmark
|
||||
return agent.GroupId % 2 == 0
|
||||
? 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:
|
||||
return new Vector3(agent.GroupId % 2 == 0 ? radius : -radius, HeightFor(agent), Random.Range(-3f, 3f));
|
||||
case AvoidanceScenarioKind.DenseCircleStress:
|
||||
|
||||
631
FishROV/Docs/avoidance-core-comparison.html
Normal file
631
FishROV/Docs/avoidance-core-comparison.html
Normal file
@@ -0,0 +1,631 @@
|
||||
<!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>
|
||||
239
FishROV/Docs/hrvo-core-principles.md
Normal file
239
FishROV/Docs/hrvo-core-principles.md
Normal file
@@ -0,0 +1,239 @@
|
||||
# HRVO 核心原理
|
||||
|
||||
## 一句话结论
|
||||
|
||||
HRVO 的核心是 **在速度空间里排除未来会发生碰撞的速度,然后选择一个尽量接近期望速度、同时更不容易产生左右振荡的新速度**。
|
||||
|
||||
它不是全局寻路算法,也不是推进器控制器,而是一个去中心化的局部避障算法。每个 agent 只根据自己感知到的邻居位置和速度独立决策,不需要中心调度,也不需要 agent 之间通信。
|
||||
|
||||
仓库地址:
|
||||
|
||||
```text
|
||||
https://github.com/snape/HRVO
|
||||
```
|
||||
|
||||
## 要解决的问题
|
||||
|
||||
多 agent 避障最麻烦的不是单个障碍物,而是 **大家都会动,并且大家都会试图避让**。
|
||||
|
||||
如果用简单 steering,常见逻辑是:
|
||||
|
||||
```text
|
||||
朝目标走
|
||||
靠近别人就加一个排斥力
|
||||
最后合成速度
|
||||
```
|
||||
|
||||
这种方式容易出现几个问题:
|
||||
|
||||
- 只看当前距离,不一定能判断未来是否会撞上。
|
||||
- 多个 agent 同时避让时,可能互相把对方推乱。
|
||||
- 对称场景下容易左右摇摆。
|
||||
- 高密度场景里,排斥力叠加后速度可能非常不稳定。
|
||||
|
||||
HRVO 换了一个视角:不直接在位置空间里把 agent 推开,而是在速度空间里判断:
|
||||
|
||||
```text
|
||||
如果我现在选择这个速度,
|
||||
按照邻居的当前位置和速度预测,
|
||||
未来一小段时间内会不会撞上?
|
||||
```
|
||||
|
||||
会撞的速度,就是不能选的速度。
|
||||
|
||||
## Velocity Obstacle
|
||||
|
||||
Velocity Obstacle,简称 VO,表示速度空间中的危险区域。
|
||||
|
||||
假设有两个 agent:A 和 B。
|
||||
|
||||
- A 的位置是 `pA`,速度是 `vA`,半径是 `rA`。
|
||||
- B 的位置是 `pB`,速度是 `vB`,半径是 `rB`。
|
||||
- 如果未来某个时间,两者中心距离小于 `rA + rB`,就会发生碰撞。
|
||||
|
||||
对 A 来说,可以把 B 膨胀成半径 `rA + rB` 的圆。然后在速度空间中构造一片区域:
|
||||
|
||||
```text
|
||||
如果 A 选择这片区域里的速度,
|
||||
并且 B 继续保持当前速度,
|
||||
A 未来会撞到 B。
|
||||
```
|
||||
|
||||
这片速度区域就是 B 对 A 产生的 Velocity Obstacle。
|
||||
|
||||
直观理解:
|
||||
|
||||
```text
|
||||
位置空间:哪里有障碍?
|
||||
速度空间:哪些速度会导致未来撞上障碍?
|
||||
```
|
||||
|
||||
VO 的价值在于,它把动态碰撞预测变成了当前帧的速度选择问题。
|
||||
|
||||
## RVO:双方共同承担避让
|
||||
|
||||
普通 VO 假设“别人保持当前速度,我来躲”。这在动态 agent 群里会偏保守,因为实际上对方也会躲。
|
||||
|
||||
RVO 的思想是 reciprocal:
|
||||
|
||||
```text
|
||||
A 和 B 都是主动体。
|
||||
如果两者当前速度组合会导致碰撞,
|
||||
不应该让 A 独自承担全部避让修正。
|
||||
双方各承担一部分速度修正。
|
||||
```
|
||||
|
||||
这样 agent 的行为会更自然,尤其适合:
|
||||
|
||||
- 机器人群;
|
||||
- 人群模拟;
|
||||
- 鱼群或艇群;
|
||||
- 多个同等级 NPC 的局部避让。
|
||||
|
||||
## HRVO:减少 reciprocal 避让振荡
|
||||
|
||||
RVO 虽然比 VO 更对称,但在某些对称场景中仍然可能出现振荡。
|
||||
|
||||
典型例子:
|
||||
|
||||
```text
|
||||
A 和 B 正面对冲
|
||||
这一帧双方都觉得向左让比较好
|
||||
下一帧相对状态变化后,双方又都觉得向右让比较好
|
||||
然后反复左右切换
|
||||
```
|
||||
|
||||
HRVO 的关键改进就是 Hybrid:
|
||||
|
||||
```text
|
||||
它不是完全使用 VO,也不是完全使用 RVO,
|
||||
而是把两者的几何边界混合起来,
|
||||
构造一个更偏向稳定避让侧的速度障碍。
|
||||
```
|
||||
|
||||
直观说,HRVO 会让速度障碍的顶点和边界选择更偏向“别来回改主意”,从而减少互相避让时的左右横跳。
|
||||
|
||||
所以 HRVO 关注的不只是无碰撞,还包括:
|
||||
|
||||
```text
|
||||
collision-free
|
||||
oscillation-free
|
||||
decentralized
|
||||
```
|
||||
|
||||
也就是无碰撞、少振荡、去中心化。
|
||||
|
||||
## 每帧流程
|
||||
|
||||
HRVO 每帧大致做这些事:
|
||||
|
||||
1. 更新每个 agent 的当前位置和当前速度。
|
||||
2. 为每个 agent 设置期望速度 `preferredVelocity`。
|
||||
3. 查找附近邻居。
|
||||
4. 对每个邻居构造一个 HRVO 速度障碍。
|
||||
5. 在可选速度中寻找一个不落入危险区域、且最接近期望速度的新速度。
|
||||
6. 用新速度推进 agent。
|
||||
|
||||
可以理解成:
|
||||
|
||||
```text
|
||||
目标方向
|
||||
-> 期望速度
|
||||
-> 邻居预测
|
||||
-> 构造速度禁区
|
||||
-> 选择安全速度
|
||||
-> 推进位置
|
||||
```
|
||||
|
||||
## 和 VO / RVO / ORCA 的区别
|
||||
|
||||
| 方法 | 核心假设 | 主要特点 |
|
||||
| --- | --- | --- |
|
||||
| VO | 我躲别人,别人保持当前速度 | 简单直接,但对动态群体偏保守 |
|
||||
| RVO | 双方都会避让,各承担一部分修正 | 更适合多 agent,但对称场景可能振荡 |
|
||||
| HRVO | 混合 VO 和 RVO 的速度障碍 | 在 reciprocal 基础上减少左右振荡 |
|
||||
| ORCA | 把避障转成半平面约束和优化问题 | 工程上更常用,求解形式更规整 |
|
||||
|
||||
HRVO 可以看作 RVO 家族中的一个改进版本,重点是处理 reciprocal collision avoidance 中的振荡问题。
|
||||
|
||||
## 关键参数
|
||||
|
||||
HRVO 这类算法通常会关心这些参数:
|
||||
|
||||
- `radius`:agent 半径,决定碰撞距离。
|
||||
- `neighborDist`:多远范围内的 agent 会被当作邻居。
|
||||
- `maxNeighbors`:最多考虑多少邻居。
|
||||
- `timeHorizon`:向未来预测多长时间内的碰撞。
|
||||
- `maxSpeed`:最大速度。
|
||||
- `timeStep`:仿真步长。
|
||||
|
||||
参数影响很直接:
|
||||
|
||||
- `neighborDist` 太小:反应太晚,容易贴脸避让。
|
||||
- `neighborDist` 太大:邻居过多,性能下降,也可能过度保守。
|
||||
- `timeHorizon` 太短:只处理眼前风险。
|
||||
- `timeHorizon` 太长:提前避让过多,路径可能绕或慢。
|
||||
- `maxNeighbors` 太低:高密度时可能漏掉重要邻居。
|
||||
|
||||
## 优点
|
||||
|
||||
- 去中心化,不需要统一调度所有 agent。
|
||||
- 对多动态 agent 场景更自然。
|
||||
- 使用速度空间建模,可解释性比纯排斥力强。
|
||||
- 相比普通 RVO,更重视减少对称避让中的振荡。
|
||||
- 适合实时局部避障。
|
||||
|
||||
## 局限
|
||||
|
||||
- 它主要是二维平面局部避障算法。
|
||||
- 不负责全局路径规划。
|
||||
- 不理解地图语义、死胡同、长期收益。
|
||||
- 对强动力学约束系统需要额外适配。
|
||||
- 高密度或狭窄通道中仍可能出现局部最优、停滞或拥堵。
|
||||
- 如果 agent 初始已经严重重叠,算法需要时间把重叠解开。
|
||||
|
||||
## 放到 FishROV 中的定位
|
||||
|
||||
在 FishROV 里,HRVO 更适合作为一层局部避障参考,而不是完整运动方案。
|
||||
|
||||
建议定位:
|
||||
|
||||
```text
|
||||
全局目标 / 路径
|
||||
-> 生成期望速度
|
||||
-> HRVO 局部避障修正速度
|
||||
-> FishROV 自身运动控制 / 刚体控制 / 推进器控制
|
||||
```
|
||||
|
||||
也就是说,HRVO 输出的是“当前更安全的速度建议”,不是最终控制指令。
|
||||
|
||||
如果用于 ROV 或鱼群 benchmark,需要特别注意:
|
||||
|
||||
- 如果当前运动是 3D,HRVO 原生二维模型需要投影到 XZ 平面,或扩展为 3D 近似。
|
||||
- 如果存在水流、惯性、最大转向角、加速度限制,需要在 HRVO 输出之后再做动力学约束。
|
||||
- 如果有地形、洞穴、障碍物、任务点顺序,仍然需要全局或中层规划。
|
||||
- 如果 agent 高度层不同,不能只看 XZ 重叠,否则会把视觉上错层的对象也当成冲突。
|
||||
|
||||
## 对自研避障算法的启发
|
||||
|
||||
HRVO 最值得吸收的不是具体代码,而是这三个思想:
|
||||
|
||||
```text
|
||||
1. 在速度空间里判断未来碰撞,而不是只看当前距离。
|
||||
2. 假设其他动态体也会避让,而不是全部由自己承担。
|
||||
3. 对称避让时要显式处理振荡,不然会左右横跳。
|
||||
```
|
||||
|
||||
如果后续继续做 FishROV 自研局部避障,可以把 HRVO 作为参考方向:
|
||||
|
||||
- 用预测碰撞时间替代单纯距离惩罚。
|
||||
- 把候选速度分成安全区、危险区和高风险区。
|
||||
- 给 reciprocal 避让加入稳定侧偏好,减少频繁换边。
|
||||
- 把“接近期望速度”和“避碰安全”放进同一个评分或约束求解里。
|
||||
- 在 3D 场景中明确区分水平面冲突和真实体积冲突。
|
||||
|
||||
## 参考
|
||||
|
||||
- HRVO 仓库:https://github.com/snape/HRVO
|
||||
- HRVO 项目页:https://gamma.cs.unc.edu/HRVO/
|
||||
- Velocity Obstacle 术语说明:https://en.wikipedia.org/wiki/Velocity_obstacle
|
||||
@@ -2,26 +2,30 @@
|
||||
|
||||
## 目标
|
||||
|
||||
这个 Unity 工程用于横向压测本地避障方案。不同方案之间唯一应该变化的变量,是 `AvoidanceFrameworkKind` 选择后由 `AvoidanceAdapterRegistry` 创建的 adapter。
|
||||
这个 Unity 工程用于压测本地避障方案。当前阶段先收束到两条主线:
|
||||
|
||||
- `Rvo2Adapter`:成熟 RVO2 / ORCA 基准。
|
||||
- 项目自研 RVO2 思想版本:用于验证可控性、可调参空间和性能上限。
|
||||
|
||||
当前工程入口只保留 `NoAvoidance`、`Rvo2` 和自研版。不同方案之间唯一应该变化的变量,是 `AvoidanceFrameworkKind` 选择后由 `AvoidanceAdapterRegistry` 创建的 adapter。
|
||||
|
||||
## 共享运行入口
|
||||
|
||||
- 场景:`Assets/Scenes/AvoidancePerfDemo.unity`
|
||||
- 入口脚本:`AvoidancePerfBootstrap`
|
||||
- Game 视口调试面板:
|
||||
- 避障方案选择
|
||||
- 避障方案选择:无避障、RVO2、自研版
|
||||
- 测试场景选择
|
||||
- 2D / 2.5D / 3D 模式
|
||||
- 观察视角切换
|
||||
- 小鱼数量预设和滑条
|
||||
- 鱼数量预设和滑条
|
||||
- 暂停 / 继续 / 重置
|
||||
- 平均 FPS、1% Low FPS、模拟耗时、GC 分配
|
||||
- 碰撞调试:XZ 平面重叠对数、3D 空间重叠对数、最近清空间距
|
||||
- 表现对象:鱼群为运行时圆锥,圆锥尖头表示朝向;鲨鱼为 Capsule。
|
||||
- 表现对象:鱼为运行时圆锥,圆锥尖头表示朝向。
|
||||
- 共享场景:
|
||||
- 自由巡游
|
||||
- 对向穿流
|
||||
- 鲨鱼冲群
|
||||
- 狭窄通道
|
||||
- 圆阵高压
|
||||
|
||||
@@ -36,7 +40,7 @@ Assets/Scripts/Benchmark/FrameworkAdapters/
|
||||
adapter 可以给运行时创建的对象追加框架自己的组件,但不应改变:
|
||||
|
||||
- 场景生成逻辑
|
||||
- 小鱼/鲨鱼数量档
|
||||
- 鱼数量档
|
||||
- Game 视口调试控件
|
||||
- 指标名称
|
||||
- 相机/视角模式
|
||||
@@ -46,16 +50,42 @@ adapter 可以给运行时创建的对象追加框架自己的组件,但不应
|
||||
|
||||
`NoAvoidanceAdapter` 是对照基线:只按期望速度移动,不做避障。
|
||||
|
||||
A* RVO、RVO2、Unity NavMesh、采样避障学习版都通过同一个调试面板选择,用同一套场景、数量、相机和指标进行对比。
|
||||
当前压测结论主线只比较 RVO2 原版和基于 RVO2 思想的自研版本。编辑器压测入口已经只保留无避障、RVO2 和自研版,避免无关选项干扰当前结论。
|
||||
|
||||
## A* Pathfinding Project Pro RVO Adapter
|
||||
## 当前压测口径
|
||||
|
||||
`AstarRvoAdapter` 位于 `Assets/Scripts/Benchmark/FrameworkAdapters/`,由 `AvoidanceFrameworkKind.AstarRvo` 选择。
|
||||
- 分辨率:标准移动端竖屏 `1080x1920`。
|
||||
- 环境:先在 Unity 编辑器下跑同一套场景,再到百元机真机复测。
|
||||
- 场景:理想空旷、双向穿流、圆阵向心、高密度聚集、初始重叠、狭窄通道、单目标争抢。
|
||||
- 数量档:建议至少覆盖 `100 / 500 / 1000 / 3000 / 5000`。
|
||||
- 指标:平均 FPS、1% Low FPS、模拟耗时、最大模拟耗时、GC 分配、XZ 平面重叠对数、3D 空间重叠对数、最近清空间距、平均速度损失。
|
||||
- 公平条件:半径、最大速度、`timeStep`、目标生成、积分方式、邻居范围、日志采样策略必须一致。
|
||||
|
||||
- adapter 通过反射查找 `Pathfinding.RVO.RVOController` 和 `Pathfinding.RVO.RVOSimulator`,所以没有导入 A* Pro 时工程仍可编译。
|
||||
- 如果缺少 A* Pro RVO 类型,Game 视口状态会提示缺包,并退回基线移动。
|
||||
- 如果类型存在,adapter 会创建运行时 `RVOSimulator`,给 benchmark 对象添加 `RVOController`,把期望速度送入 A* RVO,再把反射得到的避障速度写回 `BenchmarkAgent.ApplyVelocity`。
|
||||
- 共享场景、HUD、数量档、指标和相机逻辑不变。
|
||||
## 日志策略
|
||||
|
||||
编辑器和百元机都需要日志,但日志不能拖慢性能测试。
|
||||
|
||||
- 日志目录:`Application.persistentDataPath/AvoidanceBenchmarkLogs/`。
|
||||
- 编辑器下通常在用户目录的 `AppData/LocalLow/.../AvoidanceBenchmarkLogs/`。
|
||||
- 真机下在 App 的 persistent data path 里,使用面板的“导出日志”按钮或结束/重置测试自动写出。
|
||||
- 不要每帧 `Debug.Log`。
|
||||
- 不要在热路径里拼接字符串。
|
||||
- 不要每帧写文件。
|
||||
- 不要让某个 adapter 单独输出额外明细,导致对比不公平。
|
||||
- 建议只在内存中记录结构化数值样本。
|
||||
- 当前实现固定每 `0.5s` 采样一次,先写入内存列表。
|
||||
- 单轮重置、退出或点击“导出日志”时一次性导出 CSV。
|
||||
- Console 只打印一行摘要,例如算法、场景、数量、平均 FPS、1% Low、平均模拟耗时、最大耗时、重叠对峰值。
|
||||
|
||||
百元机真机日志建议额外记录:
|
||||
|
||||
- 设备型号。
|
||||
- 目标帧率。
|
||||
- 实际分辨率是否为 `1080x1920`。
|
||||
- Unity 图形 API。
|
||||
- 是否 Development Build。
|
||||
- 是否连接调试器。
|
||||
- 是否出现明显发热、降频或后台干扰。
|
||||
|
||||
## RVO2 Adapter
|
||||
|
||||
@@ -65,20 +95,11 @@ A* RVO、RVO2、Unity NavMesh、采样避障学习版都通过同一个调试面
|
||||
- 每帧设置期望速度前会同步 Unity Transform 位置,避免 RVO 内部位置和 benchmark 边界夹取结果漂移。
|
||||
- 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` 是项目内自写的学习算法,用来和成熟框架做横向对比。
|
||||
|
||||
- 使用空间哈希查找近邻,避免全量 O(n²) 扫描。
|
||||
- 对每个 agent 采样多组候选速度,并用目标偏差、速度平滑、预测碰撞、当前重叠、边界越界惩罚打分。
|
||||
- 它不是完整 ORCA,也不保证无碰撞;目的在于提供一个可读、可改、可调参的学习基线。
|
||||
- 如果它在某些场景比 RVO2 或 NavMesh 更稳,只能说明当前启发式适合该场景,不代表算法理论上更强。
|
||||
- 它不是完整 ORCA,也不保证无碰撞;当前只作为自研候选的弱基线,表现明显差于 RVO2 是预期内结果。
|
||||
- 如果它在某些场景比 RVO2 更稳,只能说明当前启发式适合该场景,不代表算法理论上更强。
|
||||
|
||||
230
FishROV/Docs/rl-rvo-nav-core-principles.md
Normal file
230
FishROV/Docs/rl-rvo-nav-core-principles.md
Normal file
@@ -0,0 +1,230 @@
|
||||
# rl_rvo_nav 核心原理说明
|
||||
|
||||
资料来源:
|
||||
|
||||
- GitHub 仓库:https://github.com/hanruihua/rl_rvo_nav
|
||||
- 论文:Reinforcement Learned Distributed Multi-Robot Navigation with Reciprocal Velocity Obstacle Shaped Rewards
|
||||
- arXiv 页面:https://arxiv.org/abs/2203.10229
|
||||
|
||||
## 一句话结论
|
||||
|
||||
`rl_rvo_nav` 的核心不是纯强化学习避障,也不是传统 RVO/ORCA 求解器,而是把 **RVO/VO 几何避障先验** 编码进强化学习的状态表示和奖励函数中,让 PPO 训练出的策略在多机器人局部避障中更安全、更高效、更不保守。
|
||||
|
||||
可以理解为:
|
||||
|
||||
> RVO 负责告诉策略“哪些速度区域有碰撞风险”,强化学习负责学“在这些风险结构下,当前该怎么选一个更好的速度增量”。
|
||||
|
||||
## 背景问题
|
||||
|
||||
多机器人导航常见有两类方案:
|
||||
|
||||
- 中心化调度:全局规划能力强,但机器人数量增加后计算和通信压力上升。
|
||||
- 分布式避障:每个机器人独立决策,扩展性好,但只能依靠局部观测,容易出现碰撞、震荡、过度保守或互相等待。
|
||||
|
||||
传统 VO/RVO/ORCA 方法会在速度空间中构造不可选速度区域,从而实时选择避障速度。它们计算高效、可解释,但在复杂场景中可能保守,对感知误差、非完整约束和长期收益的处理也有限。
|
||||
|
||||
纯深度强化学习方法可以通过大量仿真经验学到更激进、更灵活的策略,但如果直接输入位置、速度、距离等原始状态,网络需要自己学会推导碰撞几何关系,样本效率和可控性都不理想。
|
||||
|
||||
`rl_rvo_nav` 的设计目标就是结合两者:
|
||||
|
||||
- 用 RVO/VO 给强化学习提供结构化避障知识。
|
||||
- 用 DRL 学出比手写几何规则更灵活的局部控制策略。
|
||||
|
||||
## 核心模块
|
||||
|
||||
### 1. 分布式多机器人决策
|
||||
|
||||
每个机器人独立运行同一个策略,不依赖中心控制器,也不依赖机器人之间的通信。
|
||||
|
||||
机器人只使用自身状态和局部感知范围内的其他机器人、障碍物信息进行决策。
|
||||
|
||||
自身状态通常包括:
|
||||
|
||||
- 当前速度
|
||||
- 当前朝向
|
||||
- 期望速度
|
||||
- 用于避障的虚拟半径
|
||||
|
||||
周围环境状态包括:
|
||||
|
||||
- 动态机器人产生的 RVO 向量
|
||||
- 静态障碍物产生的 VO 向量
|
||||
- 相对距离
|
||||
- 预计碰撞时间
|
||||
|
||||
### 2. 用 RVO/VO 表示碰撞风险
|
||||
|
||||
传统 RL 避障常把邻居机器人表示成位置、速度、半径等数据,例如:
|
||||
|
||||
```text
|
||||
other_position, other_velocity, other_radius
|
||||
```
|
||||
|
||||
`rl_rvo_nav` 更进一步,把这些信息转换成速度空间中的 VO/RVO 表示。
|
||||
|
||||
VO 表示:
|
||||
|
||||
- 如果自身选择某些速度,就会在未来一段时间内撞到障碍物。
|
||||
- 这些危险速度构成一个速度空间区域。
|
||||
|
||||
RVO 表示:
|
||||
|
||||
- 对动态机器人,不只假设“我避让它”,而是假设双方都会承担一部分避让责任。
|
||||
- 这让策略更容易学到互惠避障行为,减少单方过度避让或互相卡住。
|
||||
|
||||
因此,网络输入不是单纯的几何坐标,而是更接近“碰撞约束”的状态。
|
||||
|
||||
这也是该方案最重要的思想:**把避障问题从物理空间状态学习,转成速度空间风险学习。**
|
||||
|
||||
### 3. BiGRU 处理可变数量邻居
|
||||
|
||||
周围机器人和障碍物数量是不固定的。普通 MLP 需要固定长度输入,不适合直接处理可变数量的邻居。
|
||||
|
||||
该方案把周围对象的 VO/RVO 信息组织成序列,再用 RNN 模块提取特征。
|
||||
|
||||
论文强调使用 bidirectional GRU,也就是 BiGRU。仓库代码中的 `policy_rnn_ac.py` 里也能看到 `GRU`、`LSTM`、`biGRU` 模式。
|
||||
|
||||
结构大致是:
|
||||
|
||||
```text
|
||||
自身状态
|
||||
+
|
||||
周围对象 VO/RVO 序列
|
||||
-> GRU/BiGRU
|
||||
-> 固定维度环境特征
|
||||
-> Actor 输出动作分布
|
||||
-> Critic 输出状态价值
|
||||
```
|
||||
|
||||
BiGRU 的作用是:
|
||||
|
||||
- 能处理数量变化的邻居。
|
||||
- 不只偏向序列末尾对象。
|
||||
- 在较低计算成本下提取局部交互特征。
|
||||
|
||||
### 4. 动作输出是速度增量
|
||||
|
||||
很多 RL 避障方法直接输出最终控制速度。
|
||||
|
||||
`rl_rvo_nav` 输出的是当前速度的增量,也就是 velocity increment/change rate。
|
||||
|
||||
这样做的好处:
|
||||
|
||||
- 控制更平滑。
|
||||
- 不容易出现速度突变。
|
||||
- 更接近差速机器人实际控制需求。
|
||||
|
||||
最终速度再被分解成差速机器人的线速度和角速度。
|
||||
|
||||
### 5. 奖励函数由 RVO 区域和预计碰撞时间塑形
|
||||
|
||||
该方案的奖励函数不是简单距离奖励。
|
||||
|
||||
传统距离奖励大致是:
|
||||
|
||||
- 离目标更近,奖励。
|
||||
- 离障碍更近,惩罚。
|
||||
- 碰撞,强惩罚。
|
||||
|
||||
但 `rl_rvo_nav` 的输入本身是 VO/RVO 表示,不直接以原始位置距离为核心。因此它设计了基于 RVO/VO 区域的奖励。
|
||||
|
||||
奖励重点判断:
|
||||
|
||||
- 当前选择的速度是否落入 RVO/VO 危险区域。
|
||||
- 当前速度与期望速度的差距。
|
||||
- 按当前运动趋势,预计多久会发生碰撞。
|
||||
|
||||
预计碰撞时间比单纯距离更有信息量,因为它同时考虑了距离和相对速度。
|
||||
|
||||
例如:
|
||||
|
||||
- 距离近但相对远离,风险不一定高。
|
||||
- 距离远但高速相向,风险可能很高。
|
||||
|
||||
所以该方案用 expected collision time 来衡量碰撞风险,使策略在拥挤环境里能更合理地权衡安全和效率。
|
||||
|
||||
## 训练方式
|
||||
|
||||
仓库使用 PPO 训练 actor-critic 策略。
|
||||
|
||||
训练流程大致如下:
|
||||
|
||||
1. 在仿真环境中让多个机器人运行当前策略。
|
||||
2. 收集每个机器人的 observation、action、reward。
|
||||
3. 使用 GAE 计算优势。
|
||||
4. 用 PPO 更新 actor 和 critic。
|
||||
5. 在更复杂机器人数量和场景中继续训练。
|
||||
|
||||
README 中给出的训练阶段是:
|
||||
|
||||
- 第一阶段:4 个机器人 circle 场景。
|
||||
- 第二阶段:加载已有模型,在 10 个机器人 circle 场景继续训练。
|
||||
|
||||
测试场景包括:
|
||||
|
||||
- circle scenario
|
||||
- random scenario
|
||||
|
||||
## 与传统 RVO/ORCA 的区别
|
||||
|
||||
| 维度 | 传统 RVO/ORCA | rl_rvo_nav |
|
||||
| --- | --- | --- |
|
||||
| 决策方式 | 手写几何规则或优化求解 | 神经网络策略 |
|
||||
| 避障知识 | 显式速度空间约束 | 作为状态和奖励先验 |
|
||||
| 输出 | 当前最优避障速度 | 速度增量 |
|
||||
| 优点 | 可解释、稳定、实时 | 更灵活,可从经验中学习效率和风险权衡 |
|
||||
| 风险 | 可能保守 | 依赖训练覆盖和仿真质量 |
|
||||
|
||||
## 与纯强化学习避障的区别
|
||||
|
||||
| 维度 | 纯 DRL 避障 | rl_rvo_nav |
|
||||
| --- | --- | --- |
|
||||
| 输入 | 位置、速度、距离、传感器数据等 | VO/RVO 风险向量 |
|
||||
| 学习难度 | 网络需自行推导碰撞几何 | 几何风险已经结构化 |
|
||||
| 奖励 | 常基于距离和碰撞结果 | 基于 RVO 区域和预计碰撞时间 |
|
||||
| 可解释性 | 较弱 | 更容易解释为速度空间风险规避 |
|
||||
|
||||
## 对 FishROV 的启发
|
||||
|
||||
如果把这个方案借鉴到 FishROV 或水下 ROV 局部避障中,最值得吸收的不是直接照搬 PPO 训练,而是以下思想:
|
||||
|
||||
1. **把避障风险表达在速度空间中**
|
||||
|
||||
不只判断“障碍在哪里”,而是判断“当前可选速度里哪些会导致未来碰撞”。
|
||||
|
||||
2. **把规则避障和学习策略分层**
|
||||
|
||||
几何规则可以生成风险特征,学习策略负责在风险、目标方向、速度平滑性之间做权衡。
|
||||
|
||||
3. **优先考虑可解释的 reward/score**
|
||||
|
||||
即使不用 RL,也可以把 RVO 区域、预计碰撞时间、目标速度差异组合成局部避障评分函数。
|
||||
|
||||
4. **对动态障碍比静态障碍更有价值**
|
||||
|
||||
如果 FishROV 主要面对静态地形或固定障碍,传统 NavMesh、A*、局部 steering 可能更直接。
|
||||
|
||||
如果未来要处理鱼群、其他 ROV、移动机械臂、动态水流漂浮物等,RVO/RL-RVO 的价值会明显上升。
|
||||
|
||||
## 局限和注意点
|
||||
|
||||
该方案也有明显门槛:
|
||||
|
||||
- 需要高质量仿真环境。
|
||||
- 策略泛化依赖训练场景覆盖。
|
||||
- PPO 训练成本高于传统局部避障。
|
||||
- 真实机器人部署需要考虑感知误差、控制延迟、水动力扰动等 sim-to-real 问题。
|
||||
- 对游戏或仿真项目来说,直接使用 RL 模型未必划算,抽取其 RVO 风险建模思想可能更实用。
|
||||
|
||||
## 简短评价
|
||||
|
||||
`rl_rvo_nav` 最核心的价值是提出了一种很清楚的混合范式:
|
||||
|
||||
```text
|
||||
传统几何避障知识
|
||||
-> 结构化状态表示
|
||||
-> RVO/碰撞时间塑形奖励
|
||||
-> PPO 学习连续局部控制策略
|
||||
```
|
||||
|
||||
它适合研究或高复杂度多机器人动态避障。如果当前目标是工程落地,建议优先把它当作“设计参考”:吸收 VO/RVO 风险表达和 expected collision time 评分,而不是第一步就引入完整 RL 训练链路。
|
||||
280
FishROV/Docs/rvo-py-mas-core-principles.md
Normal file
280
FishROV/Docs/rvo-py-mas-core-principles.md
Normal file
@@ -0,0 +1,280 @@
|
||||
# RVO_Py_MAS 核心原理
|
||||
|
||||
## 一句话结论
|
||||
|
||||
`RVO_Py_MAS` 的核心是 **在速度空间里排除未来会碰撞的速度,然后选择一个最接近期望速度的安全速度**。
|
||||
|
||||
它不是全局寻路系统,也不负责生成完整路径。它更像一个局部避障控制器:上层系统先给每个 agent 一个期望速度,RVO 再根据周围动态 agent 和静态障碍,把这个速度修正成更安全的实际速度。
|
||||
|
||||
典型接入方式是:
|
||||
|
||||
```text
|
||||
目标/任务控制器
|
||||
-> 计算期望速度 V_desired
|
||||
-> RVO_Py_MAS 根据邻居和障碍修正速度
|
||||
-> 输出实际速度 V
|
||||
-> 推进位置
|
||||
```
|
||||
|
||||
## 它要解决的问题
|
||||
|
||||
普通 steering 或分离力算法通常在位置空间工作:
|
||||
|
||||
```text
|
||||
离目标近一点
|
||||
离邻居远一点
|
||||
离障碍远一点
|
||||
最后把多个力合成一个速度
|
||||
```
|
||||
|
||||
这类方法简单,但容易出现几个问题:
|
||||
|
||||
- 只看当前距离,不一定能判断未来是否会撞上。
|
||||
- 多个 agent 同时互相躲避时,可能左右摇摆。
|
||||
- 高密度场景下,分离力叠加后速度容易变乱。
|
||||
- 很难回答“这个速度在未来一小段时间内是否安全”。
|
||||
|
||||
RVO 的视角不同。它不直接问“我现在离别人多近”,而是问:
|
||||
|
||||
```text
|
||||
如果我选择这个速度,
|
||||
在未来一段时间内会不会撞到别人?
|
||||
```
|
||||
|
||||
## Velocity Obstacle
|
||||
|
||||
对两个 agent A 和 B:
|
||||
|
||||
- A 的位置是 `pA`,速度是 `vA`,半径是 `rA`。
|
||||
- B 的位置是 `pB`,速度是 `vB`,半径是 `rB`。
|
||||
- 如果未来某个时间点,两者中心距离小于 `rA + rB`,就会发生碰撞。
|
||||
|
||||
Velocity Obstacle 的意思是:
|
||||
|
||||
```text
|
||||
对 A 来说,有一片速度区域是危险的。
|
||||
如果 A 选择这片区域内的速度,
|
||||
并且 B 按当前趋势运动,
|
||||
A 未来就可能撞到 B。
|
||||
```
|
||||
|
||||
这片危险区域可以理解成速度空间里的一个锥形区域。锥的方向由 A 指向 B 的相对位置决定,锥的张角由双方半径和距离决定。
|
||||
|
||||
## RVO:双方共同承担避让
|
||||
|
||||
普通 VO 假设“对方保持原样,我单方面避让”。如果所有 agent 都这么算,就容易过度避让或振荡。
|
||||
|
||||
RVO 的关键变化是:
|
||||
|
||||
```text
|
||||
A 和 B 都是主动体。
|
||||
如果未来可能碰撞,不应该只让 A 负责全部避让。
|
||||
双方都假设对方也会做类似避让,因此各自承担一部分速度修正。
|
||||
```
|
||||
|
||||
在 `RVO_Py_MAS` 代码里,RVO 版本的速度障碍平移点使用了双方当前速度的平均项:
|
||||
|
||||
```python
|
||||
transl_vB_vA = [
|
||||
pA[0] + 0.5 * (vB[0] + vA[0]),
|
||||
pA[1] + 0.5 * (vB[1] + vA[1]),
|
||||
]
|
||||
```
|
||||
|
||||
这就是 reciprocal 的工程化表达:不是单边躲避,而是假设双方都会动一点。
|
||||
|
||||
## 每个 agent 的计算流程
|
||||
|
||||
`RVO_update` 对每个 agent 做下面几步:
|
||||
|
||||
1. 读取当前 agent 的位置 `pA` 和当前速度 `vA`。
|
||||
2. 遍历其他 agent。
|
||||
3. 为每个邻居构造一个 RVO 速度障碍。
|
||||
4. 遍历静态圆形障碍,也构造速度障碍。
|
||||
5. 调用 `intersect`,从候选速度里选择最合适的速度。
|
||||
6. 把结果写回输出速度列表。
|
||||
|
||||
可以简化成:
|
||||
|
||||
```text
|
||||
对每条鱼:
|
||||
收集周围会影响它的动态体和静态障碍
|
||||
把这些对象都转成速度空间中的禁区
|
||||
找一个不在禁区里、又最接近期望速度的速度
|
||||
```
|
||||
|
||||
## 动态 agent 如何形成禁区
|
||||
|
||||
对每个邻居 B,代码会计算:
|
||||
|
||||
- `dist_BA`:A 和 B 的距离。
|
||||
- `theta_BA`:A 指向 B 的方向角。
|
||||
- `theta_BAort`:碰撞锥的半角。
|
||||
- `bound_left` / `bound_right`:碰撞锥左右边界。
|
||||
|
||||
然后把这个速度障碍存成:
|
||||
|
||||
```python
|
||||
RVO_BA = [
|
||||
transl_vB_vA,
|
||||
bound_left,
|
||||
bound_right,
|
||||
dist_BA,
|
||||
2 * ROB_RAD,
|
||||
]
|
||||
```
|
||||
|
||||
其中 `2 * ROB_RAD` 表示两个相同半径 agent 的安全距离。代码里还给机器人半径额外加了 `0.1`,相当于保守安全边距。
|
||||
|
||||
## 静态障碍如何处理
|
||||
|
||||
静态障碍被当成速度为 0 的对象:
|
||||
|
||||
```python
|
||||
vB = [0, 0]
|
||||
```
|
||||
|
||||
圆形障碍直接用自身半径加 agent 半径形成安全距离。代码里还有一个 `OVER_APPROX_C2S = 1.5`,用于把某些障碍做更保守的圆形近似。
|
||||
|
||||
所以动态 agent 和静态障碍最终都被转成同一种东西:
|
||||
|
||||
```text
|
||||
速度空间里的危险区域
|
||||
```
|
||||
|
||||
这让算法主体可以统一处理。
|
||||
|
||||
## 候选速度选择
|
||||
|
||||
`intersect` 函数不是做严格的数学优化,而是在速度空间里采样候选速度:
|
||||
|
||||
```python
|
||||
for theta in numpy.arange(0, 2 * PI, 0.1):
|
||||
for rad in numpy.arange(0.02, norm_v + 0.02, norm_v / 5.0):
|
||||
new_v = [rad * cos(theta), rad * sin(theta)]
|
||||
```
|
||||
|
||||
也就是:
|
||||
|
||||
```text
|
||||
按不同方向 theta 采样
|
||||
按不同速度大小 rad 采样
|
||||
组合成一批候选速度
|
||||
```
|
||||
|
||||
每个候选速度都会拿去检查:它是否落在任意一个 RVO 禁区里。
|
||||
|
||||
- 没落入禁区:放入 `suitable_V`。
|
||||
- 落入禁区:放入 `unsuitable_V`。
|
||||
|
||||
## 如果有安全速度
|
||||
|
||||
如果存在 `suitable_V`,算法选一个最接近期望速度 `vA` 的速度:
|
||||
|
||||
```python
|
||||
vA_post = min(suitable_V, key=lambda v: distance(v, vA))
|
||||
```
|
||||
|
||||
这体现了 README 里说的“minimal modification of desired velocity”:
|
||||
|
||||
```text
|
||||
能不改就不改。
|
||||
必须避让时,也尽量少改。
|
||||
```
|
||||
|
||||
## 如果没有安全速度
|
||||
|
||||
高密度或被包围时,可能没有任何候选速度完全安全。
|
||||
|
||||
这时算法会从 `unsuitable_V` 里选一个折中速度。它会估计每个不安全速度的碰撞时间 `tc`,然后用下面的代价选择:
|
||||
|
||||
```python
|
||||
(WT / tc) + distance(v, vA)
|
||||
```
|
||||
|
||||
含义是:
|
||||
|
||||
```text
|
||||
尽量晚碰撞
|
||||
同时尽量接近期望速度
|
||||
```
|
||||
|
||||
这不是严格安全保证,而是“没有好选择时选一个相对不坏的选择”。
|
||||
|
||||
## VO / RVO / HRVO 的区别
|
||||
|
||||
这个仓库的代码里保留了三种思路的切换痕迹:
|
||||
|
||||
| 方法 | 核心假设 | 工程表现 |
|
||||
| --- | --- | --- |
|
||||
| VO | 对方保持当前速度,我单方避让 | 简单,但多主动体容易过度避让 |
|
||||
| RVO | 双方都会避让,各承担一部分修正 | 更适合多 agent 对等场景 |
|
||||
| HRVO | 混合 VO 和 RVO,减少振荡和死锁 | 理论上更稳,但实现略复杂 |
|
||||
|
||||
当前代码默认使用 RVO。普通 VO 和 HRVO 的部分在代码里以注释形式保留。
|
||||
|
||||
## 优点
|
||||
|
||||
- 思路直观,代码量小,适合学习和改造。
|
||||
- 不需要复杂全局地图,只依赖局部位置、速度、半径。
|
||||
- 上层控制目标可以保持独立,只需要提供期望速度。
|
||||
- 对多 agent 场景比单纯分离力更有“未来碰撞预测”意识。
|
||||
- 动态 agent 和静态障碍可以统一成速度空间约束。
|
||||
|
||||
## 局限
|
||||
|
||||
- 这是局部避障,不是全局寻路。
|
||||
- 候选速度靠采样,可能错过真正更优的速度。
|
||||
- 高密度场景可能没有完全安全速度,只能选折中。
|
||||
- 代码没有空间索引,agent 数量大时两两遍历会变贵。
|
||||
- 它不是 ORCA/RVO2 那种半平面约束加线性规划求解。
|
||||
- 参数、步长、最大速度不合适时仍可能碰撞或卡住。
|
||||
|
||||
## 和 RVO2 / ORCA 的区别
|
||||
|
||||
| 维度 | RVO_Py_MAS | RVO2 / ORCA |
|
||||
| --- | --- | --- |
|
||||
| 核心方式 | 采样候选速度,过滤禁区 | 构造半平面约束,求最近安全速度 |
|
||||
| 实现复杂度 | 低 | 更高 |
|
||||
| 可读性 | 很直观 | 数学和工程结构更复杂 |
|
||||
| 理论性质 | 较弱 | 更强 |
|
||||
| 性能 | 小规模足够,规模大需优化 | 更成熟,适合大量 agent |
|
||||
| 学习价值 | 很高,适合理解速度障碍 | 更适合工程使用和高性能仿真 |
|
||||
|
||||
## 对 FishROV 的启发
|
||||
|
||||
如果 FishROV 后续要做自研鱼群避障,可以吸收它的核心结构:
|
||||
|
||||
```text
|
||||
目标速度
|
||||
-> 构造邻居/障碍的未来碰撞风险
|
||||
-> 在速度空间选择替代速度
|
||||
-> 优先保持目标意图
|
||||
-> 只有必要时才避让
|
||||
```
|
||||
|
||||
更适合 FishROV 的工程版本可以考虑:
|
||||
|
||||
- 加空间哈希或网格邻居查询,避免全量两两比较。
|
||||
- 把 3D 鱼群投影到 XZ 平面做水平避障,再单独处理高度层。
|
||||
- 在候选速度里加入最大转向角、最大加速度、速度平滑约束。
|
||||
- 对不同鱼群状态加权,例如巡游、逃逸、聚集、追逐。
|
||||
- 增加右侧/左侧通行偏置,减少对称场景下的互相卡住。
|
||||
- 把“预计碰撞时间”和“当前重叠深度”作为调试指标输出。
|
||||
|
||||
## 结论
|
||||
|
||||
`RVO_Py_MAS` 最值得学习的不是它的具体 Python 代码,而是它的建模方式:
|
||||
|
||||
```text
|
||||
不要只在位置空间推开物体。
|
||||
要在速度空间判断:哪些速度会导致未来碰撞。
|
||||
```
|
||||
|
||||
它适合作为理解 RVO 思想和自研局部避障算法的入门参考;如果目标是直接上生产级大规模 agent 避障,RVO2 / ORCA 或项目内针对鱼群特征优化后的采样避障会更合适。
|
||||
|
||||
## 参考
|
||||
|
||||
- [MengGuo/RVO_Py_MAS](https://github.com/MengGuo/RVO_Py_MAS)
|
||||
- [RVO_Py_MAS/RVO.py](https://github.com/MengGuo/RVO_Py_MAS/blob/master/RVO.py)
|
||||
- [Reciprocal Velocity Obstacles for Real-Time Multi-Agent Navigation](https://gamma.cs.unc.edu/RVO/)
|
||||
295
FishROV/Packages/packages-lock.json
Normal file
295
FishROV/Packages/packages-lock.json
Normal file
@@ -0,0 +1,295 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"com.unity.ext.nunit": {
|
||||
"version": "1.0.6",
|
||||
"depth": 1,
|
||||
"source": "registry",
|
||||
"dependencies": {},
|
||||
"url": "https://packages.unity.cn"
|
||||
},
|
||||
"com.unity.test-framework": {
|
||||
"version": "1.1.33",
|
||||
"depth": 0,
|
||||
"source": "registry",
|
||||
"dependencies": {
|
||||
"com.unity.ext.nunit": "1.0.6",
|
||||
"com.unity.modules.imgui": "1.0.0",
|
||||
"com.unity.modules.jsonserialize": "1.0.0"
|
||||
},
|
||||
"url": "https://packages.unity.cn"
|
||||
},
|
||||
"com.unity.textmeshpro": {
|
||||
"version": "3.0.7",
|
||||
"depth": 0,
|
||||
"source": "registry",
|
||||
"dependencies": {
|
||||
"com.unity.ugui": "1.0.0"
|
||||
},
|
||||
"url": "https://packages.unity.cn"
|
||||
},
|
||||
"com.unity.timeline": {
|
||||
"version": "1.7.7",
|
||||
"depth": 0,
|
||||
"source": "registry",
|
||||
"dependencies": {
|
||||
"com.unity.modules.audio": "1.0.0",
|
||||
"com.unity.modules.director": "1.0.0",
|
||||
"com.unity.modules.animation": "1.0.0",
|
||||
"com.unity.modules.particlesystem": "1.0.0"
|
||||
},
|
||||
"url": "https://packages.unity.cn"
|
||||
},
|
||||
"com.unity.ugui": {
|
||||
"version": "1.0.0",
|
||||
"depth": 0,
|
||||
"source": "builtin",
|
||||
"dependencies": {
|
||||
"com.unity.modules.ui": "1.0.0",
|
||||
"com.unity.modules.imgui": "1.0.0"
|
||||
}
|
||||
},
|
||||
"com.unity.modules.ai": {
|
||||
"version": "1.0.0",
|
||||
"depth": 0,
|
||||
"source": "builtin",
|
||||
"dependencies": {}
|
||||
},
|
||||
"com.unity.modules.androidjni": {
|
||||
"version": "1.0.0",
|
||||
"depth": 0,
|
||||
"source": "builtin",
|
||||
"dependencies": {}
|
||||
},
|
||||
"com.unity.modules.animation": {
|
||||
"version": "1.0.0",
|
||||
"depth": 0,
|
||||
"source": "builtin",
|
||||
"dependencies": {}
|
||||
},
|
||||
"com.unity.modules.assetbundle": {
|
||||
"version": "1.0.0",
|
||||
"depth": 0,
|
||||
"source": "builtin",
|
||||
"dependencies": {}
|
||||
},
|
||||
"com.unity.modules.audio": {
|
||||
"version": "1.0.0",
|
||||
"depth": 0,
|
||||
"source": "builtin",
|
||||
"dependencies": {}
|
||||
},
|
||||
"com.unity.modules.cloth": {
|
||||
"version": "1.0.0",
|
||||
"depth": 0,
|
||||
"source": "builtin",
|
||||
"dependencies": {
|
||||
"com.unity.modules.physics": "1.0.0"
|
||||
}
|
||||
},
|
||||
"com.unity.modules.director": {
|
||||
"version": "1.0.0",
|
||||
"depth": 0,
|
||||
"source": "builtin",
|
||||
"dependencies": {
|
||||
"com.unity.modules.audio": "1.0.0",
|
||||
"com.unity.modules.animation": "1.0.0"
|
||||
}
|
||||
},
|
||||
"com.unity.modules.imageconversion": {
|
||||
"version": "1.0.0",
|
||||
"depth": 0,
|
||||
"source": "builtin",
|
||||
"dependencies": {}
|
||||
},
|
||||
"com.unity.modules.imgui": {
|
||||
"version": "1.0.0",
|
||||
"depth": 0,
|
||||
"source": "builtin",
|
||||
"dependencies": {}
|
||||
},
|
||||
"com.unity.modules.jsonserialize": {
|
||||
"version": "1.0.0",
|
||||
"depth": 0,
|
||||
"source": "builtin",
|
||||
"dependencies": {}
|
||||
},
|
||||
"com.unity.modules.particlesystem": {
|
||||
"version": "1.0.0",
|
||||
"depth": 0,
|
||||
"source": "builtin",
|
||||
"dependencies": {}
|
||||
},
|
||||
"com.unity.modules.physics": {
|
||||
"version": "1.0.0",
|
||||
"depth": 0,
|
||||
"source": "builtin",
|
||||
"dependencies": {}
|
||||
},
|
||||
"com.unity.modules.physics2d": {
|
||||
"version": "1.0.0",
|
||||
"depth": 0,
|
||||
"source": "builtin",
|
||||
"dependencies": {}
|
||||
},
|
||||
"com.unity.modules.screencapture": {
|
||||
"version": "1.0.0",
|
||||
"depth": 0,
|
||||
"source": "builtin",
|
||||
"dependencies": {
|
||||
"com.unity.modules.imageconversion": "1.0.0"
|
||||
}
|
||||
},
|
||||
"com.unity.modules.subsystems": {
|
||||
"version": "1.0.0",
|
||||
"depth": 1,
|
||||
"source": "builtin",
|
||||
"dependencies": {
|
||||
"com.unity.modules.jsonserialize": "1.0.0"
|
||||
}
|
||||
},
|
||||
"com.unity.modules.terrain": {
|
||||
"version": "1.0.0",
|
||||
"depth": 0,
|
||||
"source": "builtin",
|
||||
"dependencies": {}
|
||||
},
|
||||
"com.unity.modules.terrainphysics": {
|
||||
"version": "1.0.0",
|
||||
"depth": 0,
|
||||
"source": "builtin",
|
||||
"dependencies": {
|
||||
"com.unity.modules.physics": "1.0.0",
|
||||
"com.unity.modules.terrain": "1.0.0"
|
||||
}
|
||||
},
|
||||
"com.unity.modules.tilemap": {
|
||||
"version": "1.0.0",
|
||||
"depth": 0,
|
||||
"source": "builtin",
|
||||
"dependencies": {
|
||||
"com.unity.modules.physics2d": "1.0.0"
|
||||
}
|
||||
},
|
||||
"com.unity.modules.ui": {
|
||||
"version": "1.0.0",
|
||||
"depth": 0,
|
||||
"source": "builtin",
|
||||
"dependencies": {}
|
||||
},
|
||||
"com.unity.modules.uielements": {
|
||||
"version": "1.0.0",
|
||||
"depth": 0,
|
||||
"source": "builtin",
|
||||
"dependencies": {
|
||||
"com.unity.modules.ui": "1.0.0",
|
||||
"com.unity.modules.imgui": "1.0.0",
|
||||
"com.unity.modules.jsonserialize": "1.0.0"
|
||||
}
|
||||
},
|
||||
"com.unity.modules.umbra": {
|
||||
"version": "1.0.0",
|
||||
"depth": 0,
|
||||
"source": "builtin",
|
||||
"dependencies": {}
|
||||
},
|
||||
"com.unity.modules.unityanalytics": {
|
||||
"version": "1.0.0",
|
||||
"depth": 0,
|
||||
"source": "builtin",
|
||||
"dependencies": {
|
||||
"com.unity.modules.unitywebrequest": "1.0.0",
|
||||
"com.unity.modules.jsonserialize": "1.0.0"
|
||||
}
|
||||
},
|
||||
"com.unity.modules.unitywebrequest": {
|
||||
"version": "1.0.0",
|
||||
"depth": 0,
|
||||
"source": "builtin",
|
||||
"dependencies": {}
|
||||
},
|
||||
"com.unity.modules.unitywebrequestassetbundle": {
|
||||
"version": "1.0.0",
|
||||
"depth": 0,
|
||||
"source": "builtin",
|
||||
"dependencies": {
|
||||
"com.unity.modules.assetbundle": "1.0.0",
|
||||
"com.unity.modules.unitywebrequest": "1.0.0"
|
||||
}
|
||||
},
|
||||
"com.unity.modules.unitywebrequestaudio": {
|
||||
"version": "1.0.0",
|
||||
"depth": 0,
|
||||
"source": "builtin",
|
||||
"dependencies": {
|
||||
"com.unity.modules.unitywebrequest": "1.0.0",
|
||||
"com.unity.modules.audio": "1.0.0"
|
||||
}
|
||||
},
|
||||
"com.unity.modules.unitywebrequesttexture": {
|
||||
"version": "1.0.0",
|
||||
"depth": 0,
|
||||
"source": "builtin",
|
||||
"dependencies": {
|
||||
"com.unity.modules.unitywebrequest": "1.0.0",
|
||||
"com.unity.modules.imageconversion": "1.0.0"
|
||||
}
|
||||
},
|
||||
"com.unity.modules.unitywebrequestwww": {
|
||||
"version": "1.0.0",
|
||||
"depth": 0,
|
||||
"source": "builtin",
|
||||
"dependencies": {
|
||||
"com.unity.modules.unitywebrequest": "1.0.0",
|
||||
"com.unity.modules.unitywebrequestassetbundle": "1.0.0",
|
||||
"com.unity.modules.unitywebrequestaudio": "1.0.0",
|
||||
"com.unity.modules.audio": "1.0.0",
|
||||
"com.unity.modules.assetbundle": "1.0.0",
|
||||
"com.unity.modules.imageconversion": "1.0.0"
|
||||
}
|
||||
},
|
||||
"com.unity.modules.vehicles": {
|
||||
"version": "1.0.0",
|
||||
"depth": 0,
|
||||
"source": "builtin",
|
||||
"dependencies": {
|
||||
"com.unity.modules.physics": "1.0.0"
|
||||
}
|
||||
},
|
||||
"com.unity.modules.video": {
|
||||
"version": "1.0.0",
|
||||
"depth": 0,
|
||||
"source": "builtin",
|
||||
"dependencies": {
|
||||
"com.unity.modules.audio": "1.0.0",
|
||||
"com.unity.modules.ui": "1.0.0",
|
||||
"com.unity.modules.unitywebrequest": "1.0.0"
|
||||
}
|
||||
},
|
||||
"com.unity.modules.vr": {
|
||||
"version": "1.0.0",
|
||||
"depth": 0,
|
||||
"source": "builtin",
|
||||
"dependencies": {
|
||||
"com.unity.modules.jsonserialize": "1.0.0",
|
||||
"com.unity.modules.physics": "1.0.0",
|
||||
"com.unity.modules.xr": "1.0.0"
|
||||
}
|
||||
},
|
||||
"com.unity.modules.wind": {
|
||||
"version": "1.0.0",
|
||||
"depth": 0,
|
||||
"source": "builtin",
|
||||
"dependencies": {}
|
||||
},
|
||||
"com.unity.modules.xr": {
|
||||
"version": "1.0.0",
|
||||
"depth": 0,
|
||||
"source": "builtin",
|
||||
"dependencies": {
|
||||
"com.unity.modules.physics": "1.0.0",
|
||||
"com.unity.modules.jsonserialize": "1.0.0",
|
||||
"com.unity.modules.subsystems": "1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user