fix: DispersionModelTests 共享状态污染导致随机失败
Dissipates_AfterMaxDuration 直接用 Ammo() 返回的共享引用并修改 MaxDuration=2,后续 Phase2/Phase3 测试拿到被污染的数据 改为独立 new AmmunitionSpec 避免修改共享对象
This commit is contained in:
parent
2a87b77085
commit
b3dec77e92
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 73afbe638ef585a4ba07c649d294a417
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 59b41f7ef22cc3747990cebf8bdb28b3
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -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);
|
||||
|
||||
@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user