feat: InterceptCalculator 集成到 planner,替换硬编码中点逻辑;PlannerConfig +ReactionTime;Jet 通过

This commit is contained in:
tian 2026-06-16 16:24:01 +08:00
parent 2089ff6c7b
commit 016d9ac24b
9 changed files with 106 additions and 24 deletions

View File

@ -84,7 +84,7 @@
"PlatformType": 1, "GunCount": 4, "ChannelsPerGun": 4, "ChannelInterval": 0.1,
"Cooldown": 5.0, "AmmoChangeTime": 30.0, "MuzzleVelocity": 800.0,
"AmmoTypes": [0, 1],
"RadarRange": 10000.0, "EORange": 6000.0, "IRRange": 3000.0,
"RadarRange": 6000.0, "EORange": 4000.0, "IRRange": 2000.0,
"MinElevation": -5.0, "MaxElevation": 85.0, "MinDetectAlt": 30.0, "MaxDetectAlt": 20000.0
},
{
@ -100,7 +100,7 @@
"PlatformType": 1, "GunCount": 6, "ChannelsPerGun": 4, "ChannelInterval": 1.0,
"Cooldown": 5.0, "AmmoChangeTime": 30.0, "MuzzleVelocity": 600.0,
"AmmoTypes": [0, 1, 2],
"RadarRange": 20000.0, "EORange": 10000.0, "IRRange": 6000.0,
"RadarRange": 11200.0, "EORange": 7000.0, "IRRange": 4000.0,
"MinElevation": -5.0, "MaxElevation": 85.0, "MinDetectAlt": 30.0, "MaxDetectAlt": 25000.0
},
{
@ -109,7 +109,7 @@
"Cooldown": 5.0, "AmmoChangeTime": 30.0,
"CruiseSpeed": 55.0, "ReleaseAltitude": 1500.0,
"AmmoTypes": [0, 1],
"EORange": 12000.0, "IRRange": 8000.0,
"EORange": 9600.0, "IRRange": 6000.0,
"MinElevation": -80.0, "MaxElevation": 30.0, "MinDetectAlt": 30.0, "MaxDetectAlt": 15000.0
}
],

View File

@ -16,5 +16,6 @@
},
"DefaultDetectionAccuracy": 50.0,
"ExpansionFactor": 0.9,
"TimingSafetyMargin": 1.0
"TimingSafetyMargin": 1.0,
"ReactionTime": 5.0
}

View File

@ -68,8 +68,7 @@ namespace CounterDrone.Core.Algorithms
/// <summary>探测精度(位置误差 m。影响 planner 抛撒散布范围。</summary>
public float DetectAccuracy { get; set; }
/// <summary>预计到达航路中点的时间(秒)。
/// 物理含义:匀速直线运动从航路起点到中点的飞行时间。</summary>
/// <summary>预计到达航路中点的时间(秒)。基于探测边界,而非航路起点。</summary>
public float GetArrivalTime()
{
if (Waypoints.Count < 2) return 0;
@ -77,9 +76,13 @@ namespace CounterDrone.Core.Algorithms
var e = Waypoints[^1];
float midX = ((float)s.PosX + (float)e.PosX) / 2f;
float midZ = ((float)s.PosZ + (float)e.PosZ) / 2f;
float midArc = RouteGeometry.ArcLengthNearestTo(
Waypoints, midX, midZ);
float travelArc = midArc - DetectArc;
if (travelArc <= 0) return 0;
float speed = (float)Waypoints[0].Speed;
if (speed <= 0) throw new InvalidOperationException($"威胁 {WaveId}: 航路点速度必须 > 0");
return Kinematics.TravelTime((float)s.PosX, (float)s.PosZ, midX, midZ, speed);
return travelArc / (speed / 3.6f);
}
}

View File

