feat: 编队2D模型 — LateralSpacing/LongitudinalSpacing + DroneEntity 纵向偏移
- RoutePlan: FormationSpacing→LateralSpacing, 新增LateralCount/LongitudinalCount/LongitudinalSpacing - DroneEntity: 2D编队(latIdx/longIdx), 横向Y展开+纵向X落后 - DefaultFormations: 6种编队模板 - DroneEntityTests: 11个(含2D编队/2×2方队/蜂群) - Planner: yLanes=ceil(formationWidth/cloudDiam), totalNeeded=single×lanes
This commit is contained in:
parent
4706c4b8c1
commit
a9339c36e3
@ -179,14 +179,14 @@ namespace CounterDrone.Core.Algorithms
|
||||
|
||||
// 同单元所有弹药以中心点计算飞行时间,仅通过 eventIdx 错开间隔
|
||||
float centerOffset = (eventIdx + (toTake - 1) / 2f - (totalRoundsNeeded - 1) / 2f) * spacing;
|
||||
var feTemplate = GenerateFireEventsAt(threat, c.Unit, c.AmmoType, ammo, env, centerOffset, spacing, totalRoundsNeeded);
|
||||
var feTemplate = GenerateFireEventsAt(threat, c.Unit, c.AmmoType, ammo, env, centerOffset);
|
||||
float baseFireTime = feTemplate[0].FireTime;
|
||||
|
||||
var fevents = new List<FireEvent>();
|
||||
for (int ch = 0; ch < toTake; ch++)
|
||||
{
|
||||
float offset = (eventIdx - (totalRoundsNeeded - 1) / 2f) * spacing;
|
||||
var fe = GenerateFireEventsAt(threat, c.Unit, c.AmmoType, ammo, env, offset, spacing, totalRoundsNeeded);
|
||||
var fe = GenerateFireEventsAt(threat, c.Unit, c.AmmoType, ammo, env, offset);
|
||||
foreach (var e in fe)
|
||||
{
|
||||
e.FireTime = baseFireTime + salvoDelay + eventIdx * physicsInterval;
|
||||
@ -436,8 +436,7 @@ namespace CounterDrone.Core.Algorithms
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
private List<FireEvent> GenerateFireEventsAt(DroneGroup threat, FireUnit unit,
|
||||
AerosolType ammoType, AmmunitionSpec ammo, CombatScene env, float targetOffset,
|
||||
float formationY)
|
||||
AerosolType ammoType, AmmunitionSpec ammo, CombatScene env, float targetOffset)
|
||||
{
|
||||
var events = new List<FireEvent>();
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ namespace CounterDrone.Core.Simulation
|
||||
public TargetType TargetType { get; }
|
||||
public PowerType PowerType { get; }
|
||||
public float Wingspan { get; }
|
||||
public float TypicalSpeed { get; } // km/h
|
||||
public float TypicalSpeed { get; }
|
||||
public List<Waypoint> Route { get; }
|
||||
public int CurrentWaypointIndex { get; private set; }
|
||||
public float PosX { get; private set; }
|
||||
@ -20,11 +20,12 @@ namespace CounterDrone.Core.Simulation
|
||||
public float PosZ { get; private set; }
|
||||
public float Hp { get; private set; } = 1.0f;
|
||||
public DroneStatus Status { get; private set; } = DroneStatus.Flying;
|
||||
public float ExposureTime { get; set; } // 在云团中的累计暴露时间
|
||||
public float ExposureTime { get; set; }
|
||||
public float FormationOffsetX { get; set; }
|
||||
public float FormationOffsetY { get; set; }
|
||||
|
||||
public DroneEntity(string id, string groupId, TargetConfig config, List<Waypoint> route, int formationIndex, float spacing, FormationMode mode)
|
||||
public DroneEntity(string id, string groupId, TargetConfig config, List<Waypoint> route,
|
||||
int formationIndex, float lateralSpacing, int longitudinalIndex, float longitudinalSpacing, FormationMode mode)
|
||||
{
|
||||
Id = id;
|
||||
GroupId = groupId;
|
||||
@ -41,25 +42,24 @@ namespace CounterDrone.Core.Simulation
|
||||
PosZ = (float)route[0].PosZ;
|
||||
}
|
||||
|
||||
// 编队偏移
|
||||
ApplyFormationOffset(formationIndex, spacing, mode);
|
||||
ApplyFormationOffset(formationIndex, lateralSpacing, longitudinalIndex, longitudinalSpacing, mode);
|
||||
PosX += FormationOffsetX;
|
||||
PosY += FormationOffsetY;
|
||||
}
|
||||
|
||||
private void ApplyFormationOffset(int index, float spacing, FormationMode mode)
|
||||
private void ApplyFormationOffset(int latIdx, float latSpacing, int longIdx, float longSpacing, FormationMode mode)
|
||||
{
|
||||
if (index == 0) return;
|
||||
var offset = spacing * index;
|
||||
if (latIdx == 0 && longIdx == 0) return;
|
||||
switch (mode)
|
||||
{
|
||||
case FormationMode.Formation:
|
||||
FormationOffsetY = offset; // 沿 Y 轴横队排列
|
||||
FormationOffsetY = latIdx * latSpacing;
|
||||
FormationOffsetX = -longIdx * longSpacing;
|
||||
break;
|
||||
case FormationMode.Swarm:
|
||||
var rng = new Random(index * 137);
|
||||
FormationOffsetX = (float)(rng.NextDouble() - 0.5) * spacing * 3;
|
||||
FormationOffsetY = (float)(rng.NextDouble() - 0.5) * spacing * 3;
|
||||
var rng = new Random((latIdx + longIdx * 100) * 137);
|
||||
FormationOffsetX = (float)(rng.NextDouble() - 0.5) * latSpacing * 3;
|
||||
FormationOffsetY = (float)(rng.NextDouble() - 0.5) * latSpacing * 3;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -93,13 +93,11 @@ namespace CounterDrone.Core.Simulation
|
||||
return;
|
||||
}
|
||||
|
||||
// 移动方向
|
||||
var ratio = step / dist;
|
||||
PosX += dx * ratio;
|
||||
PosY += dy * ratio;
|
||||
PosZ += dz * ratio;
|
||||
|
||||
// 风偏
|
||||
(float wx, float wy, float wz) = Kinematics.WindToVector(windDir, windSpeed);
|
||||
PosX += wx * deltaTime;
|
||||
PosY += wy * deltaTime;
|
||||
|
||||
@ -92,10 +92,17 @@ namespace CounterDrone.Core.Simulation
|
||||
var wps = config.WaypointGroups.GetValueOrDefault(target.GroupId, new List<Waypoint>());
|
||||
if (route == null || wps.Count == 0) continue;
|
||||
var mode = (FormationMode)route.FormationMode;
|
||||
var spacing = (float)route.LateralSpacing;
|
||||
var lateralSpacing = (float)route.LateralSpacing;
|
||||
var longSpacing = (float)route.LongitudinalSpacing;
|
||||
int latCount = route.LateralCount ?? target.Quantity;
|
||||
int longCount = route.LongitudinalCount ?? 1;
|
||||
for (int i = 0; i < target.Quantity; i++)
|
||||
{
|
||||
int latIdx = i % latCount;
|
||||
int longIdx = i / latCount;
|
||||
_drones.Add(new DroneEntity($"drone_{++_entityCounter}", target.GroupId,
|
||||
target, wps, i, spacing, mode));
|
||||
target, wps, latIdx, lateralSpacing, longIdx, longSpacing, mode));
|
||||
}
|
||||
}
|
||||
|
||||
_platforms.Clear();
|
||||
|
||||
@ -20,105 +20,139 @@ namespace CounterDrone.Core.Tests
|
||||
};
|
||||
}
|
||||
|
||||
private List<Waypoint> CreateRoute(float fromX, float toX, float altitude, float speed)
|
||||
private List<Waypoint> CreateRoute(float fromX, float toX, float alt, float speed)
|
||||
{
|
||||
return new List<Waypoint>
|
||||
{
|
||||
new Waypoint { PosX = fromX, PosY = altitude, PosZ = 0, Altitude = altitude, Speed = speed },
|
||||
new Waypoint { PosX = toX, PosY = altitude, PosZ = 0, Altitude = altitude, Speed = speed },
|
||||
new Waypoint { PosX = fromX, PosY = alt, PosZ = 0, Altitude = alt, Speed = speed },
|
||||
new Waypoint { PosX = toX, PosY = alt, PosZ = 0, Altitude = alt, Speed = speed },
|
||||
};
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InitialPosition_IsFirstWaypoint()
|
||||
{
|
||||
var route = CreateRoute(100, 500, 300, 120);
|
||||
var drone = new DroneEntity("d1", "g1", CreateConfig(), route, 0, 50, FormationMode.Single);
|
||||
|
||||
var drone = new DroneEntity("d1", "g1", CreateConfig(),
|
||||
CreateRoute(100, 500, 300, 120), 0, 50, 0, 0, FormationMode.Single);
|
||||
Assert.Equal(100, drone.PosX, 1);
|
||||
Assert.Equal(300, drone.PosY, 1); // 高度 = waypoint.PosY
|
||||
Assert.Equal(300, drone.PosY, 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_MovesTowardTarget()
|
||||
{
|
||||
var route = CreateRoute(0, 1000, 300, 120);
|
||||
var drone = new DroneEntity("d1", "g1", CreateConfig(), route, 0, 50, FormationMode.Single);
|
||||
|
||||
var drone = new DroneEntity("d1", "g1", CreateConfig(),
|
||||
CreateRoute(0, 1000, 300, 120), 0, 50, 0, 0, FormationMode.Single);
|
||||
float prevX = drone.PosX;
|
||||
drone.Update(1f, 0, WindDirection.N);
|
||||
|
||||
Assert.True(drone.PosX > prevX, "无人机应向目标移动");
|
||||
Assert.True(drone.PosX > prevX);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ReachesTarget_StatusChanges()
|
||||
{
|
||||
var route = CreateRoute(0, 100, 300, 360); // 360 km/h = 100 m/s
|
||||
var drone = new DroneEntity("d1", "g1", CreateConfig(360), route, 0, 50, FormationMode.Single);
|
||||
|
||||
// 100m 距离, 100m/s 速度, 1s 后应到达
|
||||
var drone = new DroneEntity("d1", "g1", CreateConfig(360),
|
||||
CreateRoute(0, 100, 300, 360), 0, 50, 0, 0, FormationMode.Single);
|
||||
drone.Update(2f, 0, WindDirection.N);
|
||||
|
||||
Assert.Equal(DroneStatus.ReachedTarget, drone.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyDamage_ReducesHp()
|
||||
public void Damage_ReducesHp()
|
||||
{
|
||||
var drone = new DroneEntity("d1", "g1", CreateConfig(),
|
||||
CreateRoute(0, 100, 300, 60), 0, 50, FormationMode.Single);
|
||||
|
||||
CreateRoute(0, 100, 300, 60), 0, 50, 0, 0, FormationMode.Single);
|
||||
drone.ApplyDamage(0.4f);
|
||||
Assert.Equal(0.6f, drone.Hp, 0.01f);
|
||||
Assert.Equal(DroneStatus.Flying, drone.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyDamage_ExceedingHp_DestroysDrone()
|
||||
public void Damage_ExceedingHp_Destroys()
|
||||
{
|
||||
var drone = new DroneEntity("d1", "g1", CreateConfig(),
|
||||
CreateRoute(0, 100, 300, 60), 0, 50, FormationMode.Single);
|
||||
|
||||
CreateRoute(0, 100, 300, 60), 0, 50, 0, 0, FormationMode.Single);
|
||||
drone.ApplyDamage(1.5f);
|
||||
Assert.True(drone.Hp <= 0);
|
||||
Assert.Equal(DroneStatus.Destroyed, drone.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Formation_YOffsetsDiffer()
|
||||
public void Wind_AffectsPosition()
|
||||
{
|
||||
var drone = new DroneEntity("d1", "g1", CreateConfig(),
|
||||
CreateRoute(0, 1000, 300, 60), 0, 50, 0, 0, FormationMode.Single);
|
||||
float xBefore = drone.PosX;
|
||||
drone.Update(1f, 20f, WindDirection.E);
|
||||
Assert.True(drone.PosX > xBefore + 15f, "东风 20m/s 应增加 X");
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// 编队位置
|
||||
// ═══════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void Formation_Lateral_YOffsetsDiffer()
|
||||
{
|
||||
var route = CreateRoute(0, 100, 300, 60);
|
||||
var d0 = new DroneEntity("d0", "g1", CreateConfig(), route, 0, 50, FormationMode.Formation);
|
||||
var d1 = new DroneEntity("d1", "g1", CreateConfig(), route, 1, 50, 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 d2 = new DroneEntity("d2", "g1", CreateConfig(), route, 2, 50, 0, 0, FormationMode.Formation);
|
||||
// 横向展开:Y 递增
|
||||
Assert.Equal(0, d0.FormationOffsetY);
|
||||
Assert.Equal(50, d1.FormationOffsetY);
|
||||
Assert.Equal(100, d2.FormationOffsetY);
|
||||
}
|
||||
|
||||
Assert.NotEqual(d0.PosY, d1.PosY);
|
||||
[Fact]
|
||||
public void Formation_Longitudinal_XOffsetsDiffer()
|
||||
{
|
||||
var route = CreateRoute(0, 100, 300, 60);
|
||||
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 d2 = new DroneEntity("d2", "g1", CreateConfig(), route, 0, 0, 2, 100, FormationMode.Formation);
|
||||
// 纵向串列:X 落后递增
|
||||
Assert.Equal(0, d0.FormationOffsetX);
|
||||
Assert.Equal(-100, d1.FormationOffsetX);
|
||||
Assert.Equal(-200, d2.FormationOffsetX);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Formation_Mixed_2x2()
|
||||
{
|
||||
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); // lat=0, long=0
|
||||
var d1 = new DroneEntity("d1", "g1", CreateConfig(), route,
|
||||
1, 50, 0, 100, FormationMode.Formation); // lat=1, long=0
|
||||
var d2 = new DroneEntity("d2", "g1", CreateConfig(), route,
|
||||
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]
|
||||
public void Swarm_ProducesRandomOffsets()
|
||||
{
|
||||
var route = CreateRoute(0, 100, 300, 60);
|
||||
var d0 = new DroneEntity("d0", "g1", CreateConfig(), route, 0, 50, FormationMode.Swarm);
|
||||
var d1 = new DroneEntity("d1", "g1", CreateConfig(), route, 1, 50, FormationMode.Swarm);
|
||||
var d2 = new DroneEntity("d2", "g1", CreateConfig(), route, 2, 50, 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);
|
||||
Assert.True(d0.PosX != d1.PosX || d0.PosY != d1.PosY, "蜂群应有位置差异");
|
||||
Assert.True(d1.PosX != d2.PosX || d1.PosY != d2.PosY, "蜂群应有位置差异");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Wind_AffectsPosition()
|
||||
{
|
||||
var route = CreateRoute(0, 1000, 300, 60);
|
||||
var drone = new DroneEntity("d1", "g1", CreateConfig(), route, 0, 50, FormationMode.Single);
|
||||
|
||||
float xBefore = drone.PosX;
|
||||
drone.Update(1f, 20f, WindDirection.E); // 东风 20m/s
|
||||
|
||||
// 东风 → +X 方向偏移
|
||||
Assert.True(drone.PosX > xBefore + 15f, "东风应显著增加 X 位移");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user