统一平台物理模型:去除空基/地基分支

- BuildCandidate 合并 BuildAirBased/GroundBasedCandidate 为单一方法,统一用 MuzzleVelocity
- TryGenerateFireEvents 统一用 InterceptCalculator.Compute,去除 ComputeHorizontal
- 抛物线选解策略:两解逐一验证可行性(距离匹配 + fireTime>0),选最早拦截的解
- 空基 FireUnit 补充 MuzzleVelocity 字段(等于 CruiseSpeed)
- 所有 238 个单元测试通过
This commit is contained in:
tian 2026-06-18 13:40:48 +08:00
parent 92d1131dc1
commit fb87fdfa22
6 changed files with 49 additions and 124 deletions

View File

@ -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": 80.0, "ReleaseAltitude": 1000.0,
"MuzzleVelocity": 80.0, "CruiseSpeed": 80.0, "ReleaseAltitude": 1000.0,
"AmmoTypes": [0, 1],
"EORange": 4000.0, "IRRange": 3000.0,
"MinElevation": -80.0, "MaxElevation": 30.0, "MinDetectAlt": 30.0, "MaxDetectAlt": 15000.0

View File

@ -334,22 +334,14 @@ namespace CounterDrone.Core.Algorithms
{
if (!unit.AmmoTypes.Contains(neededAmmo)) continue;
if (unit.Type == PlatformType.AirBased)
{
var c = BuildAirBasedCandidate(threat, unit, neededAmmo, ammo, ammoEff, expansionTime, mid, midArc, env);
if (c != null) candidates.Add(c);
}
else
{
var c = BuildGroundBasedCandidate(threat, unit, neededAmmo, ammo, ammoEff, expansionTime, mid, midArc, env);
if (c != null) candidates.Add(c);
}
var c = BuildCandidate(threat, unit, neededAmmo, ammo, ammoEff, expansionTime, mid, midArc, env);
if (c != null) candidates.Add(c);
}
return candidates;
}
private InterceptCandidate? BuildGroundBasedCandidate(DroneWave threat,
private InterceptCandidate? BuildCandidate(DroneWave threat,
FireUnit unit, AerosolType ammoType, AmmunitionSpec ammo,
float effectiveR, float expansionTime,
Vector3 mid, float midArc, CombatScene env)
@ -362,55 +354,10 @@ namespace CounterDrone.Core.Algorithms
float maxRange = unit.MuzzleVelocity * unit.MuzzleVelocity / 9.81f;
if (dist > maxRange) return null;
float shellTime = dist / unit.MuzzleVelocity;
if (!HasInterceptWindow(threat, midArc, expansionTime, shellTime)) return null;
float interceptTime = threat.ArrivalTime;
float avgSpeed = GetDroneSpeedMs(threat);
float neededExposure = _damageModel.RequiredExposureSeconds((TargetType)threat.Target.TargetType, (PowerType)threat.Target.PowerType, ammoType);
float actualExposure = effectiveR * 2f / avgSpeed;
float prob = Math.Min(_config.MaxInterceptProbability, actualExposure / neededExposure);
return new InterceptCandidate
{
Unit = unit,
AmmoType = ammoType,
EarliestInterceptTime = interceptTime,
KillProbability = prob,
};
}
private InterceptCandidate? BuildAirBasedCandidate(DroneWave threat,
FireUnit unit, AerosolType ammoType, AmmunitionSpec ammo,
float effectiveR, float expansionTime,
Vector3 mid, float midArc, CombatScene env)
{
if (unit.ReleaseAltitude <= 0 || unit.CruiseSpeed <= 0) return null;
float releaseAlt = unit.ReleaseAltitude;
float flightDist = Kinematics.Distance3D(
unit.Position.X, unit.Position.Y, unit.Position.Z,
mid.X, releaseAlt, mid.Z);
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;
float deliveryTime = dist / unit.MuzzleVelocity;
if (deliveryTime > threat.ArrivalTime) return null;
if (!HasInterceptWindow(threat, midArc, expansionTime, deliveryTime)) return null;
// 最小距离约束:无人机至少需要飞 vd*(R+expansion+fallTime) 才能被拦截
float speedMs = GetDroneSpeedMs(threat);
float minArc = threat.DetectArc + speedMs * (_config.ReactionTime + expansionTime + fallTime);
float totalArc = RouteGeometry.TotalLength(threat.Waypoints);
if (minArc >= totalArc) return null;
var (icArc, _, _) = InterceptCalculator.ComputeHorizontal(
threat.Waypoints, threat.DetectArc, GetDroneSpeedKph(threat),
_config.ReactionTime + expansionTime, unit.Position, unit.CruiseSpeed,
unit.ReleaseAltitude, (float)threat.Target.TypicalAltitude);
if (icArc <= 0) return null;
float avgSpeed = GetDroneSpeedMs(threat);
float neededExposure = _damageModel.RequiredExposureSeconds((TargetType)threat.Target.TargetType, (PowerType)threat.Target.PowerType, ammoType);
float actualExposure = effectiveR * 2f / avgSpeed;
@ -420,7 +367,7 @@ namespace CounterDrone.Core.Algorithms
{
Unit = unit,
AmmoType = ammoType,
EarliestInterceptTime = interceptTime,
EarliestInterceptTime = threat.ArrivalTime,
KillProbability = prob,
};
}
@ -494,34 +441,22 @@ namespace CounterDrone.Core.Algorithms
float typicalSpeed = GetDroneSpeedKph(threat);
if (typicalSpeed <= 0) return (events, "目标速度无效");
// 用 InterceptCalculator 计算拦截弧长和发射角
// ═══ 拦截弧长 ═══
var mid = ThreatMidpoint(threat);
float midArc = RouteGeometry.ArcLengthNearestTo(wps, mid.X, mid.Z);
float crossArc;
float launchAngle = 0;
float interceptShellTime = 0;
if (unit.Type == PlatformType.GroundBased)
{
var (ia, st, la) = InterceptCalculator.Compute(
wps, threat.DetectArc, typicalSpeed,
_config.ReactionTime + expansionTime, unit.Position, unit.MuzzleVelocity);
crossArc = (ia > 0 ? ia : midArc) + targetOffset;
launchAngle = la;
interceptShellTime = st;
}
else
{
var (ia, _, la) = InterceptCalculator.ComputeHorizontal(
wps, threat.DetectArc, typicalSpeed,
_config.ReactionTime + expansionTime, unit.Position, unit.CruiseSpeed,
unit.Position.Y, (float)threat.Target.TypicalAltitude);
crossArc = ia + targetOffset;
launchAngle = la;
}
var (ia, _, _) = InterceptCalculator.Compute(
wps, threat.DetectArc, typicalSpeed,
_config.ReactionTime + expansionTime, unit.Position, unit.MuzzleVelocity);
float crossArc = (ia > 0 ? ia : midArc) + targetOffset;
// 编队横向偏移:按车道分布在航路法向上,与 DroneEntity 编队偏移一致(起点展开)。
// DroneEntity: latOffset = formationIndex * lateralSpacing0/50/100...
// 编队展开方向:读 RoutePlan.LateralAxis (0=X, 1=Y, 2=Z)
// 无人机到达穿越点的时间:基于探测边界,而非航路起点
float travelArc = crossArc - threat.DetectArc;
if (travelArc < 0) return (events, $"探测边界在拦截点之后({threat.DetectArc:F0}m≥{crossArc:F0}m");
float txArrival = RouteGeometry.TravelTimeTo(wps, travelArc, typicalSpeed);
float recommendedTiming = txArrival - expansionTime;
if (recommendedTiming <= 0f) return (events, $"膨胀{expansionTime:F1}s≥到达{txArrival:F1}s");
// ═══ 编队横向偏移 ═══
int axis = threat.Route?.LateralAxis ?? 2;
float laneSpacing = yLanes > 1 ? formationWidth / (yLanes - 1) : 0f;
float laneOffset = yLane * laneSpacing;
@ -540,48 +475,38 @@ namespace CounterDrone.Core.Algorithms
float dx = cloudGenX - unit.Position.X;
float dz = cloudGenZ - unit.Position.Z;
float targetDist = (float)Math.Sqrt(dx * dx + dz * dz);
float mv = unit.Type == PlatformType.AirBased ? unit.CruiseSpeed : unit.MuzzleVelocity;
float mv = unit.MuzzleVelocity;
if (mv <= 0)
throw new InvalidOperationException($"单元 {unit.Id}: 速度={mv} 必须>0");
throw new InvalidOperationException($"单元 {unit.Id}: MuzzleVelocity={mv} 必须>0");
float heightDiff = ty - unit.Position.Y;
if (unit.Type == PlatformType.AirBased && heightDiff >= 0)
return (events, $"空基单元 {unit.Id}: 平台高度({unit.Position.Y:F0})≤目标高度({ty:F0}),无法重力下落");
// 每发独立计算发射角,选离无人机更近的命中点(更早拦截)
launchAngle = Kinematics.CalculateLaunchAngle(targetDist, mv, heightDiff);
var motion = new ParabolicMotion(mv, launchAngle);
var times = motion.GetFlightTimes(heightDiff);
if (times.Length == 0)
// ═══ 抛物线求解:两解逐一验证,选最早摧毁无人机的可行解 ═══
var angles = ParabolicMotion.SolveAngles(targetDist, heightDiff, mv);
if (!angles.HasValue)
return (events, $"目标超出弹道射程: dist={targetDist:F0}m");
// 两个解时:远点更靠近无人机(较早拦截),优先选远点
float tof = times.Length == 2 ? times[1] : times[0];
float range = mv * (float)Math.Cos(launchAngle) * tof;
// 无人机到达穿越点的时间:基于探测边界,而非航路起点。
// planner 是参谋,只能基于探测信息规划。威胁从探测边界被发现,
// 飞行时间 = (拦截弧长 - 探测弧长) / 速度。无探测时 DetectArc=0回退到起点
float travelArc = crossArc - threat.DetectArc;
if (travelArc < 0) return (events, $"探测边界在拦截点之后({threat.DetectArc:F0}m≥{crossArc:F0}m");
float txArrival = RouteGeometry.TravelTimeTo(wps, travelArc, typicalSpeed);
float recommendedTiming = txArrival - expansionTime;
if (recommendedTiming <= 0f) return (events, $"膨胀{expansionTime:F1}s≥到达{txArrival:F1}s");
// cloudGenX/Z 已在上面计算(含风偏),直接使用
float deliveryTime;
if (unit.Type == PlatformType.AirBased)
deliveryTime = tof;
else if (interceptShellTime <= 0)
return (events, "拦截计算未得出有效飞行时间");
else
float? bestLaunchAngle = null, bestTof = null, bestFireTime = null;
foreach (var a in new[] { angles.Value.Item1, angles.Value.Item2 })
{
deliveryTime = interceptShellTime;
tof = interceptShellTime;
var motion = new ParabolicMotion(mv, a);
var ts = motion.GetFlightTimes(heightDiff);
foreach (var t in ts)
{
float r = mv * (float)Math.Cos(a) * t;
if (Math.Abs(r - targetDist) > 0.5f) continue;
float ft = recommendedTiming - t;
if (ft <= 0f) continue;
// 选最早摧毁fireTime 最小 → 发射最早 → 拦截最早)
if (!bestFireTime.HasValue || ft < bestFireTime.Value)
{
bestLaunchAngle = a; bestTof = t; bestFireTime = ft;
}
}
}
float fireTime = recommendedTiming - deliveryTime;
if (fireTime <= 0f)
return (events, $"发射时机{fireTime:F1}s≤0到达{deliveryTime:F1}s>窗口{recommendedTiming:F1}s");
if (!bestLaunchAngle.HasValue)
return (events, $"所有弹道解均不可行: dist={targetDist:F0}m");
float launchAngle = bestLaunchAngle.Value;
float tof = bestTof!.Value;
float fireTime = bestFireTime!.Value;
events.Add(new FireEvent
{

View File

@ -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": 80.0, "ReleaseAltitude": 1000.0,
"MuzzleVelocity": 80.0, "CruiseSpeed": 80.0, "ReleaseAltitude": 1000.0,
"AmmoTypes": [0, 1],
"EORange": 4000.0, "IRRange": 3000.0,
"MinElevation": -80.0, "MaxElevation": 30.0, "MinDetectAlt": 30.0, "MaxDetectAlt": 15000.0

View File

@ -32,7 +32,7 @@ namespace CounterDrone.Core.Tests
Id = id, Type = PlatformType.AirBased,
Position = new Vector3(posX, 2500, -2000),
GunCount = 1, ChannelsPerGun = 16,
CruiseSpeed = 55, ReleaseAltitude = 1500,
MuzzleVelocity = 55, CruiseSpeed = 55, ReleaseAltitude = 1500,
TotalMunitions = 16, Cooldown = 5f,
AmmoTypes = new() { AerosolType.InertGas },
};
@ -247,7 +247,7 @@ namespace CounterDrone.Core.Tests
{
Id = "u0", Type = PlatformType.AirBased,
Position = new Vector3(100000, 2500, 100000), // 极远
GunCount = 1, ChannelsPerGun = 4, CruiseSpeed = 10, ReleaseAltitude = 1500,
GunCount = 1, ChannelsPerGun = 4, MuzzleVelocity = 10, CruiseSpeed = 10, ReleaseAltitude = 1500,
TotalMunitions = 4, AmmoTypes = new() { AerosolType.InertGas },
};
var result = Plan(new() { unit }, MakeThreat());

Binary file not shown.