fix: 多单元并行 + 多车道正确弹药计算
- 所有单元以同基准时间并行发射(offset=0) - 通道内 ch×ChannelInterval 独立错开 - CalcRoundsNeeded 恢复纯单机,yLanes 乘法在 Solve - Piston: 33发=11×3车道, 2/3击毁
This commit is contained in:
parent
c7792d1d4a
commit
29aa11665b
@ -162,13 +162,17 @@ namespace CounterDrone.Core.Algorithms
|
|||||||
{
|
{
|
||||||
float salvoDelay = salvo * candidates[0].Unit.Cooldown;
|
float salvoDelay = salvo * candidates[0].Unit.Cooldown;
|
||||||
|
|
||||||
// 同平台飞行时间只算一次(平台飞到投放点后所有通道齐投)
|
// 车道参数
|
||||||
var (effectiveR, _) = ComputeEffectiveRadius(threat, ammo, env);
|
var (laneEffR, _) = ComputeEffectiveRadius(threat, ammo, env);
|
||||||
float cloudDiameter = effectiveR * 2f;
|
float cloudDiameter = laneEffR * 2f;
|
||||||
float droneSpeed = (float)threat.Target.TypicalSpeed / 3.6f;
|
float droneSpeed = (float)threat.Target.TypicalSpeed / 3.6f;
|
||||||
float physicsInterval = Math.Max(candidates[0].Unit.ChannelInterval,
|
float physicsInterval = Math.Max(candidates[0].Unit.ChannelInterval,
|
||||||
cloudDiameter / droneSpeed);
|
cloudDiameter / droneSpeed);
|
||||||
float spacing = effectiveR * 1.5f;
|
float spacing = laneEffR * 1.5f;
|
||||||
|
|
||||||
|
// 所有弹药以中点偏移=0为基准计算基础发射时间
|
||||||
|
var refEvent = GenerateFireEventsAt(threat, candidates[0].Unit, neededAmmo, ammo, env, 0, 0, yLanes, formationWidth);
|
||||||
|
float globalBaseFireTime = refEvent[0].FireTime;
|
||||||
|
|
||||||
foreach (var c in candidates.OrderBy(c => c.EarliestInterceptTime)
|
foreach (var c in candidates.OrderBy(c => c.EarliestInterceptTime)
|
||||||
.ThenByDescending(c => c.KillProbability))
|
.ThenByDescending(c => c.KillProbability))
|
||||||
@ -181,21 +185,19 @@ namespace CounterDrone.Core.Algorithms
|
|||||||
int toTake = Math.Min(maxSalvo, totalRoundsNeeded - roundsCollected);
|
int toTake = Math.Min(maxSalvo, totalRoundsNeeded - roundsCollected);
|
||||||
if (toTake <= 0) continue;
|
if (toTake <= 0) continue;
|
||||||
|
|
||||||
// 同单元所有弹药以中心点计算飞行时间,仅通过 eventIdx 错开间隔
|
// 所有单元以航路中点计算相同的基准发射时间,仅通道间隔错开
|
||||||
float centerOffset = (eventIdx + (toTake - 1) / 2f - (totalRoundsNeeded - 1) / 2f) * spacing;
|
var refEvt = GenerateFireEventsAt(threat, c.Unit, c.AmmoType, ammo, env, 0, 0, yLanes, formationWidth);
|
||||||
int yLane = eventIdx % yLanes;
|
float unitBaseTime = refEvt[0].FireTime;
|
||||||
var feTemplate = GenerateFireEventsAt(threat, c.Unit, c.AmmoType, ammo, env, centerOffset, yLane, yLanes, formationWidth);
|
|
||||||
float baseFireTime = feTemplate[0].FireTime;
|
|
||||||
|
|
||||||
var fevents = new List<FireEvent>();
|
var fevents = new List<FireEvent>();
|
||||||
for (int ch = 0; ch < toTake; ch++)
|
for (int ch = 0; ch < toTake; ch++)
|
||||||
{
|
{
|
||||||
float offset = (eventIdx - (totalRoundsNeeded - 1) / 2f) * spacing;
|
float offset = (eventIdx - (totalRoundsNeeded - 1) / 2f) * spacing;
|
||||||
int yLane2 = eventIdx % yLanes;
|
int yLane = eventIdx % yLanes;
|
||||||
var fe = GenerateFireEventsAt(threat, c.Unit, c.AmmoType, ammo, env, offset, yLane2, yLanes, formationWidth);
|
var fe = GenerateFireEventsAt(threat, c.Unit, c.AmmoType, ammo, env, offset, yLane, yLanes, formationWidth);
|
||||||
foreach (var e in fe)
|
foreach (var e in fe)
|
||||||
{
|
{
|
||||||
e.FireTime = baseFireTime + salvoDelay + eventIdx * physicsInterval;
|
e.FireTime = unitBaseTime + salvoDelay + ch * c.Unit.ChannelInterval;
|
||||||
}
|
}
|
||||||
fevents.AddRange(fe);
|
fevents.AddRange(fe);
|
||||||
eventIdx++;
|
eventIdx++;
|
||||||
@ -406,17 +408,7 @@ namespace CounterDrone.Core.Algorithms
|
|||||||
var aerosolType = (AerosolType)ammo.AerosolType;
|
var aerosolType = (AerosolType)ammo.AerosolType;
|
||||||
float neededExposure = aerosolType == AerosolType.ActiveMaterial ? 2f : 6f;
|
float neededExposure = aerosolType == AerosolType.ActiveMaterial ? 2f : 6f;
|
||||||
float singleCoverage = neededExposure * avgSpeed;
|
float singleCoverage = neededExposure * avgSpeed;
|
||||||
|
float requiredCoverage = singleCoverage;
|
||||||
// 多架编队:覆盖范围需加上编队展开宽度
|
|
||||||
int quantity = Math.Max(1, threat.Target.Quantity);
|
|
||||||
float formationSpread = 0f;
|
|
||||||
if (quantity > 1 && threat.Route != null)
|
|
||||||
{
|
|
||||||
var mode = (FormationMode)threat.Route.FormationMode;
|
|
||||||
if (mode != FormationMode.Single)
|
|
||||||
formationSpread = (quantity - 1) * (float)threat.Route.LateralSpacing;
|
|
||||||
}
|
|
||||||
float requiredCoverage = singleCoverage + formationSpread;
|
|
||||||
|
|
||||||
return Math.Max(1,
|
return Math.Max(1,
|
||||||
(int)Math.Ceiling((requiredCoverage - 2f * effectiveR) / spacing) + 1);
|
(int)Math.Ceiling((requiredCoverage - 2f * effectiveR) / spacing) + 1);
|
||||||
|
|||||||
@ -95,15 +95,48 @@ namespace CounterDrone.Core.Tests
|
|||||||
// ═══════════════════════════════════════
|
// ═══════════════════════════════════════
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Ground_FireTime_VariesPerRound()
|
public void Ground_FireTime_VariesPerUnitPosition()
|
||||||
{
|
{
|
||||||
var result = Plan(new() { MakeGroundUnit("u0", 5000) }, MakeThreat(speed: 120));
|
// 两个单元不同位置 → 飞行时间不同 → 发射时间不同
|
||||||
var events = result.Best.MergedSchedule.OrderBy(e => e.FireTime).ToList();
|
var u0 = MakeGroundUnit("u0", 5000);
|
||||||
Assert.True(events.Count >= 3);
|
var u1 = MakeGroundUnit("u1", 8000);
|
||||||
// 地基弹道:目标更近的弹飞行时间更短,FireTime 更晚(更接近 recommendedTiming)
|
var threat = MakeThreat(speed: 120);
|
||||||
float first = events[0].FireTime;
|
|
||||||
float last = events[^1].FireTime;
|
var r0 = Plan(new() { u0 }, threat);
|
||||||
Assert.True(last > first, "远目标 FireTime 应 > 近目标");
|
var r1 = Plan(new() { u1 }, threat);
|
||||||
|
|
||||||
|
float t0 = r0.Best.MergedSchedule[0].FireTime;
|
||||||
|
float t1 = r1.Best.MergedSchedule[0].FireTime;
|
||||||
|
Assert.NotEqual(t0, t1); // 位置不同 → 弹道不同 → 发射时刻不同
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MultiUnit_Parallel_ShorterThanSingle()
|
||||||
|
{
|
||||||
|
var threat = MakeThreat(speed: 200);
|
||||||
|
threat.Waypoints.Clear();
|
||||||
|
threat.Waypoints.Add(new Waypoint { PosX = 0, PosY = 500, PosZ = 0, Speed = 200 });
|
||||||
|
threat.Waypoints.Add(new Waypoint { PosX = 20000, PosY = 500, PosZ = 0, Speed = 200 });
|
||||||
|
|
||||||
|
// 每个单元只有 6 通道,单机需求 11 发 → 一个不够,必须两个并射
|
||||||
|
var unitSmall = new FireUnit { Id = "u0", Type = PlatformType.GroundBased,
|
||||||
|
Position = new Vector3(5000, 0, 50), GunCount = 1, ChannelsPerGun = 6,
|
||||||
|
MuzzleVelocity = 800, TotalMunitions = 11, Cooldown = 5f,
|
||||||
|
AmmoTypes = new() { AerosolType.InertGas } };
|
||||||
|
var unitSmall2 = new FireUnit { Id = "u1", Type = PlatformType.GroundBased,
|
||||||
|
Position = new Vector3(5100, 0, 50), GunCount = 1, ChannelsPerGun = 6,
|
||||||
|
MuzzleVelocity = 800, TotalMunitions = 11, Cooldown = 5f,
|
||||||
|
AmmoTypes = new() { AerosolType.InertGas } };
|
||||||
|
|
||||||
|
var r1 = Plan(new() { unitSmall }, threat);
|
||||||
|
var r2 = Plan(new() { unitSmall, unitSmall2 }, threat);
|
||||||
|
|
||||||
|
float s1 = r1.Best.MergedSchedule.Max(e => e.FireTime) - r1.Best.MergedSchedule.Min(e => e.FireTime);
|
||||||
|
float s2 = r2.Best.MergedSchedule.Max(e => e.FireTime) - r2.Best.MergedSchedule.Min(e => e.FireTime);
|
||||||
|
|
||||||
|
// 2 单元并射应短于 1 单元
|
||||||
|
var msg = $"2单元={s2:F1}s({r2.Best.MergedSchedule.Count}发) 1单元={s1:F1}s({r1.Best.MergedSchedule.Count}发)";
|
||||||
|
Assert.True(s2 < s1, msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -146,13 +179,12 @@ namespace CounterDrone.Core.Tests
|
|||||||
var events = result.Best.MergedSchedule.OrderBy(e => e.FireTime).ToList();
|
var events = result.Best.MergedSchedule.OrderBy(e => e.FireTime).ToList();
|
||||||
Assert.True(events.Count >= 3);
|
Assert.True(events.Count >= 3);
|
||||||
|
|
||||||
// 间隔应稳定 ≈ physicsInterval(1.2s),不应因目标位置变化而波动
|
// 间隔应稳定 ≈ ChannelInterval(1s),不应因目标位置变化而波动
|
||||||
// physicsInterval = max(1, 40.38/33.33) = 1.21s
|
|
||||||
for (int i = 1; i < events.Count; i++)
|
for (int i = 1; i < events.Count; i++)
|
||||||
{
|
{
|
||||||
float gap = events[i].FireTime - events[i - 1].FireTime;
|
float gap = events[i].FireTime - events[i - 1].FireTime;
|
||||||
Assert.True(Math.Abs(gap - 1.21f) < 0.2f,
|
Assert.True(Math.Abs(gap - 1.0f) < 0.2f,
|
||||||
$"gap[{i}]={gap:F3}, expected 1.21±0.2");
|
$"gap[{i}]={gap:F3}, expected 1.0±0.2");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user