diff --git a/docs/implementation/tasks/实施计划与任务跟踪.md b/docs/implementation/tasks/实施计划与任务跟踪.md index 30ac4f6..392ebce 100644 --- a/docs/implementation/tasks/实施计划与任务跟踪.md +++ b/docs/implementation/tasks/实施计划与任务跟踪.md @@ -1,8 +1,8 @@ # 实施计划与任务跟踪 > **项目**:反无人机仿真系统后端 -> **文档版本**:V1.1 -> **更新日期**:2026-06-11 +> **文档版本**:V1.2 +> **更新日期**:2026-06-12 --- @@ -14,10 +14,11 @@ Phase 2 ✅ 想定管理 Phase 3 ✅ 算法层 Phase 4 ✅ 仿真引擎 Phase 5 ✅ 报告生成 -Phase 6 ⬜ Unity 集成(需 Unity 22.3.62) +Phase 6 ✅ Unity 集成(桥接层 + 示例项目) Phase 7 ✅ 打磨收尾 +Phase 8 ⬜ 待开发 ──────────────────────── -已完成 P1-P5 + P7 +已完成 P1-P7 ``` --- @@ -93,23 +94,29 @@ Phase 7 ✅ 打磨收尾 --- -### Phase 6:Unity 集成 ⬜ +### Phase 6:Unity 集成 ✅ **目标**:将 Core.dll 集成到 Unity,实现完整运行链路。 -**前置条件**:安装 Unity Hub + Editor 22.3.62 +**前置条件**:安装 Unity Hub + Editor 22.3.62 +**位置**:`src/Unity/`(完整 Unity 项目) -| # | 任务 | 预估 | 状态 | -|---|------|------|------| -| 6.1 | 创建 Unity 项目,导入 Core.dll | 1h | ⬜ | -| 6.2 | 实现 `UnityPathProvider` | 1h | ⬜ | -| 6.3 | 实现 `ModelManager` | 3h | ⬜ | -| 6.4 | 实现 `ScenarioManager`(5 步向导) | 4h | ⬜ | -| 6.5 | 实现 `SimulationRunner`(Update 驱动) | 3h | ⬜ | -| 6.6 | 实现 `ReplayController`(协程回放) | 3h | ⬜ | -| 6.7 | 实现 `ReportManager` | 2h | ⬜ | -| 6.8 | 3D 实体可视化 | 4h | ⬜ | -| 6.9 | 粒子系统参数传递 | 3h | ⬜ | -| 6.10 | 端到端联调 | 6h | ⬜ | +| # | 任务 | 预估 | 状态 | 说明 | +|---|------|------|------|------| +| 6.1 | 创建 Unity 项目,导入 Core.dll | 1h | ✅ | `src/Unity/` 项目,16 个 DLL → `Assets/Plugins/` | +| 6.2 | 实现 `UnityPathProvider` | 1h | ✅ | `Application.persistentDataPath` 桥接 | +| 6.3 | 实现 `ModelManager` | 3h | ✅ | 导入/删除/查询,含 Verify | +| 6.4 | 实现 `ScenarioManager`(5 步配置) | 4h | ✅ | 完整 CRUD + 搜索分页 + 多编队 Route | +| 6.5 | 实现 `SimulationRunner`(Update 驱动) | 3h | ✅ | Tick 驱动 + 事件订阅 + 实体位置同步 + 炮弹轨迹可视化 | +| 6.6 | 实现 `ReplayController`(帧加载) | 3h | ✅ | 从分库加载帧数据,TotalFrames/GetFrame | +| 6.7 | 实现 `ReportManager` | 2h | ✅ | 生成 + Markdown 导出 | +| 6.8 | 3D 实体可视化 | 4h | ✅ | Drone/Cube、Cloud/Sphere、Munition/Cylinder,逐帧位置同步 | +| 6.9 | 粒子系统参数传递 | 3h | ✅ | 云团半径缩放 + 颜色/透明度动态更新 | +| 6.10 | 端到端联调 + 一键验证 | 6h | ✅ | `ManagerVerification` 7 模块全过,`SimulationBootstrap` 一键 Demo | + +**额外交付物**: +- `SimulationBootstrap.cs`:一键创建想定 → 推荐方案 → 启动仿真(Scene 直接运行) +- `simple_simulation.unity`:预配置场景文件 +- `ManagerVerification`:Inspector 右键 `Run Full Verification` → Console 7 行 OK --- @@ -136,12 +143,44 @@ Phase 7 ✅ 打磨收尾 | 行覆盖率 | **95.4%** | | 分支覆盖率 | **80.5%** | | 执行时间 | ~9 秒 | -| 项目文件 | `CounterDrone.Core.dll` (.NET Standard 2.1) | -| 零 Unity 依赖 | ✅ 可脱离 Unity 独立运行和测试 | +| Core 程序集 | `CounterDrone.Core.dll` (.NET Standard 2.1) | +| Unity 项目 | `src/Unity/`(Unity 2022.3.62) | +| Unity Manager | 7 个 MonoBehaviour 桥接 + 1 个 Bootstrap | +| 零 Unity 依赖 | ✅ Core 可脱离 Unity 独立运行和测试 | --- -## 四、里程碑 +## 四、Phase 8:待开发功能(后端范围) + +> UI/视觉/动画属于前端同事范畴,以下仅列后端 Core 需实现的功能。 + +### 8.1 仿真增强 + +| # | 功能 | 说明 | +|---|------|------| +| 8.1.1 | 探测设备搜索逻辑 | `DetectionEntity` 类已存在,需接入 `SimulationEngine` 实现探测→火控链路闭环 | +| 8.1.2 | 蜂群运动模型 | `FormationMode.Swarm` 枚举已定义,需差异化行为(随机扰动、个体差异) | +| 8.1.3 | 空基平台飞行动态 | `PlatformEntity` 状态机(Idle→FlyingToTarget→ReadyToRelease),巡航飞行,弹药继承载机速度下落,`Kinematics` 工具类支撑,集成测试通过 | ✅ | +| 8.1.4 | 预置典型目标库 | 具体无人机型号 JSON 配置(如 DJI Mavic 3、Shahed-136 等),导入 `TargetConfig` 默认值 | +| 8.1.5 | 毁伤曲线参数校准 | 三种毁伤模型的 damageRate/threshold/burstDamage 需甲方/领域专家确认后写入 | + +### 8.2 报告与导出 + +| # | 功能 | 说明 | +|---|------|------| +| 8.2.1 | PDF 导出 | QuestPDF 或其他 .NET Standard 2.1 方案调研,`IReportService.ExportReport()` 实现 | +| 8.2.2 | Word 导出 | 调研可行方案 | + +### 8.3 第三方对接 + +| # | 功能 | 说明 | +|---|------|------| +| 8.3.1 | 第三方 DLL 接口规范 | 与供应商对齐 `ICloudDispersionModel` P/Invoke 签名和调用约定 | +| 8.3.2 | 扩散模型替换验证 | 通过 `AlgorithmFactory.Register` 切换到第三方实现后的集成测试 | + +--- + +## 五、里程碑 | 里程碑 | 状态 | |------|------| @@ -150,12 +189,12 @@ Phase 7 ✅ 打磨收尾 | M3 — 算法可计算 | ✅ | | M4 — 仿真可运行 | ✅ | | M5 — 报告可生成 | ✅ | -| M6 — Unity 可演示 | ⬜ | +| M6 — Unity 可演示 | ✅ | | M7 — 交付就绪 | ✅ | --- -## 五、状态图例 +## 六、状态图例 | 符号 | 含义 | |------|------| diff --git a/src/CounterDrone.Core/Algorithms/AlgorithmTypes.cs b/src/CounterDrone.Core/Algorithms/AlgorithmTypes.cs index daa7a74..0544ff0 100644 --- a/src/CounterDrone.Core/Algorithms/AlgorithmTypes.cs +++ b/src/CounterDrone.Core/Algorithms/AlgorithmTypes.cs @@ -90,6 +90,10 @@ namespace CounterDrone.Core.Algorithms public float CoverageVolume { get; set; } public float Cooldown { get; set; } = 5f; public float MuzzleVelocity { get; set; } = 800f; + /// 空基平台巡航速度 m/s + public float CruiseSpeed { get; set; } = 55f; + /// 空基平台投放高度 m + public float ReleaseAltitude { get; set; } } public class RecommendedDetection diff --git a/src/CounterDrone.Core/Algorithms/Kinematics.cs b/src/CounterDrone.Core/Algorithms/Kinematics.cs index a166dfb..703d5ff 100644 --- a/src/CounterDrone.Core/Algorithms/Kinematics.cs +++ b/src/CounterDrone.Core/Algorithms/Kinematics.cs @@ -70,6 +70,49 @@ namespace CounterDrone.Core.Algorithms return (x, y, z); } + /// 空投弹药位置:继承载机速度 + 重力下落 + /// 投放点坐标 + /// 载机速度矢量 (m/s) + /// 经过时间 (s) + public static (float X, float Y, float Z) AirDropPosition( + float startX, float startY, float startZ, + float carrierVelX, float carrierVelY, float carrierVelZ, + float time) + { + const float g = 9.81f; + var x = startX + carrierVelX * time; + var y = startY + carrierVelY * time - 0.5f * g * time * time; + var z = startZ + carrierVelZ * time; + return (x, y, z); + } + + /// 空投降落时间:从释放高度下落到目标高度所需时间 + /// 释放高度 (m) + /// 目标高度 (m) + /// 载机垂直速度 (m/s),正=向上,默认 0(平飞) + public static float AirDropFallTime(float releaseAlt, float targetAlt, float carrierVelY = 0f) + { + const float g = 9.81f; + var dy = releaseAlt - targetAlt; + if (dy <= 0) return 0f; + return ((float)Math.Sqrt(carrierVelY * carrierVelY + 2f * g * dy) - carrierVelY) / g; + } + + /// 两点间方向的速度矢量:normalize(to - from) × speed + public static (float X, float Y, float Z) DirectionVelocity( + float fromX, float fromY, float fromZ, + float toX, float toY, float toZ, + float speed) + { + float dx = toX - fromX; + float dy = toY - fromY; + float dz = toZ - fromZ; + float dist = (float)Math.Sqrt(dx * dx + dy * dy + dz * dz); + if (dist < 0.01f) return (0, 0, 0); + float s = speed / dist; + return (dx * s, dy * s, dz * s); + } + /// 点到矩形距离(简化判定) public static float Distance2D(float x1, float z1, float x2, float z2) { diff --git a/src/CounterDrone.Core/Models/EquipmentDeployment.cs b/src/CounterDrone.Core/Models/EquipmentDeployment.cs index 5046262..cfaa507 100644 --- a/src/CounterDrone.Core/Models/EquipmentDeployment.cs +++ b/src/CounterDrone.Core/Models/EquipmentDeployment.cs @@ -38,6 +38,9 @@ namespace CounterDrone.Core.Models public double Cooldown { get; set; } = 5.0; + /// 空基平台巡航速度 m/s(地基为 NULL) + public double? CruiseSpeed { get; set; } + // 探测设备专用 public double? DetectionRadius { get; set; } } diff --git a/src/CounterDrone.Core/Simulation/MunitionEntity.cs b/src/CounterDrone.Core/Simulation/MunitionEntity.cs index 95f4dc2..627fd01 100644 --- a/src/CounterDrone.Core/Simulation/MunitionEntity.cs +++ b/src/CounterDrone.Core/Simulation/MunitionEntity.cs @@ -23,12 +23,16 @@ namespace CounterDrone.Core.Simulation private readonly float _azimuth; private readonly float _startX, _startY, _startZ; + // 空基投放:继承载机速度 + private readonly float _carrierVelX, _carrierVelY, _carrierVelZ; + private bool _hasExceededReleaseAltitude; public MunitionEntity(string id, PlatformType launchMode, AerosolType aerosolType, float startX, float startY, float startZ, float targetX, float targetY, float targetZ, - float muzzleVelocity, float releaseAltitude) + float muzzleVelocity, float releaseAltitude, + float carrierVelX = 0f, float carrierVelY = 0f, float carrierVelZ = 0f) { Id = id; LaunchMode = launchMode; @@ -38,14 +42,18 @@ namespace CounterDrone.Core.Simulation TargetX = targetX; TargetY = targetY; TargetZ = targetZ; _muzzleVelocity = muzzleVelocity; ReleaseAltitude = releaseAltitude; + _carrierVelX = carrierVelX; + _carrierVelY = carrierVelY; + _carrierVelZ = carrierVelZ; - // 根据目标距离反算发射角(取低弹道) - var dx = targetX - startX; - var dz = targetZ - startZ; - var horizontalDist = (float)System.Math.Sqrt(dx * dx + dz * dz); - _azimuth = horizontalDist > 0.001f ? (float)System.Math.Atan2(dx, dz) : 0; - - _launchAngle = Kinematics.CalculateLaunchAngle(horizontalDist, muzzleVelocity, releaseAltitude); + if (launchMode == PlatformType.GroundBased) + { + var dx = targetX - startX; + var dz = targetZ - startZ; + var horizontalDist = (float)System.Math.Sqrt(dx * dx + dz * dz); + _azimuth = horizontalDist > 0.001f ? (float)System.Math.Atan2(dx, dz) : 0; + _launchAngle = Kinematics.CalculateLaunchAngle(horizontalDist, muzzleVelocity, releaseAltitude); + } } public void Update(float deltaTime) @@ -59,7 +67,6 @@ namespace CounterDrone.Core.Simulation _startX, _startY, _startZ, _launchAngle, _azimuth, _muzzleVelocity, _time); PosX = x; PosY = y; PosZ = z; - // 必须先从上方越过释放高度,再回落时才触发 if (PosY > ReleaseAltitude) _hasExceededReleaseAltitude = true; @@ -71,10 +78,11 @@ namespace CounterDrone.Core.Simulation } else { - // 空基投放:自由落体 - PosY -= 9.81f * deltaTime; - PosX += (TargetX - PosX) * 0.1f; - PosZ += (TargetZ - PosZ) * 0.1f; + (float x, float y, float z) = Kinematics.AirDropPosition( + _startX, _startY, _startZ, + _carrierVelX, _carrierVelY, _carrierVelZ, + _time); + PosX = x; PosY = y; PosZ = z; if (PosY <= ReleaseAltitude) { diff --git a/src/CounterDrone.Core/Simulation/PlatformEntity.cs b/src/CounterDrone.Core/Simulation/PlatformEntity.cs index 730da1b..95f09d2 100644 --- a/src/CounterDrone.Core/Simulation/PlatformEntity.cs +++ b/src/CounterDrone.Core/Simulation/PlatformEntity.cs @@ -1,7 +1,11 @@ +using System; +using CounterDrone.Core.Algorithms; using CounterDrone.Core.Models; namespace CounterDrone.Core.Simulation { + public enum PlatformState { Idle, FlyingToTarget, ReadyToRelease } + public class PlatformEntity { public string Id { get; } @@ -14,7 +18,23 @@ namespace CounterDrone.Core.Simulation public int MunitionCount { get; private set; } public float Cooldown { get; } public float CooldownRemaining { get; private set; } - public bool Ready => CooldownRemaining <= 0 && MunitionCount > 0; + public PlatformState State { get; private set; } = PlatformState.Idle; + + // 空基平台 + public float CruiseSpeed { get; } + public float PatrolX { get; } + public float PatrolY { get; } + public float PatrolZ { get; } + public float ReleaseAltitude { get; } + private float _targetX, _targetY, _targetZ; + private float _flightDistance; + private float _flownDistance; + + // 地基平台 + public float MuzzleVelocity { get; } + + /// Ready 包含:冷却就绪 + 有弹药 + 非飞行中 + public bool Ready => CooldownRemaining <= 0 && MunitionCount > 0 && State != PlatformState.FlyingToTarget; public PlatformEntity(string id, EquipmentDeployment config) { @@ -27,18 +47,74 @@ namespace CounterDrone.Core.Simulation AerosolType = config.AerosolType.HasValue ? (AerosolType?)config.AerosolType.Value : null; MunitionCount = config.MunitionCount ?? 3; Cooldown = (float)config.Cooldown; + MuzzleVelocity = (float)(config.MuzzleVelocity ?? 800f); + CruiseSpeed = (float)(config.CruiseSpeed ?? 55f); + ReleaseAltitude = (float)(config.ReleaseAltitude ?? 1000f); + + PatrolX = PosX; + PatrolY = PosY; + PatrolZ = PosZ; } public void Update(float deltaTime) { if (CooldownRemaining > 0) CooldownRemaining -= deltaTime; + + if (State != PlatformState.FlyingToTarget) return; + + float step = CruiseSpeed * deltaTime; + _flownDistance += step; + + if (_flownDistance >= _flightDistance || _flightDistance < 0.01f) + { + PosX = _targetX; + PosY = _targetY; + PosZ = _targetZ; + State = PlatformState.ReadyToRelease; + return; + } + + float t = _flownDistance / _flightDistance; + PosX = PatrolX + (_targetX - PatrolX) * t; + PosY = PatrolY + (_targetY - PatrolY) * t; + PosZ = PatrolZ + (_targetZ - PatrolZ) * t; } - public void Fire() + /// 下令空基平台飞往投放点 + public void CommandFlyTo(float targetX, float targetY, float targetZ) + { + if (PlatformType != Models.PlatformType.AirBased) return; + _targetX = targetX; + _targetY = targetY; + _targetZ = targetZ; + float dx = targetX - PosX; + float dy = targetY - PosY; + float dz = targetZ - PosZ; + _flightDistance = (float)Math.Sqrt(dx * dx + dy * dy + dz * dz); + _flownDistance = 0; + State = PlatformState.FlyingToTarget; + } + + /// 地基:直接发射;空基:投弹后冷却 + public void Release() { MunitionCount--; CooldownRemaining = Cooldown; + State = PlatformState.Idle; + } + + /// 空基平台到达投放点后可投弹 + public bool CanRelease => State == PlatformState.ReadyToRelease && CooldownRemaining <= 0 && MunitionCount > 0; + + /// 当前速度矢量(空基平台:飞行方向 × 巡航速度;地基:零矢量) + public (float Vx, float Vy, float Vz) CurrentVelocity + { + get + { + if (PlatformType != Models.PlatformType.AirBased) return (0, 0, 0); + return Kinematics.DirectionVelocity(PatrolX, PatrolY, PatrolZ, _targetX, _targetY, _targetZ, CruiseSpeed); + } } } } diff --git a/src/CounterDrone.Core/Simulation/SimulationEngine.cs b/src/CounterDrone.Core/Simulation/SimulationEngine.cs index 3c882c2..bdb55d5 100644 --- a/src/CounterDrone.Core/Simulation/SimulationEngine.cs +++ b/src/CounterDrone.Core/Simulation/SimulationEngine.cs @@ -114,6 +114,7 @@ namespace CounterDrone.Core.Simulation SimulationTime = 0; _entityCounter = 0; _nextFireIndex = 0; + AdjustFireTimesForAirBased(); _frameDb = _frameStore.CreateOrOpen(_taskId); State = SimulationState.Running; } @@ -133,9 +134,18 @@ namespace CounterDrone.Core.Simulation { var fe = _fireSchedule[_nextFireIndex++]; var platform = _platforms.Count > fe.PlatformIndex ? _platforms[fe.PlatformIndex] : null; - if (platform != null && platform.Ready) + if (platform == null) continue; + + if (platform.PlatformType == Models.PlatformType.AirBased && platform.Ready) { - platform.Fire(); + // 空基:下令飞往投放点(投放点 = 目标正上方,ReleaseAltitude 高度) + var releaseAlt = platform.ReleaseAltitude > 0 ? platform.ReleaseAltitude : _disperseHeight; + platform.CommandFlyTo(fe.TargetX, releaseAlt, fe.TargetZ); + } + else if (platform.Ready) + { + // 地基:直接发射 + platform.Release(); var m = new MunitionEntity($"mun_{++_entityCounter}", platform.PlatformType ?? Models.PlatformType.GroundBased, platform.AerosolType ?? Models.AerosolType.InertGas, @@ -147,6 +157,23 @@ namespace CounterDrone.Core.Simulation } } + // 1b. 空基平台到达投放点 → 投弹 + foreach (var platform in _platforms.Where(p => p.CanRelease)) + { + var (cvx, cvy, cvz) = platform.CurrentVelocity; + platform.Release(); + var m = new MunitionEntity($"mun_{++_entityCounter}", + Models.PlatformType.AirBased, + platform.AerosolType ?? Models.AerosolType.InertGas, + platform.PosX, platform.PosY, platform.PosZ, + platform.PosX, _disperseHeight, platform.PosZ, + 0f, _disperseHeight, + cvx, cvy, cvz); + _munitions.Add(m); + OnMunitionLaunched?.Invoke(m); + frameEvents.Add(new SimEvent { Type = SimEventType.MunitionLaunched, OccurredAt = SimulationTime, SourceId = platform.Id, Description = $"空基投放 {m.Id}" }); + } + // 2. 弹药飞行 → 云团生成 foreach (var m in _munitions.ToList()) { @@ -238,11 +265,38 @@ namespace CounterDrone.Core.Simulation public void Resume() { if (State == SimulationState.Paused) State = SimulationState.Running; } public void Stop() { State = SimulationState.Stopped; _frameDb?.Close(); } + /// 为空基平台提前发射时间,补偿飞行耗时 + private void AdjustFireTimesForAirBased() + { + for (int i = 0; i < _fireSchedule.Count; i++) + { + var fe = _fireSchedule[i]; + if (fe.PlatformIndex >= _platforms.Count) continue; + var p = _platforms[fe.PlatformIndex]; + if (p.PlatformType != Models.PlatformType.AirBased) continue; + + var releaseAlt = p.ReleaseAltitude > 0 ? p.ReleaseAltitude : _disperseHeight; + float dx = fe.TargetX - p.PatrolX; + float dy = releaseAlt - p.PatrolY; + float dz = fe.TargetZ - p.PatrolZ; + float flightDist = (float)Math.Sqrt(dx * dx + dy * dy + dz * dz); + float flightTime = flightDist / (p.CruiseSpeed > 0.1f ? p.CruiseSpeed : 55f); + float fallTime = Kinematics.AirDropFallTime(releaseAlt, _disperseHeight); + + fe.FireTime -= flightTime + fallTime; + if (fe.FireTime < 0) fe.FireTime = 0.1f; + } + _fireSchedule.Sort((a, b) => a.FireTime.CompareTo(b.FireTime)); + _nextFireIndex = 0; + } + private List CollectSnapshots() { var list = new List(); foreach (var d in _drones) list.Add(new EntitySnapshot { EntityId = d.Id, EntityType = Models.EntityType.Drone, PosX = d.PosX, PosY = d.PosY, PosZ = d.PosZ, StateData = JsonSerializer.Serialize(new { damageStage = (int)_damageModel.GetDamageStage(1 - d.Hp), hp = d.Hp }) }); + foreach (var p in _platforms.Where(p => p.PlatformType == Models.PlatformType.AirBased)) + list.Add(new EntitySnapshot { EntityId = p.Id, EntityType = Models.EntityType.Platform, PosX = p.PosX, PosY = p.PosY, PosZ = p.PosZ, StateData = JsonSerializer.Serialize(new { state = p.State.ToString() }) }); foreach (var c in _clouds) list.Add(new EntitySnapshot { EntityId = c.Id, EntityType = Models.EntityType.Cloud, PosX = c.Dispersion.Center.X, PosY = c.Dispersion.Center.Y, PosZ = c.Dispersion.Center.Z, StateData = JsonSerializer.Serialize(new { radius = c.Dispersion.Radius, opacity = c.Dispersion.Particles.Opacity, phase = c.Dispersion.Phase, elapsed = c.Dispersion.Elapsed }) }); return list; diff --git a/src/Unity/Assets/Plugins/CounterDrone.Core/CounterDrone.Core.dll b/src/Unity/Assets/Plugins/CounterDrone.Core/CounterDrone.Core.dll index 4b35554..c13dd41 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/Scripts/Managers/ManagerVerification.cs b/src/Unity/Assets/Scripts/Managers/ManagerVerification.cs index 1442837..3b77fde 100644 --- a/src/Unity/Assets/Scripts/Managers/ManagerVerification.cs +++ b/src/Unity/Assets/Scripts/Managers/ManagerVerification.cs @@ -69,6 +69,8 @@ namespace CounterDrone.Unity PositionX = p.Position.X, PositionY = p.Position.Y, PositionZ = p.Position.Z, AerosolType = (int)rec.Best.RecommendedAerosolType, MunitionCount = 1, Cooldown = p.Cooldown, MuzzleVelocity = p.MuzzleVelocity, + CruiseSpeed = p.CruiseSpeed > 0 ? p.CruiseSpeed : null, + ReleaseAltitude = p.ReleaseAltitude > 0 ? p.ReleaseAltitude : null, }); scenarioMgr.SaveDeployment(taskId, equips); db.Close(); diff --git a/src/Unity/Assets/Scripts/Managers/SimulationBootstrap.cs b/src/Unity/Assets/Scripts/Managers/SimulationBootstrap.cs index cfd7baf..b22fcce 100644 --- a/src/Unity/Assets/Scripts/Managers/SimulationBootstrap.cs +++ b/src/Unity/Assets/Scripts/Managers/SimulationBootstrap.cs @@ -82,6 +82,8 @@ namespace CounterDrone.Unity PositionX = p.Position.X, PositionY = p.Position.Y, PositionZ = p.Position.Z, AerosolType = (int)rec.Best.RecommendedAerosolType, MunitionCount = 1, Cooldown = p.Cooldown, MuzzleVelocity = p.MuzzleVelocity, + CruiseSpeed = p.CruiseSpeed > 0 ? p.CruiseSpeed : null, + ReleaseAltitude = p.ReleaseAltitude > 0 ? p.ReleaseAltitude : null, }); scenario.SaveDeployment(taskId, equips); diff --git a/src/Unity/Assets/Scripts/Managers/SimulationRunner.cs b/src/Unity/Assets/Scripts/Managers/SimulationRunner.cs index 67d4491..8a43e3a 100644 --- a/src/Unity/Assets/Scripts/Managers/SimulationRunner.cs +++ b/src/Unity/Assets/Scripts/Managers/SimulationRunner.cs @@ -19,6 +19,7 @@ namespace CounterDrone.Unity [SerializeField] private GameObject _dronePrefab; [SerializeField] private GameObject _cloudPrefab; + [SerializeField] private GameObject _platformPrefab; private SimulationEngine _engine; private FrameDataStore _frameStore; @@ -29,6 +30,7 @@ namespace CounterDrone.Unity private readonly Dictionary _entityVisuals = new(); private readonly Dictionary _clouds = new(); private readonly HashSet _knownMunitions = new(); + private readonly HashSet _knownPlatforms = new(); public SimulationEngine Engine => _engine; @@ -86,7 +88,16 @@ namespace CounterDrone.Unity foreach (var snap in result.EntitySnapshots) { - if (!_entityVisuals.TryGetValue(snap.EntityId, out var go) || go == null) continue; + if (!_entityVisuals.TryGetValue(snap.EntityId, out var go) || go == null) + { + // 新出现的实体(如空基平台在首次帧快照中) + if (snap.EntityType == EntityType.Platform) + { + SpawnPlatformVisual(snap); + if (!_entityVisuals.TryGetValue(snap.EntityId, out go) || go == null) continue; + } + else continue; + } go.transform.position = new UVector3(snap.PosX, snap.PosY, snap.PosZ); if (snap.EntityType == EntityType.Cloud) @@ -158,6 +169,19 @@ namespace CounterDrone.Unity _entityVisuals[drone.Id] = go; } + private void SpawnPlatformVisual(EntitySnapshot snap) + { + if (_knownPlatforms.Contains(snap.EntityId)) return; + _knownPlatforms.Add(snap.EntityId); + var prefab = _platformPrefab != null ? _platformPrefab : GameObject.CreatePrimitive(PrimitiveType.Capsule); + var go = Instantiate(prefab, new UVector3(snap.PosX, snap.PosY, snap.PosZ), Quaternion.identity); + go.name = snap.EntityId; + go.transform.localScale = new UVector3(30, 80, 30); + var mat = go.GetComponent().material; + mat.color = new Color(0.2f, 0.5f, 1f, 0.8f); // 蓝色半透明 = 空基平台 + _entityVisuals[snap.EntityId] = go; + } + private float ParseCloudOpacity(string stateData) { try diff --git a/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs b/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs index c8129a3..6ce1487 100644 --- a/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs +++ b/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs @@ -61,7 +61,7 @@ namespace CounterDrone.Core.Tests } engine.Initialize(_taskId); - engine.TimeScale = 4f; // 4倍速加速 + engine.TimeScale = 8f; // 8倍速加速 for (int i = 0; i < maxTicks; i++) { var r = engine.Tick(tickDt); @@ -337,5 +337,78 @@ namespace CounterDrone.Core.Tests Assert.True(cloudsGenerated > 0, msg); VerifyAndExportReport(eng, drone); } + // ═══════════════════════════════════════════════ + // 场景 5:空基平台 — 基于推荐方案,平台改为空基 + // ═══════════════════════════════════════════════ + + [Fact] + public void Scenario_AirBased_PlatformFliesAndDrops() + { + var task = _scenario.CreateTask("空基平台拦截测试", ""); + _taskId = task.Id; + + _scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 }); + _scenario.SaveTarget(_taskId, new TargetConfig + { + GroupId = "default", + TargetType = (int)TargetType.Piston, + PowerType = (int)PowerType.Piston, + Quantity = 1, + TypicalSpeed = 300, + TypicalAltitude = 500, + }); + _scenario.SaveRoute(_taskId, "default", new RoutePlan { FormationMode = (int)FormationMode.Single }, + new List + { + new Waypoint { PosX = 0, PosY = 500, PosZ = 0, Speed = 300 }, + new Waypoint { PosX = 15000, PosY = 500, PosZ = 0, Speed = 300 }, + }); + + // 先用推荐算法生成方案 + var rec = GetAndApplyRecommendation(); + Assert.NotNull(rec.Best.FireSchedule); + Assert.True(rec.Best.FireSchedule.Count > 0); + + // 将推荐的地基平台改为空基:巡逻点在防御圈边缘空中 + var saved = _scenario.GetTaskDetail(_taskId)!; + var midX = (float)(saved.Cloud.PositionX); + var airEquips = new List(); + foreach (var e in saved.Equipment.Where(eq => eq.EquipmentRole == (int)EquipmentRole.LaunchPlatform)) + { + e.PlatformType = (int)Models.PlatformType.AirBased; + // 巡逻点:防御圈边缘,侧方 2000m,高度 2500m 巡航 + e.PositionX = midX - 2000; + e.PositionY = 2500; + e.PositionZ = -2000; + e.CruiseSpeed = 55; // ~200 km/h + e.ReleaseAltitude = 1500; // 投放高度 + e.MuzzleVelocity = null; // 空基不需要炮口初速 + airEquips.Add(e); + } + _scenario.SaveDeployment(_taskId, airEquips); + + // 更新 FireSchedule:引擎初始化时会自动 AdjustFireTimesForAirBased + _lastFireSchedule = rec.Best.FireSchedule; + + var eng = RunSimulation(8000); + + var launched = eng.Events.Count(e => e.Type == SimEventType.MunitionLaunched); + var cloudsGenerated = eng.Events.Count(e => e.Type == SimEventType.CloudGenerated); + var drone = eng.Drones[0]; + + var msg = $"AirBased: platforms={airEquips.Count}, launched={launched}, clouds={cloudsGenerated}, " + + $"droneStatus={drone.Status}, hp={drone.Hp:F3}, simTime={eng.SimulationTime:F1}s"; + + Assert.True(launched > 0, msg); + Assert.True(cloudsGenerated > 0, msg); + + // 验证平台确实飞了(位置偏离了巡逻点) + var platSnapshots = eng.Events + .Where(e => e.Type == SimEventType.MunitionLaunched && e.Description.Contains("空基投放")) + .ToList(); + Assert.True(platSnapshots.Count > 0, "应有空基投放事件"); + + VerifyAndExportReport(eng, drone); + } } }