Add Astar RVO benchmark adapter
This commit is contained in:
@@ -26,8 +26,8 @@ namespace FishROV.AvoidanceBenchmark
|
||||
|
||||
private void Start()
|
||||
{
|
||||
Application.targetFrameRate = 60;
|
||||
QualitySettings.vSyncCount = 0;
|
||||
Application.targetFrameRate = -1;
|
||||
pendingFishCount = fishCount;
|
||||
metrics.Start();
|
||||
EnsureCameraAndLight();
|
||||
@@ -63,33 +63,34 @@ namespace FishROV.AvoidanceBenchmark
|
||||
private void OnGUI()
|
||||
{
|
||||
GUILayout.BeginArea(new Rect(12, 12, 390, Screen.height - 24), GUI.skin.box);
|
||||
GUILayout.Label("FishROV Local Avoidance Benchmark");
|
||||
GUILayout.Label($"Adapter: {adapter?.Name ?? "None"}");
|
||||
GUILayout.Label($"Status: {adapter?.Status ?? "Not initialized"}");
|
||||
GUILayout.Label($"Agents: fish {fishCount}, shark {sharkCount}, total {agents.Count}");
|
||||
GUILayout.Label($"FPS avg {metrics.AverageFps:F1} | 1% low {metrics.OnePercentLowFps:F1}");
|
||||
GUILayout.Label($"Simulation {metrics.SimulationMs:F2} ms | GC {metrics.GcAllocBytes / 1024f:F1} KB");
|
||||
GUILayout.Label("FishROV 本地避障压测");
|
||||
GUILayout.Label($"当前方案:{adapter?.Name ?? "未初始化"}");
|
||||
GUILayout.Label($"状态:{adapter?.Status ?? "未初始化"}");
|
||||
GUILayout.Label($"对象数量:小鱼 {fishCount},鲨鱼 {sharkCount},总数 {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($"帧率限制:不锁帧,vSync={QualitySettings.vSyncCount},targetFrameRate={Application.targetFrameRate}");
|
||||
|
||||
GUILayout.Space(8);
|
||||
DrawEnumButtons("Framework", framework, value =>
|
||||
DrawEnumButtons("避障方案", framework, value =>
|
||||
{
|
||||
framework = value;
|
||||
ResetBenchmark();
|
||||
});
|
||||
DrawEnumButtons("Scenario", scenario, value =>
|
||||
DrawEnumButtons("测试场景", scenario, value =>
|
||||
{
|
||||
scenario = value;
|
||||
ResetBenchmark();
|
||||
});
|
||||
DrawEnumButtons("Dimension", dimensionMode, value =>
|
||||
DrawEnumButtons("计算维度", dimensionMode, value =>
|
||||
{
|
||||
dimensionMode = value;
|
||||
ResetBenchmark();
|
||||
});
|
||||
DrawEnumButtons("View", viewMode, value => viewMode = value);
|
||||
DrawEnumButtons("观察视角", viewMode, value => viewMode = value);
|
||||
|
||||
GUILayout.Space(8);
|
||||
GUILayout.Label($"Fish Count: {pendingFishCount}");
|
||||
GUILayout.Label($"小鱼数量:{pendingFishCount}");
|
||||
GUILayout.BeginHorizontal();
|
||||
foreach (var preset in CountPresets)
|
||||
{
|
||||
@@ -105,18 +106,18 @@ namespace FishROV.AvoidanceBenchmark
|
||||
pendingFishCount = Mathf.RoundToInt(GUILayout.HorizontalSlider(pendingFishCount, 10, 5000));
|
||||
|
||||
GUILayout.BeginHorizontal();
|
||||
if (GUILayout.Button("Apply Count"))
|
||||
if (GUILayout.Button("应用数量"))
|
||||
{
|
||||
fishCount = pendingFishCount;
|
||||
ResetBenchmark();
|
||||
}
|
||||
|
||||
if (GUILayout.Button(paused ? "Resume" : "Pause"))
|
||||
if (GUILayout.Button(paused ? "继续" : "暂停"))
|
||||
{
|
||||
paused = !paused;
|
||||
}
|
||||
|
||||
if (GUILayout.Button("Reset"))
|
||||
if (GUILayout.Button("重置"))
|
||||
{
|
||||
ResetBenchmark();
|
||||
}
|
||||
@@ -322,7 +323,7 @@ namespace FishROV.AvoidanceBenchmark
|
||||
foreach (T value in System.Enum.GetValues(typeof(T)))
|
||||
{
|
||||
GUI.enabled = !EqualityComparer<T>.Default.Equals(value, current);
|
||||
if (GUILayout.Button(value.ToString(), GUILayout.Height(24)))
|
||||
if (GUILayout.Button(GetDisplayName(value), GUILayout.Height(24)))
|
||||
{
|
||||
onChanged(value);
|
||||
}
|
||||
@@ -330,5 +331,49 @@ namespace FishROV.AvoidanceBenchmark
|
||||
GUI.enabled = true;
|
||||
GUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
private static string GetDisplayName<T>(T value) where T : System.Enum
|
||||
{
|
||||
var boxed = (object)value;
|
||||
switch (boxed)
|
||||
{
|
||||
case AvoidanceFrameworkKind.NoAvoidance:
|
||||
return "无避障";
|
||||
case AvoidanceFrameworkKind.AstarRvo:
|
||||
return "A* RVO";
|
||||
case AvoidanceFrameworkKind.Rvo2:
|
||||
return "RVO2";
|
||||
case AvoidanceFrameworkKind.UnityNavMesh:
|
||||
return "NavMesh";
|
||||
case AvoidanceScenarioKind.FreeSwim:
|
||||
return "自由巡游";
|
||||
case AvoidanceScenarioKind.CrossFlow:
|
||||
return "对向穿流";
|
||||
case AvoidanceScenarioKind.SharkCharge:
|
||||
return "鲨鱼冲群";
|
||||
case AvoidanceScenarioKind.NarrowPassage:
|
||||
return "狭窄通道";
|
||||
case AvoidanceScenarioKind.DenseCircleStress:
|
||||
return "圆阵高压";
|
||||
case AvoidanceDimensionMode.XZ2D:
|
||||
return "2D/XZ";
|
||||
case AvoidanceDimensionMode.XZ25D:
|
||||
return "2.5D";
|
||||
case AvoidanceDimensionMode.XYZ3D:
|
||||
return "3D/XYZ";
|
||||
case BenchmarkViewMode.Overview:
|
||||
return "总览";
|
||||
case BenchmarkViewMode.TopDown:
|
||||
return "俯视";
|
||||
case BenchmarkViewMode.Side:
|
||||
return "侧视";
|
||||
case BenchmarkViewMode.Shark:
|
||||
return "跟鲨鱼";
|
||||
case BenchmarkViewMode.FollowSchool:
|
||||
return "跟鱼群";
|
||||
default:
|
||||
return value.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3f0e3f32e10e42eda4a2c6e58d0f9f8b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -9,6 +9,7 @@ namespace FishROV.AvoidanceBenchmark
|
||||
case AvoidanceFrameworkKind.NoAvoidance:
|
||||
return new NoAvoidanceAdapter();
|
||||
case AvoidanceFrameworkKind.AstarRvo:
|
||||
return new AstarRvoAdapter();
|
||||
case AvoidanceFrameworkKind.Rvo2:
|
||||
case AvoidanceFrameworkKind.UnityNavMesh:
|
||||
return new UnavailableAvoidanceAdapter(kind);
|
||||
|
||||
@@ -4,9 +4,9 @@ namespace FishROV.AvoidanceBenchmark
|
||||
{
|
||||
public sealed class NoAvoidanceAdapter : IAvoidanceAdapter
|
||||
{
|
||||
public string Name => "No Avoidance";
|
||||
public string Name => "无避障";
|
||||
public bool IsAvailable => true;
|
||||
public string Status => "Baseline movement only";
|
||||
public string Status => "基线移动:只按期望速度移动,不做避障。";
|
||||
|
||||
public void Initialize(AvoidanceBenchmarkConfig config)
|
||||
{
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace FishROV.AvoidanceBenchmark
|
||||
|
||||
public string Name => kind.ToString();
|
||||
public bool IsAvailable => false;
|
||||
public string Status => "Adapter placeholder. Implement this framework in its worktree.";
|
||||
public string Status => "当前方案在这个 worktree 中只是占位;请选择本分支要测试的避障方案。";
|
||||
|
||||
public void Initialize(AvoidanceBenchmarkConfig config)
|
||||
{
|
||||
|
||||
@@ -46,3 +46,12 @@ Adapter implementations may add package-specific components to created agents, b
|
||||
`NoAvoidanceAdapter` is the control group. It moves agents directly by preferred velocity and does not perform avoidance.
|
||||
|
||||
The A* RVO, RVO2, and Unity NavMesh adapters currently start as placeholders so their worktree diffs stay easy to review.
|
||||
|
||||
## A* Pathfinding Project Pro RVO Adapter
|
||||
|
||||
`AstarRvoAdapter` is isolated under `Assets/Scripts/Benchmark/FrameworkAdapters/` and is selected only by `AvoidanceFrameworkKind.AstarRvo`.
|
||||
|
||||
- It uses reflection to find `Pathfinding.RVO.RVOController` and `Pathfinding.RVO.RVOSimulator`, so the project still compiles when A* Pathfinding Project Pro is not imported.
|
||||
- When the A* Pro RVO types are missing, the Game view status reports the missing package and the agents fall back to baseline preferred-velocity movement.
|
||||
- When available, the adapter creates one runtime `RVOSimulator`, adds `RVOController` components to benchmark-created agents, feeds each preferred velocity into A* RVO, and writes the reflected avoidance velocity back through `BenchmarkAgent.ApplyVelocity`.
|
||||
- The shared benchmark scene, HUD, scenarios, count presets, metrics, and camera logic remain unchanged; the adapter is the only intended variable for this branch.
|
||||
|
||||
Reference in New Issue
Block a user