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}");
}
// ═══════════════════════════════════════════════