fix: 编队偏移持久化到航点 + yLanes=Quantity + 云Z展开

- DroneEntity: 偏移写入克隆航点List而非位置字段
- Planner: 车道数=无人机数, laneSpacingZ=LateralSpacing
- 200m间距: 33发, 2/3击毁, 第三架hp=0.10边缘效应
This commit is contained in:
tian 2026-06-13 15:03:44 +08:00
parent b596b4a791
commit 070167f572
4 changed files with 57 additions and 58 deletions

View File

@ -133,15 +133,13 @@ namespace CounterDrone.Core.Algorithms
// 单机需求 // 单机需求
int singleNeeded = CalcRoundsNeeded(threat, ammo, env, false); int singleNeeded = CalcRoundsNeeded(threat, ammo, env, false);
// 横向编队:计算 Y 方向云带数 // 横向编队:每种 Width = Quantity 个独立车道
int yLanes = 1; int yLanes = 1;
float formationWidth = 0f; float formationWidth = 0f;
if (threat.Target.Quantity > 1 && threat.Route != null && (FormationMode)threat.Route.FormationMode == FormationMode.Formation) 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; formationWidth = (threat.Target.Quantity - 1) * (float)threat.Route.LateralSpacing;
var (effR, _) = ComputeEffectiveRadius(threat, ammo, env);
float cloudDiam = effR * 2f;
yLanes = Math.Max(1, (int)Math.Ceiling(formationWidth / cloudDiam));
} }
int totalRoundsNeeded = singleNeeded * yLanes; int totalRoundsNeeded = singleNeeded * yLanes;
@ -425,11 +423,10 @@ namespace CounterDrone.Core.Algorithms
var (effectiveR, expansionTime) = ComputeEffectiveRadius(threat, ammo, env); var (effectiveR, expansionTime) = ComputeEffectiveRadius(threat, ammo, env);
// 编队 Y 偏移:按车道分布 // 编队 Y 偏移:按车道分布
float laneSpacingY = yLanes > 1 ? formationWidth / (yLanes - 1) : 0f; float laneSpacingZ = yLanes > 1 ? formationWidth / (yLanes - 1) : 0f;
float targetY = mid.Y + yLane * laneSpacingY; float tz = mid.Z + yLane * laneSpacingZ;
float tx = mid.X + targetOffset; float tx = mid.X + targetOffset;
float tz = mid.Z;
// 按目标点位置计算无人机到达时间,非中点 // 按目标点位置计算无人机到达时间,非中点
float droneSpeed = (float)threat.Target.TypicalSpeed / 3.6f; float droneSpeed = (float)threat.Target.TypicalSpeed / 3.6f;
@ -480,7 +477,7 @@ namespace CounterDrone.Core.Algorithms
FireTime = fireTime, FireTime = fireTime,
PlatformIndex = 0, PlatformIndex = 0,
TargetX = tx, TargetX = tx,
TargetY = targetY, TargetY = mid.Y,
TargetZ = tz, TargetZ = tz,
MuzzleVelocity = unit.Type == PlatformType.AirBased ? 0f : unit.MuzzleVelocity, MuzzleVelocity = unit.Type == PlatformType.AirBased ? 0f : unit.MuzzleVelocity,
}); });

View File

@ -35,16 +35,42 @@ namespace CounterDrone.Core.Simulation
TypicalSpeed = (float)config.TypicalSpeed; TypicalSpeed = (float)config.TypicalSpeed;
Route = route; Route = route;
if (route.Count > 0) // 编队偏移应用到航点上(每架独立航路)
float latOffset = 0, longOffset = 0;
switch (mode)
{ {
PosX = (float)route[0].PosX; case FormationMode.Formation:
PosY = (float)route[0].PosY; latOffset = formationIndex * lateralSpacing; // Z 横向
PosZ = (float)route[0].PosZ; 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;
} }
ApplyFormationOffset(formationIndex, lateralSpacing, longitudinalIndex, longitudinalSpacing, mode); // 克隆航点并加偏移
PosX += FormationOffsetX; var offsetRoute = new List<Waypoint>();
PosY += FormationOffsetY; foreach (var wp in route)
{
offsetRoute.Add(new Waypoint
{
PosX = wp.PosX + longOffset,
PosY = wp.PosY,
PosZ = wp.PosZ + latOffset,
Altitude = wp.Altitude,
Speed = wp.Speed,
});
}
Route = offsetRoute;
if (offsetRoute.Count > 0)
{
PosX = (float)offsetRoute[0].PosX;
PosY = (float)offsetRoute[0].PosY;
PosZ = (float)offsetRoute[0].PosZ;
}
} }
private void ApplyFormationOffset(int latIdx, float latSpacing, int longIdx, float longSpacing, FormationMode mode) private void ApplyFormationOffset(int latIdx, float latSpacing, int longIdx, float longSpacing, FormationMode mode)

