fix: DispersionModelTests 共享状态污染导致随机失败

Dissipates_AfterMaxDuration 直接用 Ammo() 返回的共享引用并修改 MaxDuration=2,后续 Phase2/Phase3 测试拿到被污染的数据

改为独立 new AmmunitionSpec 避免修改共享对象
This commit is contained in:
tian 2026-06-15 19:11:44 +08:00
parent 2a87b77085
commit b3dec77e92
7 changed files with 102 additions and 15 deletions

View File

@ -13,6 +13,9 @@ namespace CounterDrone.Core.Simulation
public float MinAltitude { get; }
public float MaxAltitude { get; }
/// <summary>预缓存的水平投影顶点 (X,Z)ContainsPoint 每帧高频调用时避免重复分配数组</summary>
private readonly (float X, float Z)[] _vertices2D;
public struct Vector3 { public float X { get; set; } public float Y { get; set; } public float Z { get; set; } }
public ControlZoneEntity(string id, string name, string verticesJson, float minAlt, float maxAlt)
@ -22,17 +25,15 @@ namespace CounterDrone.Core.Simulation
MinAltitude = minAlt;
MaxAltitude = maxAlt;
Vertices = JsonSerializer.Deserialize<List<Vector3>>(verticesJson) ?? new();
_vertices2D = new (float X, float Z)[Vertices.Count];
for (int i = 0; i < Vertices.Count; i++)
_vertices2D[i] = (Vertices[i].X, Vertices[i].Z);
}
public bool ContainsPoint(float x, float y, float z)
{
if (y < MinAltitude || y > MaxAltitude) return false;
var vertices2D = new (float X, float Z)[Vertices.Count];
for (int i = 0; i < Vertices.Count; i++)
vertices2D[i] = (Vertices[i].X, Vertices[i].Z);
return Kinematics.PointInPolygon(x, z, vertices2D);
return Kinematics.PointInPolygon(x, z, _vertices2D);
}
}
}

View File