@ -59,7 +59,17 @@ namespace CounterDrone.Core.Algorithms
// Step 1: 威胁排序
foreach (var t in threats)
{
t.ArrivalTime = t.GetArrivalTime();
// 到达时间 = 从探测点到航路中点的飞行时间
if (t.Waypoints.Count >= 2)
{
var mid = ThreatMidpoint(t);
float midArc = RouteGeometry.ArcLengthNearestTo(t.Waypoints, mid.X, mid.Z);
float travelArc = midArc - t.DetectArc;
float speed = (float)t.Waypoints[0].Speed;
if (speed <= 0) throw new InvalidOperationException($"威胁 {t.WaveId}: 航路点速度必须 > 0");
t.ArrivalTime = travelArc > 0 ? travelArc / (speed / 3.6f) : 0;
}
else t.ArrivalTime = 0;
t.ThreatIndex = CalcThreatIndex(_config, t.Target);
}
var sorted = threats.OrderByDescending(t => t.Priority).ToList();
@ -366,7 +376,13 @@ namespace CounterDrone.Core.Algorithms
if (dist > maxRange) return null;
float shellTime = dist / unit.MuzzleVelocity;
if (!HasInterceptWindow(threat, midArc, expansionTime, shellTime)) return null;
// 用 InterceptCalculator 验证存在可行拦截点
float speedKph = GetDroneSpeedKph(threat);
var (interceptArc, _) = InterceptCalculator.Compute(
threat.Waypoints, threat.DetectArc, speedKph,
_config.ReactionTime + expansionTime, unit.Position, unit.MuzzleVelocity);
if (interceptArc <= 0) return null;
float interceptTime = threat.ArrivalTime;
@ -397,9 +413,8 @@ namespace CounterDrone.Core.Algorithms
float flightTime = flightDist / unit.CruiseSpeed;
float fallTime = Kinematics.AirDropFallTime(releaseAlt, (float)threat.Target.TypicalAltitude);
float deliveryTime = flightTime + fallTime;
float interceptTime = threat.ArrivalTime;
if (deliveryTime > interceptTime) return null;
if (!HasInterceptWindow(threat, midArc, expansionTime, deliveryTime)) return null;
float avgSpeed = GetDroneSpeedMs(threat);
@ -485,11 +500,22 @@ namespace CounterDrone.Core.Algorithms
float typicalSpeed = GetDroneSpeedKph(threat);
if (typicalSpeed <= 0) return (events, "目标速度无效");
// 航路感知布局:穿越点弧长 = 航路中点弧长 + 沿航路偏移
// targetOffset 现在是沿航路切向的弧长偏移(不再是 X 分量),支持任意方向航路
// 用 InterceptCalculator 计算拦截弧长(替换硬编码中点)
var mid = ThreatMidpoint(threat);
float midArc = RouteGeometry.ArcLengthNearestTo(wps, mid.X, mid.Z);
float crossArc = midArc + targetOffset;
float crossArc;
if (unit.Type == PlatformType.GroundBased)
{
var (ia, _) = InterceptCalculator.Compute(
wps, threat.DetectArc, typicalSpeed,
_config.ReactionTime + expansionTime, unit.Position, unit.MuzzleVelocity);
if (ia <= 0) return (events, $"拦截点计算失败");
crossArc = ia;
}
else
{
crossArc = midArc + targetOffset;
}
// 编队横向偏移:按车道分布在航路法向上,与 DroneEntity 编队偏移一致(起点展开)。
// DroneEntity: latOffset = formationIndex * lateralSpacing0/50/100...

View File

@ -35,6 +35,9 @@ namespace CounterDrone.Core.Algorithms
/// <summary>拦截窗口安全余量s。计算所需探测弧长时在膨胀时间基础上额外预留。默认 1</summary>
public float TimingSafetyMargin { get; set; } = 1f;
/// <summary>火力单元反应时间s。探测到目标后装填+瞄准的时间。默认 5</summary>
public float ReactionTime { get; set; } = 5f;
private const string ConfigFileName = "planner_config.json";
/// <summary>从 dataRoot 加载配置。文件缺失或字段非法即抛异常。</summary>

View File

