CounterDroneBackend/src/CounterDrone.Core/Simulation/SimulationEngine.cs
tian e3ea595204 refactor: SimulationEngine 内部调用 Planner,不再依赖外部传 SetFireSchedule
- Initialize 内建 BuildFireUnits/BuildDroneGroups
- 自动从 DB 取 AmmunitionSpec 创建 DefaultDefensePlanner
- SetFireSchedule 保留为手动覆盖(测试可用)
- fireSchedule 空时才自动生成
2026-06-13 09:53:08 +08:00

346 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 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 = null)
{
_scenarioService = scenarioService;
_frameStore = frameStore;
_damageModel = damageModel;
_paths = paths;
_planner = planner ?? new DefaultDefensePlanner(new List<AmmunitionSpec>());
}
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 spacing = (float)route.FormationSpacing;
for (int i = 0; i < target.Quantity; i++)
_drones.Add(new DroneEntity($"drone_{++_entityCounter}", target.GroupId,
target, wps, i, spacing, mode));
}
_platforms.Clear();
foreach (var equip in config.Equipment)
if (equip.EquipmentRole != (int)EquipmentRole.Detection)
for (int i = 0; i < equip.Quantity; i++)
_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();
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)
{
using (var ammoDb = new SQLiteConnection(_paths.GetMainDbPath()))
{
var catalog = new List<AmmunitionSpec>(ammoDb.Table<AmmunitionSpec>().ToList());
if (catalog.Count == 0)
catalog = DefaultAmmunition.GetAll();
var planner = new DefaultDefensePlanner(catalog);
var result = planner.Plan(fireUnits, threats, _scene);
SetFireSchedule(result.Best.MergedSchedule);
}
}
}
_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)
{
// 空基:下令飞往投放点(投放点 = 目标正上方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,
platform.PosX, platform.PosY, platform.PosZ,
fe.TargetX, fe.TargetY, fe.TargetZ, fe.MuzzleVelocity, _disperseHeight);
_munitions.Add(m);
OnMunitionLaunched?.Invoke(m);
frameEvents.Add(new SimEvent { Type = SimEventType.MunitionLaunched, OccurredAt = SimulationTime, 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,
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())
{
m.Update(scaledDt);
if (m.HasArrived)
{
var disp = AlgorithmFactory.Create<ICloudDispersionModel>();
disp.Initialize(_ammoSpec, _scene, new Algorithms.Vector3(m.TargetX, m.TargetY, m.TargetZ), SimulationTime);
var cloud = new CloudEntity($"cloud_{++_entityCounter}", m.AerosolType, disp, SimulationTime);
_clouds.Add(cloud);
_munitions.Remove(m);
OnCloudGenerated?.Invoke(cloud);
frameEvents.Add(new SimEvent { Type = SimEventType.CloudGenerated, OccurredAt = SimulationTime, Description = "云团生成" });
}
}
// 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)
{
if (cloud.ContainsPoint(drone.PosX, drone.PosY, drone.PosZ))
{
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} 被摧毁" });
}
}
}
}
// 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);
_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>();
int idx = 0;
foreach (var eq in config.Equipment.Where(e => e.EquipmentRole == (int)EquipmentRole.LaunchPlatform))
{
int qty = Math.Max(1, eq.Quantity);
for (int i = 0; i < qty; i++)
{
units.Add(new FireUnit
{
Id = $"fu{idx++}",
Type = (PlatformType)(eq.PlatformType ?? (int)PlatformType.GroundBased),
Position = new Algorithms.Vector3((float)eq.PositionX + i * 50, (float)eq.PositionY, (float)eq.PositionZ),
CruiseSpeed = (float)(eq.CruiseSpeed ?? 55f),
ReleaseAltitude = (float)(eq.ReleaseAltitude ?? 1000f),
MuzzleVelocity = (float)(eq.MuzzleVelocity ?? 800f),
TotalMunitions = eq.MunitionCount ?? 1,
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;
}
}
}