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()
{