diff --git a/src/CounterDrone.Core/Simulation/ControlZone.cs b/src/CounterDrone.Core/Simulation/ControlZone.cs index d8e4c7a..b7188cb 100644 --- a/src/CounterDrone.Core/Simulation/ControlZone.cs +++ b/src/CounterDrone.Core/Simulation/ControlZone.cs @@ -13,6 +13,9 @@ namespace CounterDrone.Core.Simulation public float MinAltitude { get; } public float MaxAltitude { get; } + /// 预缓存的水平投影顶点 (X,Z),ContainsPoint 每帧高频调用时避免重复分配数组 + 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>(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); } } } diff --git a/src/CounterDrone.Core/Simulation/DroneEntity.cs b/src/CounterDrone.Core/Simulation/DroneEntity.cs index bf1b862..de95616 100644 --- a/src/CounterDrone.Core/Simulation/DroneEntity.cs +++ b/src/CounterDrone.Core/Simulation/DroneEntity.cs @@ -26,6 +26,13 @@ namespace CounterDrone.Core.Simulation public float PrevZ { get; private set; } /// 沿航路已飞行弧长(米),由 RouteGeometry 驱动 private float _traveledArc; + /// 航路总弧长(米),构造时一次性算好(航路静态,无需每帧重算) + private readonly float _totalArc; + /// 每段段长(米),_segLen[i] = Route[i]→Route[i+1] + private readonly float[] _segLen; + /// 每段终点累积弧长(米),_cumArc[i] = sum(_segLen[0..i])。 + /// 查找弧长 s 所在段:最大的 i 使 _cumArc[i] >= s。 + private readonly float[] _cumArc; public DroneEntity(string id, string waveId, TargetConfig config, List 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) diff --git a/src/Unity/Assets/Plugins/CounterDrone.Core/CounterDrone.Core.deps.json.meta b/src/Unity/Assets/Plugins/CounterDrone.Core/CounterDrone.Core.deps.json.meta new file mode 100644 index 0000000..2fc0548 --- /dev/null +++ b/src/Unity/Assets/Plugins/CounterDrone.Core/CounterDrone.Core.deps.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 73afbe638ef585a4ba07c649d294a417 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/Unity/Assets/Plugins/CounterDrone.Core/CounterDrone.Core.dll b/src/Unity/Assets/Plugins/CounterDrone.Core/CounterDrone.Core.dll index e63c440..f05fc78 100644 Binary files a/src/Unity/Assets/Plugins/CounterDrone.Core/CounterDrone.Core.dll and b/src/Unity/Assets/Plugins/CounterDrone.Core/CounterDrone.Core.dll differ diff --git a/src/Unity/Assets/Plugins/CounterDrone.Core/CounterDrone.Core.pdb.meta b/src/Unity/Assets/Plugins/CounterDrone.Core/CounterDrone.Core.pdb.meta new file mode 100644 index 0000000..0c525d2 --- /dev/null +++ b/src/Unity/Assets/Plugins/CounterDrone.Core/CounterDrone.Core.pdb.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 59b41f7ef22cc3747990cebf8bdb28b3 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/test/unit/CounterDrone.Core.Tests/DispersionModelTests.cs b/test/unit/CounterDrone.Core.Tests/DispersionModelTests.cs index 629bbe1..526ae0c 100644 --- a/test/unit/CounterDrone.Core.Tests/DispersionModelTests.cs +++ b/test/unit/CounterDrone.Core.Tests/DispersionModelTests.cs @@ -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); diff --git a/test/unit/CounterDrone.Core.Tests/DroneEntityTests.cs b/test/unit/CounterDrone.Core.Tests/DroneEntityTests.cs index 9f19c51..f867f3a 100644 --- a/test/unit/CounterDrone.Core.Tests/DroneEntityTests.cs +++ b/test/unit/CounterDrone.Core.Tests/DroneEntityTests.cs @@ -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 + { + 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}"); + } + } } }