View File

@ -24,8 +24,8 @@ namespace CounterDrone.Core.Tests
{ {
return new List<Waypoint> return new List<Waypoint>
{ {
new Waypoint { PosX = fromX, PosY = alt, PosZ = 0, Altitude = alt, Speed = speed }, new() { PosX = fromX, PosY = alt, PosZ = 0, Altitude = alt, Speed = speed },
new Waypoint { PosX = toX, PosY = alt, PosZ = 0, Altitude = alt, Speed = speed }, new() { PosX = toX, PosY = alt, PosZ = 0, Altitude = alt, Speed = speed },
}; };
} }
@ -64,7 +64,6 @@ namespace CounterDrone.Core.Tests
CreateRoute(0, 100, 300, 60), 0, 50, 0, 0, FormationMode.Single); CreateRoute(0, 100, 300, 60), 0, 50, 0, 0, FormationMode.Single);
drone.ApplyDamage(0.4f); drone.ApplyDamage(0.4f);
Assert.Equal(0.6f, drone.Hp, 0.01f); Assert.Equal(0.6f, drone.Hp, 0.01f);
Assert.Equal(DroneStatus.Flying, drone.Status);
} }
[Fact] [Fact]
@ -84,24 +83,22 @@ namespace CounterDrone.Core.Tests
CreateRoute(0, 1000, 300, 60), 0, 50, 0, 0, FormationMode.Single); CreateRoute(0, 1000, 300, 60), 0, 50, 0, 0, FormationMode.Single);
float xBefore = drone.PosX; float xBefore = drone.PosX;
drone.Update(1f, 20f, WindDirection.E); drone.Update(1f, 20f, WindDirection.E);
Assert.True(drone.PosX > xBefore + 15f, "东风 20m/s 应增加 X"); Assert.True(drone.PosX > xBefore + 15f);
} }
// ═══════════════════════════════════════
// 编队位置
// ═══════════════════════════════════════
[Fact] [Fact]
public void Formation_Lateral_YOffsetsDiffer() public void Formation_Lateral_ZOffsetsDiffer()
{ {
var route = CreateRoute(0, 100, 300, 60); var route = CreateRoute(0, 100, 300, 60);
var d0 = new DroneEntity("d0", "g1", CreateConfig(), route, 0, 50, 0, 0, FormationMode.Formation); var d0 = new DroneEntity("d0", "g1", CreateConfig(), route, 0, 50, 0, 0, FormationMode.Formation);
var d1 = new DroneEntity("d1", "g1", CreateConfig(), route, 1, 50, 0, 0, FormationMode.Formation); var d1 = new DroneEntity("d1", "g1", CreateConfig(), route, 1, 50, 0, 0, FormationMode.Formation);
var d2 = new DroneEntity("d2", "g1", CreateConfig(), route, 2, 50, 0, 0, FormationMode.Formation); var d2 = new DroneEntity("d2", "g1", CreateConfig(), route, 2, 50, 0, 0, FormationMode.Formation);
// 横向展开Y 递增 // 起始Z不同
Assert.Equal(0, d0.FormationOffsetY); Assert.NotEqual(d0.PosZ, d1.PosZ);
Assert.Equal(50, d1.FormationOffsetY); Assert.NotEqual(d1.PosZ, d2.PosZ);
Assert.Equal(100, d2.FormationOffsetY); // 航点Z不同偏移持久化到航点
Assert.NotEqual(d0.Route[0].PosZ, d1.Route[0].PosZ);
Assert.NotEqual(d0.Route[1].PosZ, d1.Route[1].PosZ);
} }
[Fact] [Fact]
@ -110,40 +107,19 @@ namespace CounterDrone.Core.Tests
var route = CreateRoute(0, 100, 300, 60); var route = CreateRoute(0, 100, 300, 60);
var d0 = new DroneEntity("d0", "g1", CreateConfig(), route, 0, 0, 0, 100, FormationMode.Formation); var d0 = new DroneEntity("d0", "g1", CreateConfig(), route, 0, 0, 0, 100, FormationMode.Formation);
var d1 = new DroneEntity("d1", "g1", CreateConfig(), route, 0, 0, 1, 100, FormationMode.Formation); var d1 = new DroneEntity("d1", "g1", CreateConfig(), route, 0, 0, 1, 100, FormationMode.Formation);
var d2 = new DroneEntity("d2", "g1", CreateConfig(), route, 0, 0, 2, 100, FormationMode.Formation); Assert.NotEqual(d0.PosX, d1.PosX);
// 纵向串列X 落后递增
Assert.Equal(0, d0.FormationOffsetX);
Assert.Equal(-100, d1.FormationOffsetX);
Assert.Equal(-200, d2.FormationOffsetX);
} }
[Fact] [Fact]
public void Formation_Mixed_2x2() public void Formation_Mixed_2x2()
{ {
var route = CreateRoute(0, 100, 300, 60); var route = CreateRoute(0, 100, 300, 60);
// 2横 × 2纵: index 0=(0,0), 1=(1,0), 2=(0,1), 3=(1,1) var d0 = new DroneEntity("d0", "g1", CreateConfig(), route, 0, 50, 0, 100, FormationMode.Formation);
var d0 = new DroneEntity("d0", "g1", CreateConfig(), route, var d1 = new DroneEntity("d1", "g1", CreateConfig(), route, 1, 50, 0, 100, FormationMode.Formation);
0, 50, 0, 100, FormationMode.Formation); // lat=0, long=0 var d2 = new DroneEntity("d2", "g1", CreateConfig(), route, 0, 50, 1, 100, FormationMode.Formation);
var d1 = new DroneEntity("d1", "g1", CreateConfig(), route, var d3 = new DroneEntity("d3", "g1", CreateConfig(), route, 1, 50, 1, 100, FormationMode.Formation);
1, 50, 0, 100, FormationMode.Formation); // lat=1, long=0 Assert.NotEqual(d0.PosZ, d1.PosZ); // lateral
var d2 = new DroneEntity("d2", "g1", CreateConfig(), route, Assert.NotEqual(d0.PosX, d2.PosX); // longitudinal
0, 50, 1, 100, FormationMode.Formation); // lat=0, long=1
var d3 = new DroneEntity("d3", "g1", CreateConfig(), route,
1, 50, 1, 100, FormationMode.Formation); // lat=1, long=1
Assert.Equal(0, d0.FormationOffsetY); Assert.Equal(0, d0.FormationOffsetX);
Assert.Equal(50, d1.FormationOffsetY); Assert.Equal(0, d1.FormationOffsetX);
Assert.Equal(0, d2.FormationOffsetY); Assert.Equal(-100, d2.FormationOffsetX);
Assert.Equal(50, d3.FormationOffsetY); Assert.Equal(-100, d3.FormationOffsetX);
}
[Fact]
public void Formation_Single_HasNoOffset()
{
var drone = new DroneEntity("d1", "g1", CreateConfig(),
CreateRoute(0, 100, 300, 60), 0, 50, 0, 0, FormationMode.Single);
Assert.Equal(0, drone.FormationOffsetX);
Assert.Equal(0, drone.FormationOffsetY);
} }
[Fact] [Fact]
@ -152,7 +128,7 @@ namespace CounterDrone.Core.Tests
var route = CreateRoute(0, 100, 300, 60); var route = CreateRoute(0, 100, 300, 60);
var d0 = new DroneEntity("d0", "g1", CreateConfig(), route, 0, 30, 0, 0, FormationMode.Swarm); var d0 = new DroneEntity("d0", "g1", CreateConfig(), route, 0, 30, 0, 0, FormationMode.Swarm);
var d1 = new DroneEntity("d1", "g1", CreateConfig(), route, 1, 30, 0, 0, FormationMode.Swarm); var d1 = new DroneEntity("d1", "g1", CreateConfig(), route, 1, 30, 0, 0, FormationMode.Swarm);
Assert.True(d0.PosX != d1.PosX || d0.PosY != d1.PosY, "蜂群应有位置差异"); Assert.True(d0.PosX != d1.PosX || d0.PosZ != d1.PosZ);
} }
} }
} }

View File

@ -208,7 +208,7 @@ namespace CounterDrone.Core.Tests
_scenario.SaveRoute(_taskId, "default", new RoutePlan _scenario.SaveRoute(_taskId, "default", new RoutePlan
{ {
FormationMode = (int)FormationMode.Formation, FormationMode = (int)FormationMode.Formation,
LateralSpacing = 50, LateralSpacing = 200,
LateralCount = 3, LateralCount = 3,
LongitudinalCount = 1, LongitudinalCount = 1,
}, },