diff --git a/src/CounterDrone.Core/Algorithms/Kinematics.cs b/src/CounterDrone.Core/Algorithms/Kinematics.cs
index 8198ee2..7aa8733 100644
--- a/src/CounterDrone.Core/Algorithms/Kinematics.cs
+++ b/src/CounterDrone.Core/Algorithms/Kinematics.cs
@@ -38,19 +38,11 @@ namespace CounterDrone.Core.Algorithms
throw new ArgumentException($"range 必须 > 0,实际: {range}", nameof(range));
if (muzzleVelocity <= 0)
throw new ArgumentException($"muzzleVelocity 必须 > 0,实际: {muzzleVelocity}", nameof(muzzleVelocity));
-
- float v2 = muzzleVelocity * muzzleVelocity;
- float g = 9.81f;
- // 轨迹方程: heightDiff = R·tanθ - gR²/(2v²cos²θ)
- // 令 T = tanθ, a = gR²/(2v²): aT² - RT + (a + heightDiff) = 0
- float a = 0.5f * g * range * range / v2;
- float discriminant = range * range - 4f * a * (heightDiff + a);
- if (discriminant < 0)
+ var angle = ParabolicMotion.SolveAngle(range, heightDiff, muzzleVelocity, TrajectoryPreference.Low);
+ if (!angle.HasValue)
throw new ArgumentException(
$"当前参数无法命中目标: range={range}, heightDiff={heightDiff}, v₀={muzzleVelocity}");
- // 低弹道解(取较小 tanθ)
- float tanTheta = (range - (float)Math.Sqrt(discriminant)) / (2f * a);
- return (float)Math.Atan(tanTheta);
+ return angle.Value;
}
/// 弹道飞行时间(秒)。水平距离 / 水平分速
@@ -85,25 +77,12 @@ namespace CounterDrone.Core.Algorithms
throw new ArgumentException($"muzzleVelocity 必须 > 0,实际: {muzzleVelocity}", nameof(muzzleVelocity));
if (launchAngle <= -Math.PI / 2 || launchAngle >= Math.PI / 2)
throw new ArgumentException($"launchAngle 必须在 (-π/2, π/2) 内,实际: {launchAngle}", nameof(launchAngle));
-
- float g = 9.81f;
- float cosA = (float)Math.Cos(launchAngle);
- float sinA = (float)Math.Sin(launchAngle);
-
- // 垂直运动: ½g·t² - v·sinθ·t + heightDiff = 0
- float vy = muzzleVelocity * sinA;
- float disc = vy * vy - 2f * g * heightDiff;
- if (disc < 0)
+ var motion = new ParabolicMotion(muzzleVelocity, launchAngle);
+ var result = motion.ComputeRange(heightDiff, TrajectoryPreference.Nearest);
+ if (!result.HasValue)
throw new ArgumentException(
$"当前参数无法达到目标高度: heightDiff={heightDiff}, v₀={muzzleVelocity}, θ={launchAngle * 180 / Math.PI:F1}°");
-
- // 取较小正时间(低弹道或唯一解)
- float t1 = (vy - (float)Math.Sqrt(disc)) / g;
- float t2 = (vy + (float)Math.Sqrt(disc)) / g;
- float timeOfFlight = t1 > 0 ? t1 : t2;
- float range = muzzleVelocity * cosA * timeOfFlight;
-
- return (range, timeOfFlight);
+ return result.Value;
}
/// 计算弹道顶点(最大高度和到达时间),相对发射点
diff --git a/src/CounterDrone.Core/Algorithms/ParabolicMotion.cs b/src/CounterDrone.Core/Algorithms/ParabolicMotion.cs
new file mode 100644
index 0000000..d0556a6
--- /dev/null
+++ b/src/CounterDrone.Core/Algorithms/ParabolicMotion.cs
@@ -0,0 +1,109 @@
+using System;
+
+namespace CounterDrone.Core.Algorithms
+{
+ /// 轨迹偏好(多解选择)
+ public enum TrajectoryPreference
+ {
+ High, // 高抛(较大仰角 / 较晚到达目标高度)
+ Low, // 低抛(较小仰角 / 较早到达目标高度)
+ Nearest, // 最短飞行时间(最小正时间)
+ Farthest // 最长飞行时间(最大正时间)
+ }
+
+ /// 抛物线运动学核心模块
+ public class ParabolicMotion
+ {
+ public readonly float V0, Theta, Y0, X0;
+ public readonly float Vx, Vy;
+ public const float G = 9.81f;
+
+ public ParabolicMotion(float v0, float theta, float y0 = 0, float x0 = 0)
+ {
+ V0 = v0; Theta = theta; Y0 = y0; X0 = x0;
+ Vx = v0 * (float)Math.Cos(theta);
+ Vy = v0 * (float)Math.Sin(theta);
+ }
+
+ /// 任意时刻 t 的状态
+ public (float x, float y, float vx, float vy) GetState(float t)
+ {
+ return (X0 + Vx * t, Y0 + Vy * t - 0.5f * G * t * t, Vx, Vy - G * t);
+ }
+
+ /// 到达目标高度 yTarget 的所有正时间解(0/1/2 个)
+ public float[] GetFlightTimes(float yTarget)
+ {
+ float a = 0.5f * G;
+ float b = -Vy;
+ float c = yTarget - Y0;
+ float d = b * b - 4f * a * c;
+ if (d < 0) return Array.Empty();
+
+ float sqrtD = (float)Math.Sqrt(d);
+ float t1 = (-b - sqrtD) / (2f * a);
+ float t2 = (-b + sqrtD) / (2f * a);
+
+ if (t1 > 0 && t2 > 0)
+ return t1 < t2 ? new[] { t1, t2 } : new[] { t2, t1 };
+ if (t1 > 0) return new[] { t1 };
+ if (t2 > 0) return new[] { t2 };
+ return Array.Empty();
+ }
+
+ /// 根据偏好选择到达目标高度的飞行时间
+ public float? GetFlightTime(float yTarget, TrajectoryPreference pref)
+ {
+ var ts = GetFlightTimes(yTarget);
+ if (ts.Length == 0) return null;
+ if (ts.Length == 1) return ts[0];
+ return pref switch
+ {
+ TrajectoryPreference.Nearest or TrajectoryPreference.Low => ts[0],
+ TrajectoryPreference.Farthest or TrajectoryPreference.High => ts[1],
+ _ => ts[0]
+ };
+ }
+
+ /// 根据偏好获取水平和飞行时间
+ public (float range, float timeOfFlight)? ComputeRange(float yTarget, TrajectoryPreference pref)
+ {
+ float? t = GetFlightTime(yTarget, pref);
+ if (!t.HasValue) return null;
+ return (Vx * t.Value, t.Value);
+ }
+
+ // ═══ 逆问题:求解瞄准角度 ═══
+
+ /// 已知水平距离、高度差、初速,求解两个可能的发射角 (rad)
+ public static (float theta1, float theta2)? SolveAngles(float range, float heightDiff, float v0)
+ {
+ if (range <= 0 || v0 <= 0) return null;
+ float v2 = v0 * v0;
+ float A = (G * range * range) / (2f * v2);
+ float B = -range;
+ float C = heightDiff + A;
+ float d = B * B - 4f * A * C;
+ if (d < 0 || A == 0) return null;
+ float sqrtD = (float)Math.Sqrt(d);
+ float p1 = (-B + sqrtD) / (2f * A);
+ float p2 = (-B - sqrtD) / (2f * A);
+ return ((float)Math.Atan(p1), (float)Math.Atan(p2));
+ }
+
+ /// 根据偏好选择一个瞄准角度
+ public static float? SolveAngle(float range, float heightDiff, float v0, TrajectoryPreference pref)
+ {
+ var result = SolveAngles(range, heightDiff, v0);
+ if (!result.HasValue) return null;
+ var (a1, a2) = result.Value;
+ float high = Math.Max(a1, a2);
+ float low = Math.Min(a1, a2);
+ return pref switch
+ {
+ TrajectoryPreference.High => high,
+ TrajectoryPreference.Low or TrajectoryPreference.Nearest or _ => low
+ };
+ }
+ }
+}
diff --git a/test/unit/CounterDrone.Core.Tests/KinematicsTests.cs b/test/unit/CounterDrone.Core.Tests/KinematicsTests.cs
index 2cfa464..8a5725b 100644
--- a/test/unit/CounterDrone.Core.Tests/KinematicsTests.cs
+++ b/test/unit/CounterDrone.Core.Tests/KinematicsTests.cs
@@ -176,8 +176,8 @@ namespace CounterDrone.Core.Tests
[Fact]
public void ComputeParabolicRange_ThrowsOnUnreachableHeight()
{
- // 300m/s 45° 不可能达到 5000m 高度的目标
float angle45 = (float)(System.Math.PI / 4);
+ // 300m/s 45° 不可能达到 5000m 高
Assert.Throws(() => Kinematics.ComputeParabolicRange(300f, angle45, 5000f));
}
@@ -256,16 +256,11 @@ namespace CounterDrone.Core.Tests
[Fact]
public void ComputeParabolicRange_ElevatedTarget_VerifiableByPosition()
{
- // v₀=800, θ=10°, 目标在发射点上方 500m
+ // Farthest=下行段, 弹在目标距离命中
float v0 = 800f, heightDiff = 500f;
float angle = 10f * (float)System.Math.PI / 180f;
-
var (range, time) = Kinematics.ComputeParabolicRange(v0, angle, heightDiff);
-
- // 用 ParabolicPosition 验证:在 time 时刻应到达 (range, heightDiff)
var (x, y, z) = Kinematics.ParabolicPosition(0, 0, 0, angle, 0, v0, time);
- // X 方向(azimuth=0 → cos(0) → +Z 方向)
- // 实际水平距离 = sqrt(x²+z²) = z(因为 azimuth=0)
float actualDist = (float)System.Math.Sqrt(x * x + z * z);
Assert.True(System.Math.Abs(actualDist - range) < 5f,
$"水平距离={actualDist:F1} expected={range:F1}");
@@ -276,19 +271,14 @@ namespace CounterDrone.Core.Tests
[Fact]
public void ComputeParabolicRange_DepressedTarget_VerifiableByPosition()
{
- // v₀=500, θ=5°, 目标在发射点下方 200m
+ // 俯射只有唯一解
float v0 = 500f, heightDiff = -200f;
float angle = 5f * (float)System.Math.PI / 180f;
-
var (range, time) = Kinematics.ComputeParabolicRange(v0, angle, heightDiff);
-
- // 验证到达时间时位置
var (x, y, z) = Kinematics.ParabolicPosition(0, 0, 0, angle, 0, v0, time);
float actualDist = (float)System.Math.Sqrt(x * x + z * z);
- Assert.True(System.Math.Abs(actualDist - range) < 5f,
- $"水平距离={actualDist:F1} expected={range:F1}");
- Assert.True(System.Math.Abs(y - heightDiff) < 5f,
- $"高度={y:F1} expected={heightDiff:F1}");
+ Assert.True(System.Math.Abs(actualDist - range) < 5f);
+ Assert.True(System.Math.Abs(y - heightDiff) < 5f);
}
[Fact]