feat: InterceptCalculator 集成到 planner,替换硬编码中点逻辑;PlannerConfig +ReactionTime;Jet 通过
This commit is contained in:
parent
2089ff6c7b
commit
016d9ac24b
@ -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
|
||||
}
|
||||
],
|
||||
|
||||
@ -16,5 +16,6 @@
|
||||
},
|
||||
"DefaultDetectionAccuracy": 50.0,
|
||||
"ExpansionFactor": 0.9,
|
||||
"TimingSafetyMargin": 1.0
|
||||
"TimingSafetyMargin": 1.0,
|
||||
"ReactionTime": 5.0
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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...)
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
}
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
@ -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()
|
||||
{
|
||||
|
||||
Loading…
Reference in New Issue
Block a user