diff --git a/data/defaults.json b/data/defaults.json index d508162..361d99f 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": 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 diff --git a/src/CounterDrone.Core/Algorithms/DefensePlanner.cs b/src/CounterDrone.Core/Algorithms/DefensePlanner.cs index 964c48e..97e8b70 100644 --- a/src/CounterDrone.Core/Algorithms/DefensePlanner.cs +++ b/src/CounterDrone.Core/Algorithms/DefensePlanner.cs @@ -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 * lateralSpacing(0/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 { diff --git a/src/Unity/Assets/Plugins/CounterDrone.Core/CounterDrone.Core.dll b/src/Unity/Assets/Plugins/CounterDrone.Core/CounterDrone.Core.dll index 86fec23..f4d28df 100644 Binary files a/src/Unity/Assets/Plugins/CounterDrone.Core/CounterDrone.Core.dll and b/src/Unity/Assets/Plugins/CounterDrone.Core/CounterDrone.Core.dll differ diff --git a/src/Unity/Assets/StreamingAssets/defaults.json b/src/Unity/Assets/StreamingAssets/defaults.json index d508162..361d99f 100644 --- a/src/Unity/Assets/StreamingAssets/defaults.json +++ b/src/Unity/Assets/StreamingAssets/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": 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 diff --git a/test/unit/CounterDrone.Core.Tests/DefensePlannerTests.cs b/test/unit/CounterDrone.Core.Tests/DefensePlannerTests.cs index 3846074..f5be091 100644 --- a/test/unit/CounterDrone.Core.Tests/DefensePlannerTests.cs +++ b/test/unit/CounterDrone.Core.Tests/DefensePlannerTests.cs @@ -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()); diff --git a/unity_plugins/CounterDrone.Core.dll b/unity_plugins/CounterDrone.Core.dll index c9d6dfb..f4d28df 100644 Binary files a/unity_plugins/CounterDrone.Core.dll and b/unity_plugins/CounterDrone.Core.dll differ