- SimulationEngine: 密度<EffectiveConcentration则跳过损伤 - InertGas/ActiveMaterial/ActiveFuel 移除各自的硬编码阈值 - CloudExpansionModel新增TurbulentRadius属性 - DefaultAmmunition ActiveMaterial EffectiveConcentration 0.0002→0.0001
383 lines
19 KiB
C#
383 lines
19 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text.Json;
|
|
using CounterDrone.Core.Algorithms;
|
|
using CounterDrone.Core.Models;
|
|
using CounterDrone.Core.Services;
|
|
using SQLite;
|
|
|
|
namespace CounterDrone.Core.Simulation
|
|
{
|
|
/// <summary>仿真引擎 — 纯执行器,接收发射计划按时间表执行</summary>
|
|
public class SimulationEngine
|
|
{
|
|
private readonly IScenarioService _scenarioService;
|
|
private readonly FrameDataStore _frameStore;
|
|
private readonly IDamageModel _damageModel;
|
|
private readonly IPathProvider _paths;
|
|
|
|
public SimulationState State { get; private set; } = SimulationState.Idle;
|
|
public int FrameIndex { get; private set; }
|
|
public float SimulationTime { get; private set; }
|
|
public float TimeScale { get; set; } = 1f;
|
|
private float _tickInterval;
|
|
|
|
public IReadOnlyList<DroneEntity> Drones => _drones;
|
|
public IReadOnlyList<CloudEntity> Clouds => _clouds;
|
|
public IReadOnlyList<MunitionEntity> Munitions => _munitions;
|
|
public IReadOnlyList<SimEvent> Events => _allEvents;
|
|
|
|
public event Action<MunitionEntity> OnMunitionLaunched;
|
|
public event Action<CloudEntity> OnCloudGenerated;
|
|
public event Action<DroneEntity> OnDroneDestroyed;
|
|
public event Action<DroneEntity> OnDroneReachedTarget;
|
|
public event Action<DroneEntity, ControlZoneEntity> OnZoneIntruded;
|
|
public event Action OnSimulationEnded;
|
|
|
|
private List<DroneEntity> _drones = new();
|
|
private List<PlatformEntity> _platforms = new();
|
|
private List<MunitionEntity> _munitions = new();
|
|
private List<CloudEntity> _clouds = new();
|
|
private List<ControlZoneEntity> _zones = new();
|
|
private CombatScene _scene = new();
|
|
private float _disperseHeight;
|
|
private AmmunitionSpec _ammoSpec = new();
|
|
private int _tickRate = 20;
|
|
private readonly List<FireEvent> _fireSchedule = new();
|
|
private int _nextFireIndex;
|
|
private readonly List<SimEvent> _allEvents = new();
|
|
private System.Text.StringBuilder _hitLog = new();
|
|
private SQLiteConnection _frameDb = null!;
|
|
private string _taskId = string.Empty;
|
|
private readonly IDefensePlanner _planner;
|
|
private int _entityCounter;
|
|
|
|
public SimulationEngine(IScenarioService scenarioService, FrameDataStore frameStore,
|
|
IDamageModel damageModel, IPathProvider paths, IDefensePlanner planner)
|
|
{
|
|
_scenarioService = scenarioService;
|
|
_frameStore = frameStore;
|
|
_damageModel = damageModel;
|
|
_paths = paths;
|
|
_planner = planner;
|
|
}
|
|
|
|
public void SetFireSchedule(List<FireEvent> schedule)
|
|
{
|
|
_fireSchedule.Clear();
|
|
_fireSchedule.AddRange(schedule.OrderBy(f => f.FireTime));
|
|
_nextFireIndex = 0;
|
|
}
|
|
|
|
public void Initialize(string taskId)
|
|
{
|
|
_taskId = taskId;
|
|
var config = _scenarioService.GetTaskDetail(taskId);
|
|
if (config == null) throw new InvalidOperationException("Task not found");
|
|
|
|
_scene = config.Scene;
|
|
_disperseHeight = (float)config.Cloud.DisperseHeight;
|
|
_tickRate = config.Task.TickRate;
|
|
_tickInterval = 1.0f / _tickRate;
|
|
|
|
using (var ammoDb = new SQLiteConnection(_paths.GetMainDbPath()))
|
|
_ammoSpec = ammoDb.Table<AmmunitionSpec>()
|
|
.FirstOrDefault(a => a.AerosolType == config.Cloud.AerosolType)
|
|
?? DefaultAmmunition.GetByType((AerosolType)config.Cloud.AerosolType);
|
|
|
|
_drones.Clear();
|
|
foreach (var target in config.Targets)
|
|
{
|
|
var route = config.Routes.FirstOrDefault(r => r.GroupId == target.GroupId);
|
|
var wps = config.WaypointGroups.GetValueOrDefault(target.GroupId, new List<Waypoint>());
|
|
if (route == null || wps.Count == 0) continue;
|
|
var mode = (FormationMode)route.FormationMode;
|
|
var lateralSpacing = (float)route.LateralSpacing;
|
|
var longSpacing = (float)route.LongitudinalSpacing;
|
|
int latCount = route.LateralCount ?? target.Quantity;
|
|
int longCount = route.LongitudinalCount ?? 1;
|
|
for (int i = 0; i < target.Quantity; i++)
|
|
{
|
|
int latIdx = i % latCount;
|
|
int longIdx = i / latCount;
|
|
_drones.Add(new DroneEntity($"drone_{++_entityCounter}", target.GroupId,
|
|
target, wps, latIdx, lateralSpacing, longIdx, longSpacing, mode));
|
|
}
|
|
}
|
|
|
|
// 平台 ID 从 1 开始,确保与报告映射一致
|
|
_entityCounter = 0;
|
|
|
|
_platforms.Clear();
|
|
foreach (var equip in config.Equipment)
|
|
if (equip.EquipmentRole != (int)EquipmentRole.Detection)
|
|
for (int i = 0; i < equip.Quantity; i++)
|
|
{
|
|
int gunCount = equip.GunCount ?? 1;
|
|
int chPerGun = equip.ChannelsPerGun ?? 1;
|
|
for (int g = 0; g < gunCount; g++)
|
|
for (int ch = 0; ch < chPerGun; ch++)
|
|
_platforms.Add(new PlatformEntity($"plat_{++_entityCounter}", equip));
|
|
}
|
|
|
|
_zones.Clear();
|
|
foreach (var z in config.ControlZones)
|
|
_zones.Add(new ControlZoneEntity(z.Id, z.Name, z.VerticesJson,
|
|
(float)z.MinAltitude, (float)z.MaxAltitude));
|
|
|
|
_munitions.Clear();
|
|
_clouds.Clear();
|
|
_allEvents.Clear();
|
|
_hitLog.Clear();
|
|
_hitLog.AppendLine("droneX,droneY,cloudX,cloudY,dist,radius,inside");
|
|
FrameIndex = 0;
|
|
SimulationTime = 0;
|
|
_entityCounter = 0;
|
|
_nextFireIndex = 0;
|
|
|
|
// 自动从 Planner 生成发射计划
|
|
if (_fireSchedule.Count == 0)
|
|
{
|
|
var fireUnits = BuildFireUnits(config);
|
|
var threats = BuildDroneGroups(config);
|
|
if (fireUnits.Count > 0 && threats.Count > 0)
|
|
{
|
|
var result = _planner.Plan(fireUnits, threats, _scene);
|
|
SetFireSchedule(result.Best.MergedSchedule);
|
|
// 诊断:输出 Planner 云团预期位置
|
|
var lines = new List<string> { "idx,fireTime,tx,ty,tz,mv" };
|
|
for (int i = 0; i < _fireSchedule.Count; i++)
|
|
{
|
|
var f = _fireSchedule[i];
|
|
lines.Add($"{i},{f.FireTime:F3},{f.TargetX:F1},{f.TargetY:F1},{f.TargetZ:F1},{f.MuzzleVelocity:F0}");
|
|
}
|
|
System.IO.File.WriteAllLines(@"C:\Users\Tellme\Desktop\planner_targets.csv", lines);
|
|
}
|
|
}
|
|
|
|
// 初始化 cloud dump
|
|
System.IO.File.WriteAllText(@"C:\Users\Tellme\Desktop\cloud_actual.csv", "m.id,arrivalTime,px,py,pz,cloud.id,tx,ty,tz\n");
|
|
_frameDb = _frameStore.CreateOrOpen(_taskId);
|
|
State = SimulationState.Running;
|
|
}
|
|
|
|
public SimulationFrameResult Tick(float deltaTime)
|
|
{
|
|
if (State != SimulationState.Running)
|
|
return new SimulationFrameResult { State = State, FrameIndex = FrameIndex, SimulationTime = SimulationTime };
|
|
|
|
var scaledDt = deltaTime * TimeScale;
|
|
SimulationTime += scaledDt;
|
|
FrameIndex++;
|
|
var frameEvents = new List<SimEvent>();
|
|
|
|
// 1. 按发射计划执行
|
|
while (_nextFireIndex < _fireSchedule.Count && _fireSchedule[_nextFireIndex].FireTime <= SimulationTime)
|
|
{
|
|
var fe = _fireSchedule[_nextFireIndex++];
|
|
var platform = _platforms.Count > fe.PlatformIndex ? _platforms[fe.PlatformIndex] : null;
|
|
if (platform == null) continue;
|
|
|
|
if (platform.PlatformType == Models.PlatformType.AirBased && platform.Ready)
|
|
{
|
|
platform.CommandFlyTo(fe.TargetX, platform.ReleaseAltitude, fe.TargetZ, fe.FireTime, _disperseHeight);
|
|
}
|
|
else if (platform.Ready)
|
|
{
|
|
// 地基:直接发射
|
|
platform.Release();
|
|
var m = new MunitionEntity($"mun_{++_entityCounter}",
|
|
platform.PlatformType ?? Models.PlatformType.GroundBased,
|
|
platform.AerosolType ?? Models.AerosolType.InertGas,
|
|
platform.PosX, platform.PosY, platform.PosZ,
|
|
fe.TargetX, fe.TargetY, fe.TargetZ, fe.MuzzleVelocity, _disperseHeight,
|
|
fe.FireTime);
|
|
_munitions.Add(m);
|
|
OnMunitionLaunched?.Invoke(m);
|
|
frameEvents.Add(new SimEvent { Type = SimEventType.MunitionLaunched, OccurredAt = fe.FireTime, SourceId = platform.Id, Description = $"计划发射 {m.Id}" });
|
|
}
|
|
}
|
|
|
|
// 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,
|
|
platform.ExactReleaseTime,
|
|
cvx, cvy, cvz);
|
|
_munitions.Add(m);
|
|
OnMunitionLaunched?.Invoke(m);
|
|
frameEvents.Add(new SimEvent { Type = SimEventType.MunitionLaunched, OccurredAt = platform.ExactReleaseTime, SourceId = platform.Id, Description = $"空基投放 {m.Id}" });
|
|
}
|
|
|
|
// 2. 弹药飞行 → 云团生成
|
|
foreach (var m in _munitions.ToList())
|
|
{
|
|
m.Update(scaledDt);
|
|
if (m.HasArrived)
|
|
{
|
|
var disp = AlgorithmFactory.Create<ICloudDispersionModel>();
|
|
disp.Initialize(_ammoSpec, _scene, new Algorithms.Vector3(m.PosX, m.PosY, m.PosZ), m.ArrivalTime);
|
|
var cloud = new CloudEntity($"cloud_{++_entityCounter}", m.AerosolType, disp, m.ArrivalTime);
|
|
_clouds.Add(cloud);
|
|
_munitions.Remove(m);
|
|
OnCloudGenerated?.Invoke(cloud);
|
|
frameEvents.Add(new SimEvent { Type = SimEventType.CloudGenerated, OccurredAt = m.ArrivalTime, Description = "云团生成" });
|
|
System.IO.File.AppendAllText(@"C:\Users\Tellme\Desktop\cloud_actual.csv",
|
|
$"{m.Id},{m.ArrivalTime:F3},{m.PosX:F1},{m.PosY:F1},{m.PosZ:F1},{cloud.Id},{m.TargetX:F1},{m.TargetY:F1},{m.TargetZ:F1}\n");
|
|
}
|
|
}
|
|
|
|
// 3. 云团演化
|
|
foreach (var c in _clouds.ToList())
|
|
{
|
|
c.Tick(scaledDt, (float)_scene.WindSpeed, (WindDirection)_scene.WindDirection);
|
|
if (c.IsDissipated) _clouds.Remove(c);
|
|
}
|
|
|
|
// 4. 毁伤判定
|
|
foreach (var drone in _drones.Where(d => d.Status == DroneStatus.Flying))
|
|
{
|
|
foreach (var cloud in _clouds)
|
|
{
|
|
var dx = drone.PosX - cloud.Dispersion.Center.X;
|
|
var dy = drone.PosY - cloud.Dispersion.Center.Y;
|
|
var dz = drone.PosZ - cloud.Dispersion.Center.Z;
|
|
var dist = (float)System.Math.Sqrt(dx * dx + dy * dy + dz * dz);
|
|
bool inside = dist <= cloud.Dispersion.EffectiveRadius;
|
|
_hitLog.AppendLine($"{drone.PosX:F1},{drone.PosY:F1},{cloud.Dispersion.Center.X:F1},{cloud.Dispersion.Center.Y:F1},{dist:F1},{cloud.Dispersion.EffectiveRadius:F1},{inside}");
|
|
if (inside && cloud.Dispersion.CoreDensity >= (float)_ammoSpec.EffectiveConcentration)
|
|
{
|
|
drone.ExposureTime += scaledDt;
|
|
var dmg = _damageModel.CalculateDamage(drone.TargetType, drone.PowerType, cloud.AerosolType, cloud.Dispersion.CoreDensity, drone.ExposureTime, scaledDt);
|
|
drone.ApplyDamage(dmg);
|
|
if (drone.Status == DroneStatus.Destroyed)
|
|
{
|
|
OnDroneDestroyed?.Invoke(drone);
|
|
frameEvents.Add(new SimEvent { Type = SimEventType.DroneDestroyed, OccurredAt = SimulationTime, TargetId = drone.Id, Description = $"无人机 {drone.Id} 被摧毁" });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (_hitLog.Length > 0 && SimulationTime >= 120)
|
|
System.IO.File.WriteAllText(@"C:\Users\Tellme\Desktop\hit_log.csv", _hitLog.ToString());
|
|
|
|
// 5. 无人机移动
|
|
foreach (var drone in _drones.Where(d => d.Status == DroneStatus.Flying))
|
|
{
|
|
drone.Update(scaledDt, (float)_scene.WindSpeed, (WindDirection)_scene.WindDirection);
|
|
if (drone.Status == DroneStatus.ReachedTarget)
|
|
{
|
|
OnDroneReachedTarget?.Invoke(drone);
|
|
frameEvents.Add(new SimEvent { Type = SimEventType.DroneReachedTarget, OccurredAt = SimulationTime, SourceId = drone.Id, Description = "到达攻击目标" });
|
|
}
|
|
}
|
|
|
|
// 6. 平台冷却
|
|
foreach (var p in _platforms) p.Update(scaledDt);
|
|
|
|
// 7. 管控区域
|
|
foreach (var drone in _drones.Where(d => d.Status == DroneStatus.Flying))
|
|
{
|
|
foreach (var zone in _zones)
|
|
{
|
|
if (zone.ContainsPoint(drone.PosX, drone.PosY, drone.PosZ))
|
|
{
|
|
drone.MarkZoneIntruded();
|
|
OnZoneIntruded?.Invoke(drone, zone);
|
|
frameEvents.Add(new SimEvent { Type = SimEventType.ZoneIntruded, OccurredAt = SimulationTime, SourceId = drone.Id, TargetId = zone.Id, Description = $"侵入 {zone.Name}" });
|
|
}
|
|
}
|
|
}
|
|
|
|
// 8. 结束判定
|
|
if (_drones.All(d => d.Status != DroneStatus.Flying))
|
|
{
|
|
State = SimulationState.Completed;
|
|
OnSimulationEnded?.Invoke();
|
|
frameEvents.Add(new SimEvent { Type = SimEventType.SimulationEnd, OccurredAt = SimulationTime });
|
|
}
|
|
|
|
_allEvents.AddRange(frameEvents);
|
|
|
|
var snapshots = CollectSnapshots();
|
|
_frameStore.WriteFrame(_frameDb, _taskId, FrameIndex, SimulationTime, snapshots);
|
|
if (FrameIndex % 50 == 0) _frameDb.Commit();
|
|
|
|
return new SimulationFrameResult { EntitySnapshots = snapshots, NewEvents = frameEvents, State = State, FrameIndex = FrameIndex, SimulationTime = SimulationTime };
|
|
}
|
|
|
|
public void Pause() { if (State == SimulationState.Running) State = SimulationState.Paused; }
|
|
public void Resume() { if (State == SimulationState.Paused) State = SimulationState.Running; }
|
|
public void Stop() { State = SimulationState.Stopped; _frameDb?.Close(); }
|
|
|
|
private static List<FireUnit> BuildFireUnits(TaskFullConfig config)
|
|
{
|
|
var units = new List<FireUnit>();
|
|
foreach (var eq in config.Equipment.Where(e => e.EquipmentRole == (int)EquipmentRole.LaunchPlatform))
|
|
{
|
|
int qty = Math.Max(1, eq.Quantity);
|
|
int gunCount = eq.GunCount ?? 1;
|
|
int chPerGun = eq.ChannelsPerGun ?? 1;
|
|
int channels = gunCount * chPerGun;
|
|
for (int i = 0; i < qty; i++)
|
|
{
|
|
units.Add(new FireUnit
|
|
{
|
|
Id = $"fu{units.Count}",
|
|
Type = (PlatformType)(eq.PlatformType ?? (int)PlatformType.GroundBased),
|
|
Position = new Algorithms.Vector3((float)eq.PositionX + i * 50, (float)eq.PositionY, (float)eq.PositionZ),
|
|
GunCount = gunCount,
|
|
ChannelsPerGun = chPerGun,
|
|
ChannelInterval = (float)(eq.ChannelInterval ?? 1),
|
|
CruiseSpeed = (float)(eq.CruiseSpeed ?? 0f),
|
|
ReleaseAltitude = (float)(eq.ReleaseAltitude ?? 0f),
|
|
MuzzleVelocity = (float)(eq.MuzzleVelocity ?? 0f),
|
|
TotalMunitions = eq.MunitionCount ?? channels,
|
|
AmmoTypes = new List<AerosolType> { (AerosolType)(eq.AerosolType ?? 0) },
|
|
Cooldown = (float)eq.Cooldown,
|
|
});
|
|
}
|
|
}
|
|
return units;
|
|
}
|
|
|
|
private static List<DroneGroup> BuildDroneGroups(TaskFullConfig config)
|
|
{
|
|
var groups = new List<DroneGroup>();
|
|
foreach (var t in config.Targets)
|
|
{
|
|
var route = config.Routes.FirstOrDefault(r => r.GroupId == t.GroupId);
|
|
var wps = config.WaypointGroups.GetValueOrDefault(t.GroupId, new List<Waypoint>());
|
|
groups.Add(new DroneGroup
|
|
{
|
|
GroupId = t.GroupId,
|
|
Target = t,
|
|
Route = route ?? new RoutePlan(),
|
|
Waypoints = wps,
|
|
});
|
|
}
|
|
return groups;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|