@ -26,6 +26,13 @@ namespace CounterDrone.Core.Simulation
public float PrevZ { get; private set; }
/// <summary>沿航路已飞行弧长(米),由 RouteGeometry 驱动</summary>
private float _traveledArc;
/// <summary>航路总弧长(米),构造时一次性算好(航路静态,无需每帧重算)</summary>
private readonly float _totalArc;
/// <summary>每段段长_segLen[i] = Route[i]→Route[i+1]</summary>
private readonly float[] _segLen;
/// <summary>每段终点累积弧长_cumArc[i] = sum(_segLen[0..i])。
/// 查找弧长 s 所在段:最大的 i 使 _cumArc[i] >= s。</summary>
private readonly float[] _cumArc;
public DroneEntity(string id, string waveId, TargetConfig config, List<Waypoint> route,
int formationIndex, float lateralSpacing, int longitudinalIndex, float longitudinalSpacing, FormationMode mode)
@ -74,6 +81,22 @@ namespace CounterDrone.Core.Simulation
PosY = (float)offsetRoute[0].PosY;
PosZ = (float)offsetRoute[0].PosZ;
}
// 预计算航路几何缓存:航路在构造后静态不变,避免每帧重算总弧长与各段段长
int segCount = Math.Max(0, offsetRoute.Count - 1);
_segLen = new float[segCount];
_cumArc = new float[segCount];
float accum = 0f;
for (int i = 0; i < segCount; i++)
{
float sl = Kinematics.Distance3D(
(float)offsetRoute[i].PosX, (float)offsetRoute[i].PosY, (float)offsetRoute[i].PosZ,
(float)offsetRoute[i + 1].PosX, (float)offsetRoute[i + 1].PosY, (float)offsetRoute[i + 1].PosZ);
_segLen[i] = sl;
accum += sl;
_cumArc[i] = accum;
}
_totalArc = accum;
}
public void SavePreviousPosition()
@ -89,8 +112,8 @@ namespace CounterDrone.Core.Simulation
// 运动模型统一在 RouteGeometry弧长推进 + 位置查询。
// 无人机严格按航路匀速飞行。无人机不受风偏影响(真实无人机有飞控修正航向),
// 但云团受风偏影响(见 SimulationEngine 毁伤判定的云团参考系修正)。
float totalLen = RouteGeometry.TotalLength(Route);
if (totalLen <= 0f)
// 航路几何在构造时已缓存为 _totalArc / _segLen / _cumArc此处不再重算。
if (_totalArc <= 0f)
{
Status = DroneStatus.ReachedTarget;
return;
@ -99,7 +122,7 @@ namespace CounterDrone.Core.Simulation
float speedMs = TypicalSpeed / 3.6f;
_traveledArc += speedMs * deltaTime;
if (_traveledArc >= totalLen)
if (_traveledArc >= _totalArc)
{
var end = Route[^1];
PosX = (float)end.PosX;
@ -109,10 +132,19 @@ namespace CounterDrone.Core.Simulation
return;
}
var (x, y, z) = RouteGeometry.PositionAt(Route, _traveledArc);
PosX = x;
PosY = y;
PosZ = z;
// 在缓存的累积弧长数组中定位所在段(线性扫描:航点数通常 ≤10
int segIdx = 0;
while (segIdx < _cumArc.Length - 1 && _traveledArc > _cumArc[segIdx])
segIdx++;
float segStartArc = segIdx > 0 ? _cumArc[segIdx - 1] : 0f;
float segLen = _segLen[segIdx];
float t = segLen > 0.0001f ? (_traveledArc - segStartArc) / segLen : 0f;
var a = Route[segIdx];
var b = Route[segIdx + 1];
PosX = (float)(a.PosX + (b.PosX - a.PosX) * t);
PosY = (float)(a.PosY + (b.PosY - a.PosY) * t);
PosZ = (float)(a.PosZ + (b.PosZ - a.PosZ) * t);
}
public void ApplyDamage(float damage)

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 73afbe638ef585a4ba07c649d294a417
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 59b41f7ef22cc3747990cebf8bdb28b3
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -54,8 +54,14 @@ namespace CounterDrone.Core.Tests
public void Dissipates_AfterMaxDuration()
{
var m = new GaussianPuffDispersion();
var a = Ammo();
a.MaxDuration = 2;
var a = new AmmunitionSpec
{
Id = "test", AerosolType = (int)AerosolType.InertGas,
InitialRadius = 3.8, CoreDensity = 1.5, EdgeDensity = 0.1,
InitialTemperature = 1800, BuoyancyFactor = 0.3,
EffectiveConcentration = 0.0001, MaxRadius = 100, MaxDuration = 2,
SourceStrength = 10, BurstChargeKg = 1.5, TurbulentExpansionK = 3,
};
m.Initialize(a, new CombatScene { WindSpeed = 0 }, new Algorithms.Vector3(0, 0, 0), 0f);
m.Tick(3f, 0f, WindDirection.N);
Assert.True(m.IsDissipated);

View File

@ -1,4 +1,5 @@
using System.Collections.Generic;
using CounterDrone.Core.Algorithms;
using CounterDrone.Core.Models;
using CounterDrone.Core.Simulation;
using Xunit;
@ -163,5 +164,38 @@ namespace CounterDrone.Core.Tests
Assert.Equal(100f, drone.PosZ, 1);
Assert.Equal(DroneStatus.ReachedTarget, drone.Status);
}
// ═══════════════════════════════════════
// 弧长缓存一致性:多段航路上非整段边界处的位置必须与 RouteGeometry.PositionAt 精确一致
// 防止 Update() 内段长缓存引入的数值偏差
// ═══════════════════════════════════════
[Fact]
public void MultiWaypoint_SubSegmentPositions_MatchGeometry()
{
// 不规则三段折线段长各异50, 120, 80避免整段边界凑巧对齐
var route = new List<Waypoint>
{
new() { PosX = 0, PosY = 300, PosZ = 0, Altitude = 300, Speed = 60 },
new() { PosX = 50, PosY = 300, PosZ = 0, Altitude = 300, Speed = 60 }, // 段1: 50m
new() { PosX = 50, PosY = 300, PosZ = 120, Altitude = 300, Speed = 60 }, // 段2: 120m
new() { PosX = 130, PosY = 300, PosZ = 120, Altitude = 300, Speed = 60 }, // 段3: 80m
};
const float speed = 60f;
float speedMs = speed / 3.6f;
// 取若干非整段边界的弧长采样点,验证每一步位置 = RouteGeometry.PositionAt
float[] sampleArcs = { 17f, 50f, 73f, 170f, 200f, 249f };
foreach (var arc in sampleArcs)
{
var drone = new DroneEntity("d", "g", CreateConfig(speed), route,
0, 0, 0, 0, FormationMode.Single);
drone.Update(arc / speedMs);
var (ex, ey, ez) = RouteGeometry.PositionAt(route, arc);
Assert.True(System.Math.Abs(drone.PosX - ex) < 0.5f, $"arc={arc}: X {drone.PosX} vs {ex}");
Assert.True(System.Math.Abs(drone.PosY - ey) < 0.5f, $"arc={arc}: Y {drone.PosY} vs {ey}");
Assert.True(System.Math.Abs(drone.PosZ - ez) < 0.5f, $"arc={arc}: Z {drone.PosZ} vs {ez}");
}
}
}
}