Merge Astar RVO benchmark adapter

This commit is contained in:
JSD\13999
2026-06-13 14:42:01 +08:00
7 changed files with 371 additions and 19 deletions

View File

@@ -26,8 +26,8 @@ namespace FishROV.AvoidanceBenchmark
private void Start() private void Start()
{ {
Application.targetFrameRate = 60;
QualitySettings.vSyncCount = 0; QualitySettings.vSyncCount = 0;
Application.targetFrameRate = -1;
pendingFishCount = fishCount; pendingFishCount = fishCount;
metrics.Start(); metrics.Start();
EnsureCameraAndLight(); EnsureCameraAndLight();
@@ -63,33 +63,34 @@ namespace FishROV.AvoidanceBenchmark
private void OnGUI() private void OnGUI()
{ {
GUILayout.BeginArea(new Rect(12, 12, 390, Screen.height - 24), GUI.skin.box); GUILayout.BeginArea(new Rect(12, 12, 390, Screen.height - 24), GUI.skin.box);
GUILayout.Label("FishROV Local Avoidance Benchmark"); GUILayout.Label("FishROV 本地避障压测");
GUILayout.Label($"Adapter: {adapter?.Name ?? "None"}"); GUILayout.Label($"当前方案:{adapter?.Name ?? ""}");
GUILayout.Label($"Status: {adapter?.Status ?? "Not initialized"}"); GUILayout.Label($"状态:{adapter?.Status ?? ""}");
GUILayout.Label($"Agents: fish {fishCount}, shark {sharkCount}, total {agents.Count}"); GUILayout.Label($"对象数量:小鱼 {fishCount},鲨鱼 {sharkCount},总数 {agents.Count}");
GUILayout.Label($"FPS avg {metrics.AverageFps:F1} | 1% low {metrics.OnePercentLowFps:F1}"); GUILayout.Label($"帧率:平均 {metrics.AverageFps:F1} | 1% Low {metrics.OnePercentLowFps:F1}");
GUILayout.Label($"Simulation {metrics.SimulationMs:F2} ms | GC {metrics.GcAllocBytes / 1024f:F1} KB"); GUILayout.Label($"模拟耗时:{metrics.SimulationMs:F2} ms | GC{metrics.GcAllocBytes / 1024f:F1} KB");
GUILayout.Label($"帧率限制不锁帧vSync={QualitySettings.vSyncCount}targetFrameRate={Application.targetFrameRate}");
GUILayout.Space(8); GUILayout.Space(8);
DrawEnumButtons("Framework", framework, value => DrawEnumButtons("避障方案", framework, value =>
{ {
framework = value; framework = value;
ResetBenchmark(); ResetBenchmark();
}); });
DrawEnumButtons("Scenario", scenario, value => DrawEnumButtons("测试场景", scenario, value =>
{ {
scenario = value; scenario = value;
ResetBenchmark(); ResetBenchmark();
}); });
DrawEnumButtons("Dimension", dimensionMode, value => DrawEnumButtons("计算维度", dimensionMode, value =>
{ {
dimensionMode = value; dimensionMode = value;
ResetBenchmark(); ResetBenchmark();
}); });
DrawEnumButtons("View", viewMode, value => viewMode = value); DrawEnumButtons("观察视角", viewMode, value => viewMode = value);
GUILayout.Space(8); GUILayout.Space(8);
GUILayout.Label($"Fish Count: {pendingFishCount}"); GUILayout.Label($"小鱼数量:{pendingFishCount}");
GUILayout.BeginHorizontal(); GUILayout.BeginHorizontal();
foreach (var preset in CountPresets) foreach (var preset in CountPresets)
{ {
@@ -105,18 +106,18 @@ namespace FishROV.AvoidanceBenchmark
pendingFishCount = Mathf.RoundToInt(GUILayout.HorizontalSlider(pendingFishCount, 10, 5000)); pendingFishCount = Mathf.RoundToInt(GUILayout.HorizontalSlider(pendingFishCount, 10, 5000));
GUILayout.BeginHorizontal(); GUILayout.BeginHorizontal();
if (GUILayout.Button("Apply Count")) if (GUILayout.Button("应用数量"))
{ {
fishCount = pendingFishCount; fishCount = pendingFishCount;
ResetBenchmark(); ResetBenchmark();
} }
if (GUILayout.Button(paused ? "Resume" : "Pause")) if (GUILayout.Button(paused ? "继续" : "暂停"))
{ {
paused = !paused; paused = !paused;
} }
if (GUILayout.Button("Reset")) if (GUILayout.Button("重置"))
{ {
ResetBenchmark(); ResetBenchmark();
} }
@@ -322,7 +323,7 @@ namespace FishROV.AvoidanceBenchmark
foreach (T value in System.Enum.GetValues(typeof(T))) foreach (T value in System.Enum.GetValues(typeof(T)))
{ {
GUI.enabled = !EqualityComparer<T>.Default.Equals(value, current); 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); onChanged(value);
} }
@@ -330,5 +331,49 @@ namespace FishROV.AvoidanceBenchmark
GUI.enabled = true; GUI.enabled = true;
GUILayout.EndHorizontal(); 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();
}
}
} }
} }

View File

@@ -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; }
}
}
}

View File

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

View File

@@ -9,6 +9,7 @@ namespace FishROV.AvoidanceBenchmark
case AvoidanceFrameworkKind.NoAvoidance: case AvoidanceFrameworkKind.NoAvoidance:
return new NoAvoidanceAdapter(); return new NoAvoidanceAdapter();
case AvoidanceFrameworkKind.AstarRvo: case AvoidanceFrameworkKind.AstarRvo:
return new AstarRvoAdapter();
case AvoidanceFrameworkKind.Rvo2: case AvoidanceFrameworkKind.Rvo2:
case AvoidanceFrameworkKind.UnityNavMesh: case AvoidanceFrameworkKind.UnityNavMesh:
return new UnavailableAvoidanceAdapter(kind); return new UnavailableAvoidanceAdapter(kind);

View File

@@ -4,9 +4,9 @@ namespace FishROV.AvoidanceBenchmark
{ {
public sealed class NoAvoidanceAdapter : IAvoidanceAdapter public sealed class NoAvoidanceAdapter : IAvoidanceAdapter
{ {
public string Name => "No Avoidance"; public string Name => "无避障";
public bool IsAvailable => true; public bool IsAvailable => true;
public string Status => "Baseline movement only"; public string Status => "基线移动:只按期望速度移动,不做避障。";
public void Initialize(AvoidanceBenchmarkConfig config) public void Initialize(AvoidanceBenchmarkConfig config)
{ {

View File

@@ -13,7 +13,7 @@ namespace FishROV.AvoidanceBenchmark
public string Name => kind.ToString(); public string Name => kind.ToString();
public bool IsAvailable => false; public bool IsAvailable => false;
public string Status => "Adapter placeholder. Implement this framework in its worktree."; public string Status => "当前方案在这个 worktree 中只是占位;请选择本分支要测试的避障方案。";
public void Initialize(AvoidanceBenchmarkConfig config) public void Initialize(AvoidanceBenchmarkConfig config)
{ {

View File

@@ -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. `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. 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.