diff --git a/FishROV/Assets/Scripts/Benchmark/FrameworkAdapters/AvoidanceAdapterRegistry.cs b/FishROV/Assets/Scripts/Benchmark/FrameworkAdapters/AvoidanceAdapterRegistry.cs index 14987ce..d6b9350 100644 --- a/FishROV/Assets/Scripts/Benchmark/FrameworkAdapters/AvoidanceAdapterRegistry.cs +++ b/FishROV/Assets/Scripts/Benchmark/FrameworkAdapters/AvoidanceAdapterRegistry.cs @@ -11,6 +11,7 @@ namespace FishROV.AvoidanceBenchmark case AvoidanceFrameworkKind.AstarRvo: return new AstarRvoAdapter(); case AvoidanceFrameworkKind.Rvo2: + return new Rvo2Adapter(); case AvoidanceFrameworkKind.UnityNavMesh: return new UnavailableAvoidanceAdapter(kind); default: diff --git a/FishROV/Assets/Scripts/Benchmark/FrameworkAdapters/Rvo2Adapter.cs b/FishROV/Assets/Scripts/Benchmark/FrameworkAdapters/Rvo2Adapter.cs new file mode 100644 index 0000000..e2b75c1 --- /dev/null +++ b/FishROV/Assets/Scripts/Benchmark/FrameworkAdapters/Rvo2Adapter.cs @@ -0,0 +1,513 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using UnityEngine; + +namespace FishROV.AvoidanceBenchmark +{ + public sealed class Rvo2Adapter : IAvoidanceAdapter + { + private readonly Dictionary agentIds = new Dictionary(); + private Rvo2ReflectionBridge bridge; + private AvoidanceBenchmarkConfig config; + private bool initialized; + + public string Name => "RVO2 / RVO2-3D"; + public bool IsAvailable => bridge != null && bridge.IsAvailable; + public string Status => bridge != null + ? bridge.Status + : Rvo2ReflectionBridge.MissingPackageStatus; + + public void Initialize(AvoidanceBenchmarkConfig config) + { + this.config = config.Clone(); + agentIds.Clear(); + bridge = Rvo2ReflectionBridge.Create(this.config); + initialized = true; + } + + public BenchmarkAgent CreateAgent(BenchmarkAgentSpawnInfo spawnInfo, Transform parent) + { + var agent = BenchmarkAgent.CreatePrimitive(spawnInfo, parent); + + if (!initialized || bridge == null || !bridge.IsAvailable) + { + return agent; + } + + var id = bridge.AddAgent(spawnInfo); + if (id >= 0) + { + agentIds[agent] = id; + } + + return agent; + } + + public void SetPreferredVelocity(BenchmarkAgent agent, Vector3 preferredVelocity) + { + if (bridge == null || !bridge.IsAvailable || !agentIds.TryGetValue(agent, out var id)) + { + agent.ApplyVelocity(preferredVelocity); + return; + } + + bridge.SetPreferredVelocity(id, preferredVelocity); + } + + public void SetTarget(BenchmarkAgent agent, Vector3 target) + { + agent.Target = target; + } + + public void Tick(float deltaTime) + { + if (bridge == null || !bridge.IsAvailable) + { + return; + } + + bridge.Step(deltaTime); + + foreach (var pair in agentIds) + { + pair.Key.ApplyVelocity(bridge.GetVelocity(pair.Value, pair.Key.MaxSpeed)); + } + } + + public void Dispose() + { + agentIds.Clear(); + bridge?.Dispose(); + bridge = null; + initialized = false; + } + + private sealed class Rvo2ReflectionBridge + { + public const string MissingPackageStatus = + "RVO2 package/source not found. Place the RVO2 or RVO2-3D C# source/package in Assets or Packages with a Simulator type such as RVO.Simulator; this adapter will bind by reflection."; + + private static readonly string[] SimulatorTypeNames = + { + "RVO.Simulator", + "RVO2.Simulator", + "RVO3D.Simulator", + "RVO.Simulator3D" + }; + + private readonly object simulator; + private readonly Type simulatorType; + private readonly Type vector2Type; + private readonly Type vector3Type; + private readonly bool use3D; + private readonly string status; + private readonly MethodInfo addAgent; + private readonly MethodInfo setAgentPrefVelocity; + private readonly MethodInfo doStep; + private readonly MethodInfo getAgentVelocity; + private readonly MethodInfo setTimeStep; + private readonly MethodInfo setAgentDefaults; + private readonly MethodInfo clear; + + private Rvo2ReflectionBridge( + object simulator, + Type simulatorType, + Type vector2Type, + Type vector3Type, + bool use3D, + string status, + MethodInfo addAgent, + MethodInfo setAgentPrefVelocity, + MethodInfo doStep, + MethodInfo getAgentVelocity, + MethodInfo setTimeStep, + MethodInfo setAgentDefaults, + MethodInfo clear) + { + this.simulator = simulator; + this.simulatorType = simulatorType; + this.vector2Type = vector2Type; + this.vector3Type = vector3Type; + this.use3D = use3D; + this.status = status; + this.addAgent = addAgent; + this.setAgentPrefVelocity = setAgentPrefVelocity; + this.doStep = doStep; + this.getAgentVelocity = getAgentVelocity; + this.setTimeStep = setTimeStep; + this.setAgentDefaults = setAgentDefaults; + this.clear = clear; + } + + public bool IsAvailable => simulator != null; + public string Status => status; + + public static Rvo2ReflectionBridge Create(AvoidanceBenchmarkConfig config) + { + var simulatorType = FindType(SimulatorTypeNames); + if (simulatorType == null) + { + return Unavailable(); + } + + var simulator = CreateOrGetSimulator(simulatorType); + if (simulator == null) + { + return Unavailable("RVO2 Simulator type was found, but no singleton instance or public constructor could be used."); + } + + var vector2Type = FindType(simulatorType, "RVO.Vector2", "RVO2.Vector2"); + var vector3Type = FindType(simulatorType, "RVO.Vector3", "RVO3D.Vector3", "RVO2.Vector3"); + var use3D = config.DimensionMode == AvoidanceDimensionMode.XYZ3D && vector3Type != null; + var agentVectorType = use3D ? vector3Type : vector2Type; + if (agentVectorType == null) + { + return Unavailable("RVO2 Simulator type was found, but no supported RVO.Vector2 or RVO.Vector3 type was found."); + } + + var addAgent = FindMethod(simulatorType, "addAgent", "AddAgent", new[] { agentVectorType }); + var setAgentPrefVelocity = FindMethod(simulatorType, "setAgentPrefVelocity", "SetAgentPrefVelocity", new[] { typeof(int), agentVectorType }); + var getAgentVelocity = FindMethod(simulatorType, "getAgentVelocity", "GetAgentVelocity", new[] { typeof(int) }); + var doStep = FindMethod(simulatorType, "doStep", "DoStep", Type.EmptyTypes); + var setTimeStep = FindMethod(simulatorType, "setTimeStep", "SetTimeStep", new[] { typeof(float) }); + var setAgentDefaults = FindMethodByName(simulatorType, "setAgentDefaults", "SetAgentDefaults"); + var clear = FindMethodByName(simulatorType, "clear", "Clear", "reset", "Reset"); + + if (addAgent == null || setAgentPrefVelocity == null || getAgentVelocity == null || doStep == null) + { + return Unavailable("RVO2 Simulator type was found, but required methods addAgent, setAgentPrefVelocity, getAgentVelocity, and doStep were not all available."); + } + + var bridge = new Rvo2ReflectionBridge( + simulator, + simulatorType, + vector2Type, + vector3Type, + use3D, + use3D ? "RVO2-3D reflection adapter active" : "RVO2 reflection adapter active", + addAgent, + setAgentPrefVelocity, + doStep, + getAgentVelocity, + setTimeStep, + setAgentDefaults, + clear); + + bridge.Configure(config); + return bridge; + } + + public int AddAgent(BenchmarkAgentSpawnInfo spawnInfo) + { + try + { + var result = addAgent.Invoke(simulator, new[] { ToRvoVector(spawnInfo.Position) }); + return Convert.ToInt32(result); + } + catch (Exception exception) + { + Debug.LogWarning($"RVO2 addAgent failed: {exception.Message}"); + return -1; + } + } + + public void SetPreferredVelocity(int id, Vector3 preferredVelocity) + { + try + { + setAgentPrefVelocity.Invoke(simulator, new[] { id, ToRvoVector(preferredVelocity) }); + } + catch (Exception exception) + { + Debug.LogWarning($"RVO2 setAgentPrefVelocity failed: {exception.Message}"); + } + } + + public void Step(float deltaTime) + { + try + { + InvokeSetTimeStep(deltaTime); + doStep.Invoke(simulator, null); + } + catch (Exception exception) + { + Debug.LogWarning($"RVO2 doStep failed: {exception.Message}"); + } + } + + public Vector3 GetVelocity(int id, float maxSpeed) + { + try + { + var value = getAgentVelocity.Invoke(simulator, new object[] { id }); + return Vector3.ClampMagnitude(FromRvoVector(value), maxSpeed); + } + catch (Exception exception) + { + Debug.LogWarning($"RVO2 getAgentVelocity failed: {exception.Message}"); + return Vector3.zero; + } + } + + public void Dispose() + { + try + { + clear?.Invoke(simulator, null); + } + catch (Exception exception) + { + Debug.LogWarning($"RVO2 clear/reset failed: {exception.Message}"); + } + } + + private void Configure(AvoidanceBenchmarkConfig config) + { + InvokeSetTimeStep(Time.fixedDeltaTime); + InvokeAgentDefaults(config); + } + + private void InvokeSetTimeStep(float deltaTime) + { + if (setTimeStep == null) + { + return; + } + + setTimeStep.Invoke(simulator, new object[] { Mathf.Max(0.001f, deltaTime) }); + } + + private void InvokeAgentDefaults(AvoidanceBenchmarkConfig config) + { + if (setAgentDefaults == null) + { + return; + } + + var parameters = setAgentDefaults.GetParameters(); + var values = new object[parameters.Length]; + for (var i = 0; i < parameters.Length; i++) + { + var parameterType = parameters[i].ParameterType; + var name = parameters[i].Name ?? string.Empty; + + if (parameterType == typeof(int)) + { + values[i] = config.MaxNeighbours; + } + else if (parameterType == typeof(float)) + { + values[i] = GuessDefaultFloat(name, config); + } + else if (parameterType == vector2Type) + { + values[i] = CreateVector(vector2Type, 0f, 0f, 0f); + } + else if (parameterType == vector3Type) + { + values[i] = CreateVector(vector3Type, 0f, 0f, 0f); + } + else + { + values[i] = parameterType.IsValueType ? Activator.CreateInstance(parameterType) : null; + } + } + + setAgentDefaults.Invoke(simulator, values); + } + + private static float GuessDefaultFloat(string parameterName, AvoidanceBenchmarkConfig config) + { + var lower = parameterName.ToLowerInvariant(); + if (lower.Contains("radius")) + { + return config.FishRadius; + } + + if (lower.Contains("maxspeed") || lower.Contains("max_speed")) + { + return config.FishSpeed; + } + + if (lower.Contains("neigh") || lower.Contains("dist")) + { + return config.FishRadius * 8f; + } + + if (lower.Contains("horizon")) + { + return 2.5f; + } + + return 0f; + } + + private object ToRvoVector(Vector3 value) + { + if (use3D && vector3Type != null) + { + return CreateVector(vector3Type, value.x, value.y, value.z); + } + + return CreateVector(vector2Type, value.x, value.z, 0f); + } + + private Vector3 FromRvoVector(object value) + { + if (value == null) + { + return Vector3.zero; + } + + var type = value.GetType(); + if (use3D && type == vector3Type) + { + return new Vector3(GetFloatMember(value, "x", "X"), GetFloatMember(value, "y", "Y"), GetFloatMember(value, "z", "Z")); + } + + return new Vector3(GetFloatMember(value, "x", "X"), 0f, GetFloatMember(value, "y", "Y")); + } + + private static object CreateVector(Type type, float x, float y, float z) + { + var constructor = type.GetConstructor(new[] { typeof(float), typeof(float), typeof(float) }); + if (constructor != null) + { + return constructor.Invoke(new object[] { x, y, z }); + } + + constructor = type.GetConstructor(new[] { typeof(float), typeof(float) }); + if (constructor != null) + { + return constructor.Invoke(new object[] { x, y }); + } + + var value = Activator.CreateInstance(type); + SetFloatMember(value, x, "x", "X"); + SetFloatMember(value, y, "y", "Y"); + SetFloatMember(value, z, "z", "Z"); + return value; + } + + private static float GetFloatMember(object value, params string[] names) + { + var type = value.GetType(); + foreach (var name in names) + { + var field = type.GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + if (field != null) + { + return Convert.ToSingle(field.GetValue(value)); + } + + var property = type.GetProperty(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + if (property != null) + { + return Convert.ToSingle(property.GetValue(value, null)); + } + } + + return 0f; + } + + private static void SetFloatMember(object value, float memberValue, params string[] names) + { + var type = value.GetType(); + foreach (var name in names) + { + var field = type.GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + if (field != null) + { + field.SetValue(value, memberValue); + return; + } + + var property = type.GetProperty(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + if (property != null && property.CanWrite) + { + property.SetValue(value, memberValue, null); + return; + } + } + } + + private static Rvo2ReflectionBridge Unavailable(string status = MissingPackageStatus) + { + return new Rvo2ReflectionBridge(null, null, null, null, false, status, null, null, null, null, null, null, null); + } + + private static object CreateOrGetSimulator(Type type) + { + var instanceProperty = type.GetProperty("Instance", BindingFlags.Public | BindingFlags.Static); + if (instanceProperty != null) + { + return instanceProperty.GetValue(null, null); + } + + var instanceField = type.GetField("Instance", BindingFlags.Public | BindingFlags.Static); + if (instanceField != null) + { + return instanceField.GetValue(null); + } + + var constructor = type.GetConstructor(Type.EmptyTypes); + return constructor != null ? constructor.Invoke(null) : null; + } + + private static Type FindType(params string[] typeNames) + { + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + foreach (var typeName in typeNames) + { + var type = assembly.GetType(typeName, false); + if (type != null) + { + return type; + } + } + } + + return null; + } + + private static Type FindType(Type anchorType, params string[] typeNames) + { + foreach (var typeName in typeNames) + { + var type = anchorType.Assembly.GetType(typeName, false); + if (type != null) + { + return type; + } + } + + return FindType(typeNames); + } + + private static MethodInfo FindMethod(Type type, string camelName, string pascalName, Type[] parameterTypes) + { + return type.GetMethod(camelName, BindingFlags.Instance | BindingFlags.Public, null, parameterTypes, null) + ?? type.GetMethod(pascalName, BindingFlags.Instance | BindingFlags.Public, null, parameterTypes, null); + } + + private static MethodInfo FindMethodByName(Type type, params string[] names) + { + foreach (var name in names) + { + foreach (var method in type.GetMethods(BindingFlags.Instance | BindingFlags.Public)) + { + if (method.Name == name) + { + return method; + } + } + } + + return null; + } + } + } +} diff --git a/FishROV/Assets/Scripts/Benchmark/FrameworkAdapters/Rvo2Adapter.cs.meta b/FishROV/Assets/Scripts/Benchmark/FrameworkAdapters/Rvo2Adapter.cs.meta new file mode 100644 index 0000000..d1aa85d --- /dev/null +++ b/FishROV/Assets/Scripts/Benchmark/FrameworkAdapters/Rvo2Adapter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8ab9415f1c774b36a29d07f9f462c911 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/FishROV/Docs/local-avoidance-benchmark-notes.md b/FishROV/Docs/local-avoidance-benchmark-notes.md index 25dea6f..269a00b 100644 --- a/FishROV/Docs/local-avoidance-benchmark-notes.md +++ b/FishROV/Docs/local-avoidance-benchmark-notes.md @@ -45,7 +45,7 @@ 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. +The A* RVO, RVO2, and Unity NavMesh adapters are selected through the same debug panel so their behavior can be compared under the same scene, count, camera, and metrics. ## A* Pathfinding Project Pro RVO Adapter @@ -55,3 +55,13 @@ The A* RVO, RVO2, and Unity NavMesh adapters currently start as placeholders so - 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. + +## RVO2 / RVO2-3D Adapter + +`Rvo2Adapter` is intentionally a reflection adapter. The benchmark must compile even when no RVO2 package or source is present. + +- Without RVO2 source/package, selecting `Rvo2` creates the same benchmark primitives and reports the missing package in the Game view `Status` line. +- To enable the real framework, place a C# RVO2 or RVO2-3D implementation under `Assets/` or add it as a package under `Packages/`. +- The adapter looks for a simulator type such as `RVO.Simulator`, `RVO2.Simulator`, `RVO3D.Simulator`, or `RVO.Simulator3D`. +- The required simulator methods are `addAgent`/`AddAgent`, `setAgentPrefVelocity`/`SetAgentPrefVelocity`, `getAgentVelocity`/`GetAgentVelocity`, and `doStep`/`DoStep`. +- 2D and 2.5D modes bind to an RVO `Vector2` style type and map Unity `x/z` to RVO `x/y`. 3D mode binds to an RVO `Vector3` style type when available.