From 016d9ac24b8deeb35a40b89e73d469e1454ae2d3 Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Tue, 16 Jun 2026 16:24:01 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20InterceptCalculator=20=E9=9B=86?= =?UTF-8?q?=E6=88=90=E5=88=B0=20planner=EF=BC=8C=E6=9B=BF=E6=8D=A2?= =?UTF-8?q?=E7=A1=AC=E7=BC=96=E7=A0=81=E4=B8=AD=E7=82=B9=E9=80=BB=E8=BE=91?= =?UTF-8?q?=EF=BC=9BPlannerConfig=20+ReactionTime=EF=BC=9BJet=20=E9=80=9A?= =?UTF-8?q?=E8=BF=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/defaults.json | 6 +-- data/planner_config.json | 3 +- .../Algorithms/AlgorithmTypes.cs | 9 +++-- .../Algorithms/DefensePlanner.cs | 40 +++++++++++++++---- .../Algorithms/PlannerConfig.cs | 3 ++ src/CounterDrone.Core/DefaultScenarios.cs | 22 ++++++---- .../Simulation/MunitionEntity.cs | 3 ++ .../FullPipelineTests.cs | 6 ++- .../MunitionEntityTests.cs | 38 +++++++++++++++++- 9 files changed, 106 insertions(+), 24 deletions(-) diff --git a/data/defaults.json b/data/defaults.json index 5115d2a..d8365be 100644 --- a/data/defaults.json +++ b/data/defaults.json @@ -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 } ], diff --git a/data/planner_config.json b/data/planner_config.json index 8518c27..f2ad3bb 100644 --- a/data/planner_config.json +++ b/data/planner_config.json @@ -16,5 +16,6 @@ }, "DefaultDetectionAccuracy": 50.0, "ExpansionFactor": 0.9, - "TimingSafetyMargin": 1.0 + "TimingSafetyMargin": 1.0, + "ReactionTime": 5.0 } diff --git a/src/CounterDrone.Core/Algorithms/AlgorithmTypes.cs b/src/CounterDrone.Core/Algorithms/AlgorithmTypes.cs index 37c029c..518d959 100644 --- a/src/CounterDrone.Core/Algorithms/AlgorithmTypes.cs +++ b/src/CounterDrone.Core/Algorithms/AlgorithmTypes.cs @@ -68,8 +68,7 @@ namespace CounterDrone.Core.Algorithms /// 探测精度(位置误差 m)。影响 planner 抛撒散布范围。 public float DetectAccuracy { get; set; } - /// 预计到达航路中点的时间(秒)。 - /// 物理含义:匀速直线运动从航路起点到中点的飞行时间。 + /// 预计到达航路中点的时间(秒)。基于探测边界,而非航路起点。 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); } } diff --git a/src/CounterDrone.Core/Algorithms/DefensePlanner.cs b/src/CounterDrone.Core/Algorithms/DefensePlanner.cs index 5456b75..2c85096 100644 --- a/src/CounterDrone.Core/Algorithms/DefensePlanner.cs +++ b/src/CounterDrone.Core/Algorithms/DefensePlanner.cs @@ -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 * lateralSpacing(0/50/100...) diff --git a/src/CounterDrone.Core/Algorithms/PlannerConfig.cs b/src/CounterDrone.Core/Algorithms/PlannerConfig.cs index 66421cc..b0edfcc 100644 --- a/src/CounterDrone.Core/Algorithms/PlannerConfig.cs +++ b/src/CounterDrone.Core/Algorithms/PlannerConfig.cs @@ -35,6 +35,9 @@ namespace CounterDrone.Core.Algorithms /// 拦截窗口安全余量(s)。计算所需探测弧长时在膨胀时间基础上额外预留。默认 1 public float TimingSafetyMargin { get; set; } = 1f; + /// 火力单元反应时间(s)。探测到目标后装填+瞄准的时间。默认 5 + public float ReactionTime { get; set; } = 5f; + private const string ConfigFileName = "planner_config.json"; /// 从 dataRoot 加载配置。文件缺失或字段非法即抛异常。 diff --git a/src/CounterDrone.Core/DefaultScenarios.cs b/src/CounterDrone.Core/DefaultScenarios.cs index 12bb1b1..51f26f0 100644 --- a/src/CounterDrone.Core/DefaultScenarios.cs +++ b/src/CounterDrone.Core/DefaultScenarios.cs @@ -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 { + new() { PosX = 0, PosY = 500, PosZ = 0, Speed = 200 }, + new() { PosX = 7000, PosY = 500, PosZ = 0, Speed = 200 }, + }); s.SaveDeployment(t.Id, new List { - 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 { + new() { PosX = 0, PosY = 500, PosZ = 0, Speed = 200 }, + new() { PosX = 10000, PosY = 500, PosZ = 0, Speed = 200 }, + }); s.SaveDeployment(t.Id, new List { - 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 { - 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 { - 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); diff --git a/src/CounterDrone.Core/Simulation/MunitionEntity.cs b/src/CounterDrone.Core/Simulation/MunitionEntity.cs index 94ff05a..73af8a9 100644 --- a/src/CounterDrone.Core/Simulation/MunitionEntity.cs +++ b/src/CounterDrone.Core/Simulation/MunitionEntity.cs @@ -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, diff --git a/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs b/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs index 94fb506..cd3a18e 100644 --- a/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs +++ b/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs @@ -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); } // ═══════════════════════════════════════════════ diff --git a/test/unit/CounterDrone.Core.Tests/MunitionEntityTests.cs b/test/unit/CounterDrone.Core.Tests/MunitionEntityTests.cs index e4e5e5d..ea53388 100644 --- a/test/unit/CounterDrone.Core.Tests/MunitionEntityTests.cs +++ b/test/unit/CounterDrone.Core.Tests/MunitionEntityTests.cs @@ -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() {