@ -72,10 +72,14 @@ namespace CounterDrone.Core
scene.WindDirection = (int)WindDirection.W;
s.SaveScene(t.Id, scene);
s.SaveTarget(t.Id, d.Targets.First(p => p.Id == "shahed").ToTargetConfig());
s.SaveRoute(t.Id, "default", d.Formations.First(f => f.Id == "single").ToRoutePlan(), R("5km-h500", 200, d));
s.SaveRoute(t.Id, "default", d.Formations.First(f => f.Id == "single").ToRoutePlan(),
new List<Waypoint> {
new() { PosX = 0, PosY = 500, PosZ = 0, Speed = 200 },
new() { PosX = 7000, PosY = 500, PosZ = 0, Speed = 200 },
});
s.SaveDeployment(t.Id, new List<EquipmentDeployment>
{
d.FireUnits.First(f => f.Id == "ground-light").ToEquipmentDeployment(AerosolType.InertGas, 1, 1500, 0, 50),
d.FireUnits.First(f => f.Id == "ground-light").ToEquipmentDeployment(AerosolType.InertGas, 1, 7000, 0, 50),
});
s.SaveCloudDispersal(t.Id, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 });
s.UpdateStep(t.Id, 5);
@ -86,12 +90,16 @@ namespace CounterDrone.Core
var t = s.CreateTask("[Demo] 喷气式拦截-活性材料", "");
s.SaveScene(t.Id, d.Weather.First(w => w.Id == "sunny-calm").ToCombatScene());
s.SaveTarget(t.Id, d.Targets.First(p => p.Id == "cruise-missile").ToTargetConfig());
s.SaveRoute(t.Id, "default", d.Formations.First(f => f.Id == "single").ToRoutePlan(), R("5km-h800", 200, d));
s.SaveRoute(t.Id, "default", d.Formations.First(f => f.Id == "single").ToRoutePlan(),
new List<Waypoint> {
new() { PosX = 0, PosY = 500, PosZ = 0, Speed = 200 },
new() { PosX = 10000, PosY = 500, PosZ = 0, Speed = 200 },
});
s.SaveDeployment(t.Id, new List<EquipmentDeployment>
{
d.FireUnits.First(f => f.Id == "ground-standard").ToEquipmentDeployment(AerosolType.ActiveMaterial, 1, 1500, 0, 50),
d.FireUnits.First(f => f.Id == "ground-standard").ToEquipmentDeployment(AerosolType.ActiveMaterial, 1, 10000, 0, 50),
});
s.SaveCloudDispersal(t.Id, new CloudDispersal { AerosolType = (int)AerosolType.ActiveMaterial, DisperseHeight = 800 });
s.SaveCloudDispersal(t.Id, new CloudDispersal { AerosolType = (int)AerosolType.ActiveMaterial, DisperseHeight = 500 });
s.UpdateStep(t.Id, 5);
}
@ -106,7 +114,7 @@ namespace CounterDrone.Core
s.SaveRoute(t.Id, "default", d.Formations.First(f => f.Id == "single").ToRoutePlan(), R("20km-h500", 200, d));
s.SaveDeployment(t.Id, new List<EquipmentDeployment>
{
d.FireUnits.First(f => f.Id == "air-standard").ToEquipmentDeployment(AerosolType.InertGas, 3, 8000, 1000, 0),
d.FireUnits.First(f => f.Id == "air-standard").ToEquipmentDeployment(AerosolType.InertGas, 3, 12000, 1000, 0),
});
s.SaveCloudDispersal(t.Id, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 });
s.UpdateStep(t.Id, 5);
@ -123,7 +131,7 @@ namespace CounterDrone.Core
s.SaveRoute(t.Id, "default", d.Formations.First(f => f.Id == "line-3").ToRoutePlan(), R("20km-h500", 200, d));
s.SaveDeployment(t.Id, new List<EquipmentDeployment>
{
d.FireUnits.First(f => f.Id == "air-standard").ToEquipmentDeployment(AerosolType.InertGas, 3, 8000, 1000, 0),
d.FireUnits.First(f => f.Id == "air-standard").ToEquipmentDeployment(AerosolType.InertGas, 3, 12000, 1000, 0),
});
s.SaveCloudDispersal(t.Id, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 });
s.UpdateStep(t.Id, 5);

View File

