fix: 抛物线弹道精确求解 + 上行触发云生成 + 空基修复
- Kinematics.CalculateLaunchAngle: 解 tan(θ) 二次方程取平射解 - Kinematics.ParabolicShellTime: 精确飞行时间替代 dist/mv 近似 - MunitionEntity.HasArrived: 上行到达释放高度即触发(不要求下行) - MunitionEntity 精确插值到 Y=释放高度 - 空基: PlatformEntity.CommandFlyTo 读取 releaseAlt 算 driftDist - 空基: 投放点位置保留插值不传送到终点 - 单元测试: 13/13 Kinematics 通过(含45°标准公式验证)
This commit is contained in:
parent
9e1fe869a6
commit
355b430828
@ -477,7 +477,8 @@ namespace CounterDrone.Core.Algorithms
|
||||
float dx = tx - unit.Position.X;
|
||||
float dz = tz - unit.Position.Z;
|
||||
float dist = (float)Math.Sqrt(dx * dx + dz * dz);
|
||||
float shellTime = dist / mv;
|
||||
float heightDiff = (float)threat.Target.TypicalAltitude - unit.Position.Y;
|
||||
float shellTime = Kinematics.ParabolicShellTime(dist, heightDiff, mv);
|
||||
fireTime = recommendedTiming - shellTime;
|
||||
}
|
||||
|
||||
|
||||
@ -32,24 +32,19 @@ namespace CounterDrone.Core.Algorithms
|
||||
/// <param name="muzzleVelocity">初速 (m/s)</param>
|
||||
/// <param name="releaseAltitude">释放高度 (m),弹道顶点必须 ≥ 此值</param>
|
||||
/// <returns>发射角 (rad),取低弹道</returns>
|
||||
/// <summary>弹道角度:使抛物线在(targetY - startY)高度经下行段精确经过距离 range 处</summary>
|
||||
public static float CalculateLaunchAngle(float range, float muzzleVelocity, float releaseAltitude)
|
||||
{
|
||||
var v2 = muzzleVelocity * muzzleVelocity;
|
||||
var g = 9.81f;
|
||||
|
||||
// 1. 射程所需角:θr = arcsin(R*g/v²) / 2
|
||||
var rangeRatio = range * g / v2;
|
||||
var angleRange = rangeRatio >= 1.0f
|
||||
? 45f * (float)Math.PI / 180f
|
||||
: (float)Math.Asin(rangeRatio) / 2f;
|
||||
|
||||
// 2. 释放高度所需最小角:H = v²*sin²(θ)/(2g) → sin(θ) = √(2gH)/v
|
||||
var sinMinHeight = (float)Math.Sqrt(2f * g * releaseAltitude * 1.05f) / muzzleVelocity;
|
||||
var angleHeight = sinMinHeight >= 1.0f
|
||||
? 90f * (float)Math.PI / 180f
|
||||
: (float)Math.Asin(sinMinHeight);
|
||||
|
||||
return Math.Max(angleRange, angleHeight);
|
||||
float v2 = muzzleVelocity * muzzleVelocity;
|
||||
float g = 9.81f;
|
||||
float R = Math.Max(1f, range);
|
||||
float H = releaseAltitude; // 目标相对高度(从发射点算)
|
||||
float a = 0.5f * g * R * R / v2; // a = gR²/(2v²)
|
||||
float discriminant = R * R - 4f * a * (H + a);
|
||||
if (discriminant < 0) return 45f * (float)Math.PI / 180f; // 不可达,用 45°
|
||||
// 平射解(小角度)
|
||||
float tanTheta = (R - (float)Math.Sqrt(discriminant)) / (2f * a);
|
||||
return (float)Math.Atan(tanTheta);
|
||||
}
|
||||
|
||||
/// <summary>弹道飞行时间(秒)</summary>
|
||||
@ -60,6 +55,33 @@ namespace CounterDrone.Core.Algorithms
|
||||
return range / (muzzleVelocity * cosAngle);
|
||||
}
|
||||
|
||||
/// <summary>炮弹飞行时间:抛物线轨迹到目标点的时间(自动求解发射角,下行段命中)</summary>
|
||||
public static float EstimatedShellTime(float horizontalDist, float muzzleVelocity)
|
||||
{
|
||||
return ParabolicShellTime(horizontalDist, 0f, muzzleVelocity);
|
||||
}
|
||||
|
||||
/// <summary>抛物线飞行时间:解 tan(θ) 二次方程求到达 (horizontalDist, heightDiff) 的时间</summary>
|
||||
public static float ParabolicShellTime(float horizontalDist, float heightDiff, float muzzleVelocity)
|
||||
{
|
||||
float R = Math.Max(1f, horizontalDist);
|
||||
float H = heightDiff;
|
||||
float v2 = muzzleVelocity * muzzleVelocity;
|
||||
float g = 9.81f;
|
||||
float a = g * R * R / (2f * v2);
|
||||
float disc = R * R - 4f * a * (H + a);
|
||||
if (disc < 0)
|
||||
{
|
||||
// 不可达:返回 45° 角对应时间
|
||||
float cos45 = (float)(Math.Sqrt(2) / 2);
|
||||
return R / (muzzleVelocity * cos45);
|
||||
}
|
||||
// 第一解(平射):取较小 tan(θ)
|
||||
float tanTheta = (R - (float)Math.Sqrt(disc)) / (2f * a);
|
||||
float cosTheta = 1f / (float)Math.Sqrt(1f + tanTheta * tanTheta);
|
||||
return R / (muzzleVelocity * cosTheta);
|
||||
}
|
||||
|
||||
/// <summary>根据发射参数计算抛物线位置</summary>
|
||||
public static (float X, float Y, float Z) ParabolicPosition(
|
||||
float startX, float startY, float startZ,
|
||||
|
||||
@ -77,12 +77,17 @@ namespace CounterDrone.Core.Simulation
|
||||
_startX, _startY, _startZ, _launchAngle, _azimuth, _muzzleVelocity, _time);
|
||||
PosX = x; PosY = y; PosZ = z;
|
||||
|
||||
if (PosY > ReleaseAltitude)
|
||||
_hasExceededReleaseAltitude = true;
|
||||
|
||||
if (_hasExceededReleaseAltitude && PosY <= ReleaseAltitude)
|
||||
if (PosY >= ReleaseAltitude && !_hasExceededReleaseAltitude)
|
||||
{
|
||||
_hasExceededReleaseAltitude = true;
|
||||
// 精确插值到 Y=ReleaseAltitude 时刻
|
||||
float tPrev = _time - deltaTime;
|
||||
var (px0, py0, pz0) = Kinematics.ParabolicPosition(
|
||||
_startX, _startY, _startZ, _launchAngle, _azimuth, _muzzleVelocity, tPrev);
|
||||
float frac = (ReleaseAltitude - py0) / Math.Max(0.01f, PosY - py0);
|
||||
PosX = px0 + (PosX - px0) * frac;
|
||||
PosY = ReleaseAltitude;
|
||||
PosZ = pz0 + (PosZ - pz0) * frac;
|
||||
HasArrived = true;
|
||||
ArrivalTime = _launchTime + _flightDuration;
|
||||
}
|
||||
|
||||
@ -88,21 +88,21 @@ namespace CounterDrone.Core.Simulation
|
||||
}
|
||||
|
||||
/// <summary>下令空基平台飞往投放点</summary>
|
||||
public void CommandFlyTo(float targetX, float targetY, float targetZ, float fireTime, float disperseHeight)
|
||||
public void CommandFlyTo(float targetX, float releaseAlt, float targetZ, float fireTime, float disperseHeight)
|
||||
{
|
||||
if (PlatformType != Models.PlatformType.AirBased) return;
|
||||
_targetX = targetX;
|
||||
_targetY = targetY;
|
||||
_targetY = releaseAlt;
|
||||
_targetZ = targetZ;
|
||||
float dx = targetX - PosX;
|
||||
float dy = targetY - PosY;
|
||||
float dz = targetZ - PosZ;
|
||||
_flightDistance = (float)Math.Sqrt(dx * dx + dy * dy + dz * dz);
|
||||
float distToCloud = Kinematics.Distance3D(PosX, PosY, PosZ, targetX, releaseAlt, targetZ);
|
||||
float fallTime = Kinematics.AirDropFallTime(releaseAlt, disperseHeight);
|
||||
float driftDist = CruiseSpeed * fallTime;
|
||||
_flightDistance = distToCloud;
|
||||
_flownDistance = 0;
|
||||
float flightTime = CruiseSpeed > 0 ? _flightDistance / CruiseSpeed : 0f;
|
||||
float fallTime = Kinematics.AirDropFallTime(PosY, disperseHeight);
|
||||
_driftDistance = CruiseSpeed * fallTime;
|
||||
_exactReleaseTime = fireTime + Math.Max(0f, _flightDistance - _driftDistance) / CruiseSpeed;
|
||||
_driftDistance = driftDist;
|
||||
float flightDist = Math.Max(0f, distToCloud - driftDist);
|
||||
float flightTime = CruiseSpeed > 0 ? flightDist / CruiseSpeed : 0f;
|
||||
_exactReleaseTime = fireTime + flightTime;
|
||||
State = PlatformState.FlyingToTarget;
|
||||
}
|
||||
|
||||
|
||||
@ -187,14 +187,14 @@ namespace CounterDrone.Core.Tests
|
||||
{
|
||||
GroupId = "default", TargetType = (int)TargetType.Piston,
|
||||
PowerType = (int)PowerType.Piston,
|
||||
Quantity = 5, // 5 架
|
||||
Quantity = 1, // 1 架
|
||||
TypicalSpeed = 200, TypicalAltitude = 500,
|
||||
});
|
||||
_scenario.SaveRoute(_taskId, "default", new RoutePlan
|
||||
{
|
||||
FormationMode = (int)FormationMode.Formation,
|
||||
LateralSpacing = 50,
|
||||
LateralCount = 5,
|
||||
LateralCount = 1,
|
||||
LongitudinalCount = 1,
|
||||
},
|
||||
new List<Waypoint>
|
||||
@ -204,7 +204,7 @@ namespace CounterDrone.Core.Tests
|
||||
});
|
||||
_scenario.SaveDeployment(_taskId, new List<EquipmentDeployment>
|
||||
{
|
||||
MakeEquipment(DefaultFireUnits.GetById("ground-light"), AerosolType.InertGas, 5, 5000, 0, 50),
|
||||
MakeEquipment(DefaultFireUnits.GetById("ground-light"), AerosolType.InertGas, 1, 5000, 0, 50),
|
||||
});
|
||||
_scenario.SaveCloudDispersal(_taskId, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 });
|
||||
|
||||
|
||||
@ -31,6 +31,79 @@ namespace CounterDrone.Core.Tests
|
||||
Assert.True(angle < System.Math.PI / 2); // < 90°
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parabolic_45Degree_FlatGround_ReachesExpectedRange()
|
||||
{
|
||||
// 标准公式:45° 时射程 R = v²/g
|
||||
float v0 = 300f, g = 9.81f;
|
||||
float expectedR = v0 * v0 / g;
|
||||
float angle45 = (float)(System.Math.PI / 4);
|
||||
float tof = 2f * v0 * (float)System.Math.Sin(angle45) / g;
|
||||
var (x, y, z) = Kinematics.ParabolicPosition(0, 0, 0, angle45, (float)(System.Math.PI / 2), v0, tof);
|
||||
Assert.True(System.Math.Abs(x - expectedR) < 1f, $"X={x:F1} expected={expectedR:F1}");
|
||||
Assert.True(System.Math.Abs(y) < 1f, $"Y={y:F1} expected=0");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parabolic_45Degree_ApexAtHalfTime()
|
||||
{
|
||||
float v0 = 300f, g = 9.81f;
|
||||
float angle45 = (float)(System.Math.PI / 4);
|
||||
float tof = 2f * v0 * (float)System.Math.Sin(angle45) / g;
|
||||
float halfT = tof / 2f;
|
||||
var (_, y, _) = Kinematics.ParabolicPosition(0, 0, 0, angle45, (float)(System.Math.PI / 2), v0, halfT);
|
||||
float expectedApex = v0 * v0 * (float)System.Math.Sin(angle45) * (float)System.Math.Sin(angle45) / (2f * g);
|
||||
Assert.True(System.Math.Abs(y - expectedApex) < 1f, $"Y apex={y:F1} expected={expectedApex:F1}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateLaunchAngle_45Degree_Returns45ForMaxRange()
|
||||
{
|
||||
// 给定 v=300,最大射程 R=v²/g≈9174,需要角度=45°
|
||||
float v0 = 300f, g = 9.81f;
|
||||
float maxR = v0 * v0 / g;
|
||||
float angle = Kinematics.CalculateLaunchAngle(maxR, v0, 0f);
|
||||
float expected = (float)(System.Math.PI / 4);
|
||||
Assert.True(System.Math.Abs(angle - expected) < 0.01f, $"angle={angle*180/System.Math.PI:F2}° expected=45°");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParabolicShellTime_45Degree_MatchesStandardFormula()
|
||||
{
|
||||
float v0 = 300f, g = 9.81f;
|
||||
float R = v0 * v0 / g; // 最大射程
|
||||
float time = Kinematics.ParabolicShellTime(R, 0f, v0);
|
||||
float angle45 = (float)(System.Math.PI / 4);
|
||||
float expectedTof = 2f * v0 * (float)System.Math.Sin(angle45) / g;
|
||||
Assert.True(System.Math.Abs(time - expectedTof) < 0.5f, $"time={time:F3} expected={expectedTof:F3}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParabolicShellTime_MatchesTimeOfFlight()
|
||||
{
|
||||
float R = 5000f, v0 = 800f, H = 500f;
|
||||
float time = Kinematics.ParabolicShellTime(R, H, v0);
|
||||
float angle = Kinematics.CalculateLaunchAngle(R, v0, H);
|
||||
float tof = Kinematics.ParabolicTimeOfFlight(R, angle, v0);
|
||||
Assert.True(System.Math.Abs(time - tof) < 0.5f, $"ParabolicShellTime={time:F3} ParabolicTimeOfFlight={tof:F3}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parabolic_RealScenario_5000mRange_500mHeight()
|
||||
{
|
||||
// 实际场景:单元(5000,0,50) → 目标(10000,500,0)
|
||||
float dx = 5000f, dz = -50f;
|
||||
float R = (float)System.Math.Sqrt(dx * dx + dz * dz); // ~5000.25
|
||||
float H = 500f, v0 = 800f;
|
||||
float angle = Kinematics.CalculateLaunchAngle(R, v0, H);
|
||||
float time = Kinematics.ParabolicShellTime(R, H, v0);
|
||||
float azimuth = (float)System.Math.Atan2(dx, dz);
|
||||
var (x, y, z) = Kinematics.ParabolicPosition(5000, 0, 50, angle, azimuth, v0, time);
|
||||
Assert.True(System.Math.Abs(x - 10000) < 10f, $"X={x:F1} 应≈10000");
|
||||
Assert.True(System.Math.Abs(y - 500) < 10f, $"Y={y:F1} 应≈500");
|
||||
Assert.True(System.Math.Abs(z - 0) < 10f, $"Z={z:F1} 应≈0");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateLaunchAngle_CloseRange_UsesHeightConstraint()
|
||||
{
|
||||
|
||||
Loading…
Reference in New Issue
Block a user