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:
parent
a8b75c00cf
commit
c4d9ccc4fa
@ -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 — 交付就绪 | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## 五、状态图例
|
||||
## 六、状态图例
|
||||
|
||||
| 符号 | 含义 |
|
||||
|------|------|
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
{
|
||||
|
||||
@ -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; }
|
||||
}
|
||||
|
||||
@ -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)
|
||||
{
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
Binary file not shown.
@ -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();
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user