@ -29,7 +29,10 @@ namespace CounterDrone.Core.Simulation
// 空基投放:继承载机速度
private readonly float _carrierVelX, _carrierVelY, _carrierVelZ;
// 地基:标记是否已超过释放高度(等待下降阶段到达)
private bool _hasExceededReleaseAltitude;
private bool _hasArrived;
private float _arrivalTimer;
public MunitionEntity(string id, PlatformType launchMode, AerosolType aerosolType,
float startX, float startY, float startZ,

View File

@ -136,9 +136,11 @@ namespace CounterDrone.Core.Tests
{
var eng = RunPreset("喷气式拦截-活性材料");
var drone = eng.Drones[0];
var detail = _scenario.GetTaskDetail(_taskId)!;
var launched = eng.Events.Count(e => e.Type == SimEventType.MunitionLaunched);
Assert.True(launched > 0);
Assert.Equal(DroneStatus.Destroyed, drone.Status);
var report = new ReportGenerator().Generate(detail, eng.Events.ToList(), drone.Status, eng.SimulationTime);
Assert.True(launched > 0, $"No launches. Report:\n{report}");
Assert.True(drone.Status == DroneStatus.Destroyed, $"Report:\n{report}");
VerifyAndExportReport(eng, drone);
}
// ═══════════════════════════════════════════════

View File

@ -23,7 +23,6 @@ namespace CounterDrone.Core.Tests
[Fact]
public void GroundBased_ArrivesNearTarget_WithInterpolation()
{
// 大 tick 步长也应到达目标附近
var m = new MunitionEntity("m2", PlatformType.GroundBased,
AerosolType.InertGas,
startX: 5000, startY: 0, startZ: 50,
@ -41,6 +40,43 @@ namespace CounterDrone.Core.Tests
Assert.True(System.Math.Abs(m.PosX - 10000) < 200f, $"X={m.PosX:F1} 应≈10000");
}
// ═══════════════════════════════════════
// 抛物线运动学:固定角度验证
// ═══════════════════════════════════════
[Fact]
public void Parabolic_45Deg_Azimuth0_MovesInZ()
{
// ParabolicPosition: azimuth=0 → +Z方向, sin(0)=0→X, cos(0)=1→Z
float angle45 = 45f * (float)System.Math.PI / 180f;
var (x, y, z) = Kinematics.ParabolicPosition(0, 0, 0, angle45, 0, 100, 1f);
Assert.True(System.Math.Abs(x) < 0.1f, $"X={x:F1} ≈0");
Assert.True(System.Math.Abs(y - 65.8f) < 1f, $"Y={y:F1} ≈65.8");
Assert.True(System.Math.Abs(z - 70.7f) < 1f, $"Z={z:F1} ≈70.7");
}
[Fact]
public void Parabolic_30Deg_Azimuth90_MovesInX()
{
// azimuth=90°(π/2): sin(π/2)=1→X, cos(π/2)=0→Z
float angle30 = 30f * (float)System.Math.PI / 180f;
float az90 = 90f * (float)System.Math.PI / 180f;
var (x, y, z) = Kinematics.ParabolicPosition(0, 0, 0, angle30, az90, 100, 1f);
Assert.True(System.Math.Abs(x - 86.6f) < 1f, $"X={x:F1} ≈86.6");
Assert.True(System.Math.Abs(z) < 0.1f, $"Z={z:F1} ≈0");
}
[Fact]
public void Parabolic_60Deg_Azimuth270_MovesBackward()
{
// azimuth=270°(3π/2): sin=-1→X方向, cos=0→Z
float angle60 = 60f * (float)System.Math.PI / 180f;
float az270 = 270f * (float)System.Math.PI / 180f;
var (x, y, z) = Kinematics.ParabolicPosition(0, 0, 0, angle60, az270, 100, 1f);
Assert.True(System.Math.Abs(x + 50f) < 1f, $"X={x:F1} ≈-50");
Assert.True(System.Math.Abs(z) < 0.1f, $"Z={z:F1} ≈0");
}
[Fact]
public void GroundBased_LaunchAngle_ReachesReleaseAltitude()
{