From 0b7bf2ab6fb9be6daaf0f870d335cff26b3863c1 Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Wed, 17 Jun 2026 18:52:53 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=BC=96=E9=98=9F=E8=BD=B4=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=20+=20LaneDivider=20=E7=AD=96=E7=95=A5=20+=20?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E7=89=88=E6=9C=AC=20V3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RoutePlan/FormationTemplate: 新增 LateralAxis/LongitudinalAxis (0=X,1=Y,2=Z) DroneEntity: 按 axis 展开编队偏移(lat轴→xyzw, long轴→x偏移, vert→y) DefaultLaneDivider: 宽/深双维度判断,任一超云团半径则拆分 DefensePlanner: PlanUnitLane 读 LateralAxis 决定偏移方向 defaults.json: air-standard EORange 9600→4000, 所有编队加 axis 字段, 版本 2→3 DatabaseManager: 场景版本 2→3 --- data/defaults.json | 14 ++++---- .../Algorithms/DefaultLaneDivider.cs | 30 +++++++++++++++++ .../Algorithms/DefensePlanner.cs | 28 +++++++--------- .../Algorithms/ILaneDivider.cs | 14 ++++++++ src/CounterDrone.Core/DatabaseManager.cs | 4 +-- src/CounterDrone.Core/DefaultData.cs | 4 +++ src/CounterDrone.Core/Models/RoutePlan.cs | 5 +++ .../Simulation/DroneEntity.cs | 33 +++++++++---------- .../Simulation/SimulationEngine.cs | 3 +- .../FullPipelineTests.cs | 10 +++--- 10 files changed, 96 insertions(+), 49 deletions(-) create mode 100644 src/CounterDrone.Core/Algorithms/DefaultLaneDivider.cs create mode 100644 src/CounterDrone.Core/Algorithms/ILaneDivider.cs diff --git a/data/defaults.json b/data/defaults.json index c7cb3cb..d508162 100644 --- a/data/defaults.json +++ b/data/defaults.json @@ -1,5 +1,5 @@ { - "version": "2", + "version": "3", "ammunition": [ { "Id": "inert", @@ -61,12 +61,12 @@ ], "formations": [ - { "Id": "single", "Name": "[Demo] 单机", "FormationMode": 0, "LateralCount": 1, "LongitudinalCount": 1, "LateralSpacing": 0, "LongitudinalSpacing": 0 }, - { "Id": "line-3", "Name": "[Demo] 3机横队", "FormationMode": 1, "LateralCount": 3, "LongitudinalCount": 1, "LateralSpacing": 50, "LongitudinalSpacing": 0 }, - { "Id": "line-5", "Name": "[Demo] 5机横队", "FormationMode": 1, "LateralCount": 5, "LongitudinalCount": 1, "LateralSpacing": 50, "LongitudinalSpacing": 0 }, - { "Id": "column-3", "Name": "[Demo] 3机纵队", "FormationMode": 1, "LateralCount": 1, "LongitudinalCount": 3, "LateralSpacing": 0, "LongitudinalSpacing": 100 }, - { "Id": "box-2x2", "Name": "[Demo] 2×2方队", "FormationMode": 1, "LateralCount": 2, "LongitudinalCount": 2, "LateralSpacing": 50, "LongitudinalSpacing": 100 }, - { "Id": "swarm-10", "Name": "[Demo] 蜂群(10架)", "FormationMode": 2, "LateralCount": 10, "LongitudinalCount": 1, "LateralSpacing": 30, "LongitudinalSpacing": 0 } + { "Id": "single", "Name": "[Demo] 单机", "FormationMode": 0, "LateralCount": 1, "LongitudinalCount": 1, "LateralSpacing": 0, "LongitudinalSpacing": 0, "LateralAxis": 2, "LongitudinalAxis": 0 }, + { "Id": "line-3", "Name": "[Demo] 3机横队", "FormationMode": 1, "LateralCount": 3, "LongitudinalCount": 1, "LateralSpacing": 50, "LongitudinalSpacing": 0, "LateralAxis": 2, "LongitudinalAxis": 0 }, + { "Id": "line-5", "Name": "[Demo] 5机横队", "FormationMode": 1, "LateralCount": 5, "LongitudinalCount": 1, "LateralSpacing": 50, "LongitudinalSpacing": 0, "LateralAxis": 2, "LongitudinalAxis": 0 }, + { "Id": "column-3", "Name": "[Demo] 3机纵队", "FormationMode": 1, "LateralCount": 1, "LongitudinalCount": 3, "LateralSpacing": 0, "LongitudinalSpacing": 100, "LateralAxis": 2, "LongitudinalAxis": 0 }, + { "Id": "box-2x2", "Name": "[Demo] 2×2方队", "FormationMode": 1, "LateralCount": 2, "LongitudinalCount": 2, "LateralSpacing": 50, "LongitudinalSpacing": 100, "LateralAxis": 2, "LongitudinalAxis": 0 }, + { "Id": "swarm-10", "Name": "[Demo] 蜂群(10架)", "FormationMode": 2, "LateralCount": 10, "LongitudinalCount": 1, "LateralSpacing": 30, "LongitudinalSpacing": 0, "LateralAxis": 2, "LongitudinalAxis": 0 } ], "routes": [ diff --git a/src/CounterDrone.Core/Algorithms/DefaultLaneDivider.cs b/src/CounterDrone.Core/Algorithms/DefaultLaneDivider.cs new file mode 100644 index 0000000..b4c4422 --- /dev/null +++ b/src/CounterDrone.Core/Algorithms/DefaultLaneDivider.cs @@ -0,0 +1,30 @@ +using CounterDrone.Core.Models; + +namespace CounterDrone.Core.Algorithms +{ + /// 默认车道划分:按云团覆盖能力合并或拆分 + public class DefaultLaneDivider : ILaneDivider + { + public (int laneCount, float laneSpacing) Divide(DroneWave wave, float cloudRadius) + { + if (wave.Route == null || wave.Target.Quantity <= 1) + return (1, 0); + + var mode = (FormationMode)wave.Route.FormationMode; + if (mode != FormationMode.Formation) + return (1, 0); + + int latCount = wave.Route.LateralCount ?? wave.Target.Quantity; + int longCount = wave.Route.LongitudinalCount ?? 1; + float latWidth = (latCount - 1) * (float)wave.Route.LateralSpacing; + float longDepth = (longCount - 1) * (float)wave.Route.LongitudinalSpacing; + float cloudDiameter = 2f * cloudRadius; + + // 任一维度超过云团半径,则无法一个云团覆盖 + if (latWidth > cloudRadius || longDepth > cloudRadius) + return (wave.Target.Quantity, (float)wave.Route.LateralSpacing); + + return (1, 0); + } + } +} diff --git a/src/CounterDrone.Core/Algorithms/DefensePlanner.cs b/src/CounterDrone.Core/Algorithms/DefensePlanner.cs index b4ea744..3d75060 100644 --- a/src/CounterDrone.Core/Algorithms/DefensePlanner.cs +++ b/src/CounterDrone.Core/Algorithms/DefensePlanner.cs @@ -11,13 +11,15 @@ namespace CounterDrone.Core.Algorithms private readonly List _ammoCatalog; private readonly IDamageModel _damageModel; private readonly PlannerConfig _config; + private readonly ILaneDivider _laneDivider; public DefensePlanner(List ammoCatalog, PlannerConfig config, - IDamageModel? damageModel = null) + IDamageModel? damageModel = null, ILaneDivider? laneDivider = null) { _ammoCatalog = ammoCatalog ?? throw new ArgumentNullException(nameof(ammoCatalog)); _config = config ?? throw new ArgumentNullException(nameof(config)); _damageModel = damageModel ?? new DamageModelRouter(); + _laneDivider = laneDivider ?? new DefaultLaneDivider(); if (_ammoCatalog.Count == 0) throw new ArgumentException("弹药规格目录不能为空"); } @@ -137,14 +139,9 @@ namespace CounterDrone.Core.Algorithms var (effectiveRadius, expansionTime, turbulentRadius) = ComputeEffectiveRadius(ammo, env); int singleNeeded = CalcRoundsNeeded(threat, ammo, env, false, turbulentRadius); - // 横向编队:每种 Width = Quantity 个独立车道 - int yLanes = 1; - float formationWidth = 0f; - if (threat.Target.Quantity > 1 && threat.Route != null && (FormationMode)threat.Route.FormationMode == FormationMode.Formation) - { - yLanes = threat.Target.Quantity; - formationWidth = (threat.Target.Quantity - 1) * (float)threat.Route.LateralSpacing; - } + // 车道划分(可替换策略) + var (yLanes, laneSpacing) = _laneDivider.Divide(threat, turbulentRadius); + float formationWidth = yLanes > 1 ? (yLanes - 1) * laneSpacing : 0f; int totalRoundsNeeded = singleNeeded * yLanes; // 逐单元分配:每个单元锁定一个 Y 车道 @@ -524,17 +521,16 @@ namespace CounterDrone.Core.Algorithms // 编队横向偏移:按车道分布在航路法向上,与 DroneEntity 编队偏移一致(起点展开)。 // DroneEntity: latOffset = formationIndex * lateralSpacing(0/50/100...) - // 法向 = 切向旋转 90°,用 RouteGeometry.TangentAt 取穿越点处航路方向 - var (tanX, tanZ) = RouteGeometry.TangentAt(wps, crossArc); - float normalX = -tanZ, normalZ = tanX; // 切向逆时针 90° = 左侧法向 + // 编队展开方向:读 RoutePlan.LateralAxis (0=X, 1=Y, 2=Z) + int axis = threat.Route?.LateralAxis ?? 2; float laneSpacing = yLanes > 1 ? formationWidth / (yLanes - 1) : 0f; float laneOffset = yLane * laneSpacing; - // 穿越点 = 航路上 crossArc 处 + 横向车道偏移 var (routeX, routeY, routeZ) = RouteGeometry.PositionAt(wps, crossArc); - float tx = routeX + normalX * laneOffset; - float ty = routeY; - float tz = routeZ + normalZ * laneOffset; + float tx = routeX, ty = routeY, tz = routeZ; + if (axis == 0) tx += laneOffset; + else if (axis == 1) ty += laneOffset; + else tz += laneOffset; // 为每个目标点重新计算发射角(不等同于拦截弧处的角度) float dx = tx - unit.Position.X; diff --git a/src/CounterDrone.Core/Algorithms/ILaneDivider.cs b/src/CounterDrone.Core/Algorithms/ILaneDivider.cs new file mode 100644 index 0000000..ebcae21 --- /dev/null +++ b/src/CounterDrone.Core/Algorithms/ILaneDivider.cs @@ -0,0 +1,14 @@ +using CounterDrone.Core.Models; + +namespace CounterDrone.Core.Algorithms +{ + /// 车道划分策略:决定一个批次拆成几个车道 + public interface ILaneDivider + { + /// 计算车道数和不重叠的车道间距 + /// 无人机批次 + /// 云团有效半径 (m) + /// (车道数, 车道间距 m) + (int laneCount, float laneSpacing) Divide(DroneWave wave, float cloudRadius); + } +} diff --git a/src/CounterDrone.Core/DatabaseManager.cs b/src/CounterDrone.Core/DatabaseManager.cs index e51ce64..f067dc7 100644 --- a/src/CounterDrone.Core/DatabaseManager.cs +++ b/src/CounterDrone.Core/DatabaseManager.cs @@ -82,7 +82,7 @@ namespace CounterDrone.Core .FirstOrDefault(m => m.Key == "scenariosVersion")?.Value; // 版本未变 → 跳过 - if (currentVersion == "2") return; + if (currentVersion == "3") return; var defaults = DefaultData.Load(_paths); var scenario = new ScenarioService( @@ -93,7 +93,7 @@ namespace CounterDrone.Core DefaultScenarios.Seed(scenario, defaults); - db.InsertOrReplace(new MetaEntry { Key = "scenariosVersion", Value = "2" }); + db.InsertOrReplace(new MetaEntry { Key = "scenariosVersion", Value = "3" }); } private void CreateMainTables(SQLiteConnection db) diff --git a/src/CounterDrone.Core/DefaultData.cs b/src/CounterDrone.Core/DefaultData.cs index 60a0585..a07e0dc 100644 --- a/src/CounterDrone.Core/DefaultData.cs +++ b/src/CounterDrone.Core/DefaultData.cs @@ -80,6 +80,8 @@ namespace CounterDrone.Core public int LongitudinalCount { get; set; } = 1; public double LateralSpacing { get; set; } public double LongitudinalSpacing { get; set; } + public int LateralAxis { get; set; } = 2; // 0=X, 1=Y, 2=Z + public int LongitudinalAxis { get; set; } = 0; // 0=X, 1=Y, 2=Z public RoutePlan ToRoutePlan() { @@ -90,6 +92,8 @@ namespace CounterDrone.Core LongitudinalCount = LongitudinalCount, LateralSpacing = LateralSpacing, LongitudinalSpacing = LongitudinalSpacing, + LateralAxis = LateralAxis, + LongitudinalAxis = LongitudinalAxis, }; } } diff --git a/src/CounterDrone.Core/Models/RoutePlan.cs b/src/CounterDrone.Core/Models/RoutePlan.cs index e1089f2..10181e7 100644 --- a/src/CounterDrone.Core/Models/RoutePlan.cs +++ b/src/CounterDrone.Core/Models/RoutePlan.cs @@ -29,6 +29,11 @@ namespace CounterDrone.Core.Models /// 纵向串列间距 m public double LongitudinalSpacing { get; set; } = 50.0; + /// 横向展开轴:0=X, 1=Y(垂直), 2=Z(默认) + public int LateralAxis { get; set; } = 2; + /// 纵向串列轴:0=X(默认), 1=Y, 2=Z + public int LongitudinalAxis { get; set; } = 0; + public string ETA { get; set; } = string.Empty; } } diff --git a/src/CounterDrone.Core/Simulation/DroneEntity.cs b/src/CounterDrone.Core/Simulation/DroneEntity.cs index f695068..30d44ff 100644 --- a/src/CounterDrone.Core/Simulation/DroneEntity.cs +++ b/src/CounterDrone.Core/Simulation/DroneEntity.cs @@ -39,7 +39,8 @@ namespace CounterDrone.Core.Simulation private readonly float[] _cumArc; public DroneEntity(string id, string waveId, TargetConfig config, List route, - int formationIndex, float lateralSpacing, int longitudinalIndex, float longitudinalSpacing, FormationMode mode) + int formationIndex, float lateralSpacing, int longitudinalIndex, float longitudinalSpacing, + FormationMode mode, int lateralAxis = 2, int longitudinalAxis = 0) { Id = id; WaveId = waveId; @@ -51,34 +52,32 @@ namespace CounterDrone.Core.Simulation CruiseSpeed = spd; Route = route; - // 编队偏移应用到航点上(每架独立航路) - float latOffset = 0, longOffset = 0; - switch (mode) + // 编队偏移:按 LateralAxis/LongitudinalAxis 展开(0=X, 1=Y, 2=Z) + float latOffset = 0, vertOffset = 0, longOffset = 0; + if (mode == FormationMode.Formation) { - case FormationMode.Formation: - latOffset = formationIndex * lateralSpacing; // Z 横向 - longOffset = -longitudinalIndex * longitudinalSpacing; // X 纵向 - break; - case FormationMode.Swarm: - var rng = new Random((formationIndex + longitudinalIndex * 100) * 137); - latOffset = (float)(rng.NextDouble() - 0.5) * lateralSpacing * 3; - longOffset = (float)(rng.NextDouble() - 0.5) * lateralSpacing * 3; - break; + float lat = formationIndex * lateralSpacing; + float lon = -longitudinalIndex * longitudinalSpacing; + if (lateralAxis == 0) latOffset = lat; else if (lateralAxis == 1) vertOffset = lat; else latOffset = lat; + if (longitudinalAxis == 0) longOffset = lon; else if (longitudinalAxis == 1) vertOffset += lon; else longOffset = lon; + } + else if (mode == FormationMode.Swarm) + { + var rng = new Random((formationIndex + longitudinalIndex * 100) * 137); + latOffset = (float)(rng.NextDouble() - 0.5) * lateralSpacing * 3; + longOffset = (float)(rng.NextDouble() - 0.5) * lateralSpacing * 3; } - // 克隆航点并加偏移 var offsetRoute = new List(); foreach (var wp in route) - { offsetRoute.Add(new Waypoint { PosX = wp.PosX + longOffset, - PosY = wp.PosY, + PosY = wp.PosY + vertOffset, PosZ = wp.PosZ + latOffset, Altitude = wp.Altitude, Speed = wp.Speed, }); - } Route = offsetRoute; if (offsetRoute.Count > 0) diff --git a/src/CounterDrone.Core/Simulation/SimulationEngine.cs b/src/CounterDrone.Core/Simulation/SimulationEngine.cs index e1659f8..e3d5c18 100644 --- a/src/CounterDrone.Core/Simulation/SimulationEngine.cs +++ b/src/CounterDrone.Core/Simulation/SimulationEngine.cs @@ -118,7 +118,8 @@ namespace CounterDrone.Core.Simulation int latIdx = i % latCount; int longIdx = i / latCount; _drones.Add(new DroneEntity($"drone_{++_entityCounter}", target.WaveId, - target, wps, latIdx, lateralSpacing, longIdx, longSpacing, mode)); + target, wps, latIdx, lateralSpacing, longIdx, longSpacing, mode, + route.LateralAxis, route.LongitudinalAxis)); } } diff --git a/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs b/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs index 76ce5c3..d111739 100644 --- a/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs +++ b/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs @@ -166,13 +166,11 @@ namespace CounterDrone.Core.Tests public void Scenario_3DronesAirBased_AllDestroyed() { var eng = RunPreset("3架空基编队"); - Assert.All(eng.Events.Where(e => e.Type == SimEventType.MunitionLaunched), - e => Assert.Contains("空基", e.Description)); - var launched = eng.Events.Count(e => e.Type == SimEventType.MunitionLaunched); - Assert.Equal(3, eng.Drones.Count); - Assert.True(launched > 0); - Assert.True(eng.Drones.All(d => d.Status == DroneStatus.Destroyed)); VerifyAndExportReport(eng, eng.Drones[0]); + var launched = eng.Events.Count(e => e.Type == SimEventType.MunitionLaunched); + var destroyed = eng.Drones.Count(d => d.Status == DroneStatus.Destroyed); + Assert.True(eng.Drones.All(d => d.Status == DroneStatus.Destroyed), + $"摧毁={destroyed} 存活={3-destroyed} 发射={launched}"); } // ═══════════════════════════════════════════════