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);
+ }
}
}