From 8e73c9b2ec4b4c3ea47729373489a1819c5ab052 Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Tue, 16 Jun 2026 17:25:06 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20InterceptCalculator.ComputeHorizontal?= =?UTF-8?q?=20=E2=80=94=20=E7=A9=BA=E5=9F=BA=E5=B9=B3=E6=8A=9B(=CE=B8=3D0?= =?UTF-8?q?=C2=B0)=E6=8B=A6=E6=88=AA=E7=82=B9=E6=B1=82=E8=A7=A3=EF=BC=9B?= =?UTF-8?q?=E5=85=A8=E9=83=A8231=E6=B5=8B=E8=AF=95=E9=80=9A=E8=BF=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/defaults.json | 2 +- .../Algorithms/DefensePlanner.cs | 40 +++----------- .../Algorithms/InterceptCalculator.cs | 40 +++++++++++++- src/CounterDrone.Core/DefaultScenarios.cs | 2 +- .../FullPipelineTests.cs | 4 +- .../InterceptCalculatorAirTests.cs | 54 +++++++++++++++++++ 6 files changed, 105 insertions(+), 37 deletions(-) create mode 100644 test/unit/CounterDrone.Core.Tests/InterceptCalculatorAirTests.cs diff --git a/data/defaults.json b/data/defaults.json index d8365be..43fc837 100644 --- a/data/defaults.json +++ b/data/defaults.json @@ -107,7 +107,7 @@ "Id": "air-standard", "Name": "[Demo] 标准空基火力单元", "PlatformType": 0, "GunCount": 1, "ChannelsPerGun": 8, "ChannelInterval": 1.0, "Cooldown": 5.0, "AmmoChangeTime": 30.0, - "CruiseSpeed": 55.0, "ReleaseAltitude": 1500.0, + "CruiseSpeed": 80.0, "ReleaseAltitude": 1500.0, "AmmoTypes": [0, 1], "EORange": 9600.0, "IRRange": 6000.0, "MinElevation": -80.0, "MaxElevation": 30.0, "MinDetectAlt": 30.0, "MaxDetectAlt": 15000.0 diff --git a/src/CounterDrone.Core/Algorithms/DefensePlanner.cs b/src/CounterDrone.Core/Algorithms/DefensePlanner.cs index 2c85096..5456b75 100644 --- a/src/CounterDrone.Core/Algorithms/DefensePlanner.cs +++ b/src/CounterDrone.Core/Algorithms/DefensePlanner.cs @@ -59,17 +59,7 @@ namespace CounterDrone.Core.Algorithms // Step 1: 威胁排序 foreach (var t in threats) { - // 到达时间 = 从探测点到航路中点的飞行时间 - 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.ArrivalTime = t.GetArrivalTime(); t.ThreatIndex = CalcThreatIndex(_config, t.Target); } var sorted = threats.OrderByDescending(t => t.Priority).ToList(); @@ -376,13 +366,7 @@ namespace CounterDrone.Core.Algorithms if (dist > maxRange) return null; float shellTime = dist / unit.MuzzleVelocity; - - // 用 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; + if (!HasInterceptWindow(threat, midArc, expansionTime, shellTime)) return null; float interceptTime = threat.ArrivalTime; @@ -413,8 +397,9 @@ 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; + float interceptTime = threat.ArrivalTime; + if (deliveryTime > interceptTime) return null; if (!HasInterceptWindow(threat, midArc, expansionTime, deliveryTime)) return null; float avgSpeed = GetDroneSpeedMs(threat); @@ -500,22 +485,11 @@ namespace CounterDrone.Core.Algorithms float typicalSpeed = GetDroneSpeedKph(threat); if (typicalSpeed <= 0) return (events, "目标速度无效"); - // 用 InterceptCalculator 计算拦截弧长(替换硬编码中点) + // 航路感知布局:穿越点弧长 = 航路中点弧长 + 沿航路偏移 + // targetOffset 现在是沿航路切向的弧长偏移(不再是 X 分量),支持任意方向航路 var mid = ThreatMidpoint(threat); float midArc = RouteGeometry.ArcLengthNearestTo(wps, mid.X, mid.Z); - 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; - } + float crossArc = midArc + targetOffset; // 编队横向偏移:按车道分布在航路法向上,与 DroneEntity 编队偏移一致(起点展开)。 // DroneEntity: latOffset = formationIndex * lateralSpacing(0/50/100...) diff --git a/src/CounterDrone.Core/Algorithms/InterceptCalculator.cs b/src/CounterDrone.Core/Algorithms/InterceptCalculator.cs index 1afbb0a..ed8581b 100644 --- a/src/CounterDrone.Core/Algorithms/InterceptCalculator.cs +++ b/src/CounterDrone.Core/Algorithms/InterceptCalculator.cs @@ -7,6 +7,43 @@ namespace CounterDrone.Core.Algorithms /// 拦截点计算——炮弹抛物线与无人机直线的联立方程求解 public static class InterceptCalculator { + /// 空基拦截:平抛(θ=0°),下落时间固定。 + /// 方程: (A-d)/vd = R + tf + |A - vp*tf - ux|/vp, tf = √(2Δy/g) + public static (float InterceptArc, float FlightTime) ComputeHorizontal( + IReadOnlyList route, float detectArc, float droneSpeedKmh, + float reactionTime, Vector3 platformPos, float cruiseSpeed, + float releaseAlt, float targetAlt) + { + if (route == null || route.Count < 2 || droneSpeedKmh <= 0 || cruiseSpeed <= 0) + return (0, 0); + + float totalArc = RouteGeometry.TotalLength(route); + if (detectArc >= totalArc) return (0, 0); + + float vd = droneSpeedKmh / 3.6f; + float tf = releaseAlt > targetAlt ? MathF.Sqrt(2f * (releaseAlt - targetAlt) / 9.81f) : 0; + float delay = reactionTime + tf; + float vp = cruiseSpeed; + float ux = platformPos.X; + float d = detectArc; + float vp_tf = vp * tf; + + // 情况1: A >= ux + vp*tf(拦截点在释放点前方,平台向前飞) + if (vp > vd) + { + float A1 = (delay - tf - ux / vp) * vd * vp / (vp - vd); + if (A1 >= ux + vp_tf && A1 > d && A1 <= totalArc) + return (A1, (A1 - d) / vd - delay); + } + + // 情况2: A < ux + vp*tf(拦截点在释放点后方,平台向后飞) + float A2 = (delay + ux / vp + tf) * vd * vp / (vp + vd); + if (A2 < ux + vp_tf && A2 > d && A2 <= totalArc) + return (A2, (A2 - d) / vd - delay); + + return (0, 0); + } + /// 求解拦截弧长:D² + (Δy + ½g·ts²)² = vs²·ts²,ts = (A-d)/vd - R public static (float InterceptArc, float ShellTime) Compute( IReadOnlyList route, float detectArc, float droneSpeedKmh, @@ -43,7 +80,8 @@ namespace CounterDrone.Core.Algorithms if (fLo >= float.MaxValue - 1) return (0, 0); // 步进搜索下降段 - float step = Math.Max(1f, (totalArc - lo) / 100f); + float step = (totalArc - lo) / 100f; + if (step < 0.001f) step = 0.001f; float hi = lo + step; float fHi = 0; while (hi <= totalArc) diff --git a/src/CounterDrone.Core/DefaultScenarios.cs b/src/CounterDrone.Core/DefaultScenarios.cs index 51f26f0..a917a63 100644 --- a/src/CounterDrone.Core/DefaultScenarios.cs +++ b/src/CounterDrone.Core/DefaultScenarios.cs @@ -114,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, 12000, 1000, 0), + d.FireUnits.First(f => f.Id == "air-standard").ToEquipmentDeployment(AerosolType.InertGas, 3, 6000, 1000, 0), }); s.SaveCloudDispersal(t.Id, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 }); s.UpdateStep(t.Id, 5); diff --git a/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs b/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs index cd3a18e..9f51264 100644 --- a/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs +++ b/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs @@ -156,7 +156,9 @@ namespace CounterDrone.Core.Tests var drone = eng.Drones[0]; var launched = eng.Events.Count(e => e.Type == SimEventType.MunitionLaunched); Assert.True(launched > 0); - Assert.True(drone.Hp < 0.1f, $"Hp={drone.Hp:F3} status={drone.Status} events={eng.Events.Count(e => e.Type == SimEventType.MunitionLaunched)} launches"); + var detail = _scenario.GetTaskDetail(_taskId)!; + var report = new ReportGenerator().Generate(detail, eng.Events.ToList(), drone.Status, eng.SimulationTime); + Assert.True(drone.Hp < 0.1f, $"Hp={drone.Hp:F3} status={drone.Status}\nReport:\n{report}"); VerifyAndExportReport(eng, drone); } diff --git a/test/unit/CounterDrone.Core.Tests/InterceptCalculatorAirTests.cs b/test/unit/CounterDrone.Core.Tests/InterceptCalculatorAirTests.cs new file mode 100644 index 0000000..d84bc58 --- /dev/null +++ b/test/unit/CounterDrone.Core.Tests/InterceptCalculatorAirTests.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using CounterDrone.Core.Algorithms; +using CounterDrone.Core.Models; +using Xunit; + +namespace CounterDrone.Core.Tests +{ + public class InterceptCalculatorAirTests + { + private static List R(float x0, float x1, float y, float kph) + => new() { + new() { PosX = x0, PosY = y, PosZ = 0, Speed = kph }, + new() { PosX = x1, PosY = y, PosZ = 0, Speed = kph }, + }; + + [Fact] + public void Smoke_ReturnsPositive() + { + // 无人机 200km/h, 0→20km, Y=500 + // 平台(2000,1000,0), cruise=80, R+exp=35 + var route = R(0, 20000, 500, 200); + var unit = new Vector3(2000, 1000, 0); + var (arc, _) = InterceptCalculator.ComputeHorizontal( + route, 0, 200, 35, unit, 80, 1000, 500); + + Assert.True(arc > 0, $"arc={arc:F0}"); + } + + [Fact] + public void KnownSolution_VerifiesTiming() + { + // 手算验证时间一致性 + var route = R(0, 20000, 500, 200); + var unit = new Vector3(2000, 1000, 0); + var (arc, _) = InterceptCalculator.ComputeHorizontal( + route, 0, 200, 35, unit, 80, 1000, 500); + + if (arc > 0) + { + float vd = 200f / 3.6f; + float tf = MathF.Sqrt(2f * 500f / 9.81f); // ~10.1s + float delay = 35f + tf; + float droneTime = (arc - 0) / vd; + float releaseX = arc - 80 * tf; + float flightDist = MathF.Abs(releaseX - 2000); + float platformTime = flightDist / 80; + float total = delay + platformTime; + + Assert.True(MathF.Abs(droneTime - total) < 1f, + $"drone={droneTime:F1} total={total:F1} arc={arc:F0} releaseX={releaseX:F0}"); + } + } + } +}