feat: 空基平台飞行与投弹功能

- PlatformEntity: 新增状态机 Idle→FlyingToTarget→ReadyToRelease
- EquipmentDeployment: 新增 CruiseSpeed 字段(空基平台巡航速度)
- MunitionEntity: 空投弹药继承载机速度矢量,替代自由落体
- Kinematics: 新增 AirDropPosition/AirDropFallTime/DirectionVelocity 工具方法
- SimulationEngine: 空基平台先飞后投,AdjustFireTimesForAirBased 补偿飞行耗时
- SimulationRunner: 空基平台 3D 可视化(蓝色 Capsule)
- 集成测试 Scenario_AirBased_PlatformFliesAndDrops 通过(8x 加速)
- 实施文档同步进度
This commit is contained in:
tian 2026-06-12 17:38:21 +08:00
parent a8b75c00cf
commit c4d9ccc4fa
12 changed files with 370 additions and 42 deletions

View File

@ -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 6Unity 集成
### Phase 6Unity 集成
**目标**:将 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 — 交付就绪 | ✅ |
---
## 、状态图例
## 、状态图例
| 符号 | 含义 |
|------|------|

View File

@ -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;
/// <summary>空基平台巡航速度 m/s</summary>
public float CruiseSpeed { get; set; } = 55f;
/// <summary>空基平台投放高度 m</summary>
public float ReleaseAltitude { get; set; }
}
public class RecommendedDetection

View File

@ -70,6 +70,49 @@ namespace CounterDrone.Core.Algorithms
return (x, y, z);
}
/// <summary>空投弹药位置:继承载机速度 + 重力下落</summary>
/// <param name="startX,startY,startZ">投放点坐标</param>
/// <param name="carrierVelX,carrierVelY,carrierVelZ">载机速度矢量 (m/s)</param>
/// <param name="time">经过时间 (s)</param>
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);
}
/// <summary>空投降落时间:从释放高度下落到目标高度所需时间</summary>
/// <param name="releaseAlt">释放高度 (m)</param>
/// <param name="targetAlt">目标高度 (m)</param>
/// <param name="carrierVelY">载机垂直速度 (m/s),正=向上,默认 0平飞</param>
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;
}
/// <summary>两点间方向的速度矢量normalize(to - from) × speed</summary>
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);
}
/// <summary>点到矩形距离(简化判定)</summary>
public static float Distance2D(float x1, float z1, float x2, float z2)
{

View File

@ -38,6 +38,9 @@ namespace CounterDrone.Core.Models
public double Cooldown { get; set; } = 5.0;
/// <summary>空基平台巡航速度 m/s地基为 NULL</summary>
public double? CruiseSpeed { get; set; }
// 探测设备专用
public double? DetectionRadius { get; set; }
}

View File

@ -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)
{

View File

@ -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; }
/// <summary>Ready 包含:冷却就绪 + 有弹药 + 非飞行中</summary>
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()
/// <summary>下令空基平台飞往投放点</summary>
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;
}
/// <summary>地基:直接发射;空基:投弹后冷却</summary>
public void Release()
{
MunitionCount--;
CooldownRemaining = Cooldown;
State = PlatformState.Idle;
}
/// <summary>空基平台到达投放点后可投弹</summary>
public bool CanRelease => State == PlatformState.ReadyToRelease && CooldownRemaining <= 0 && MunitionCount > 0;
/// <summary>当前速度矢量(空基平台:飞行方向 × 巡航速度;地基:零矢量)</summary>
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);
}
}
}
}

View File

@ -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(); }
/// <summary>为空基平台提前发射时间,补偿飞行耗时</summary>
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<EntitySnapshot> CollectSnapshots()
{
var list = new List<EntitySnapshot>();
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;

View File

@ -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();

View File

@ -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);

View File

@ -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<string, GameObject> _entityVisuals = new();
private readonly Dictionary<string, GameObject> _clouds = new();
private readonly HashSet<string> _knownMunitions = new();
private readonly HashSet<string> _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<Renderer>().material;
mat.color = new Color(0.2f, 0.5f, 1f, 0.8f); // 蓝色半透明 = 空基平台
_entityVisuals[snap.EntityId] = go;
}
private float ParseCloudOpacity(string stateData)
{
try

View File

@ -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<Waypoint>
{
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<EquipmentDeployment>();
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);
}
}
}