235 lines
9.3 KiB
C#
235 lines
9.3 KiB
C#
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text.Json;
|
|
using CounterDrone.Core;
|
|
using CounterDrone.Core.Algorithms;
|
|
using CounterDrone.Core.Models;
|
|
using CounterDrone.Core.Repository;
|
|
using CounterDrone.Core.Services;
|
|
using CounterDrone.Core.Simulation;
|
|
using UnityEngine;
|
|
using UVector3 = UnityEngine.Vector3;
|
|
using AVector3 = CounterDrone.Core.Algorithms.Vector3;
|
|
|
|
namespace CounterDrone.Unity
|
|
{
|
|
public class SimulationRunner : MonoBehaviour
|
|
{
|
|
private bool _loggedStart;
|
|
|
|
[SerializeField] private GameObject _dronePrefab;
|
|
[SerializeField] private GameObject _cloudPrefab;
|
|
|
|
private SimulationEngine _engine;
|
|
private FrameDataStore _frameStore;
|
|
private IScenarioService _scenario;
|
|
private IPathProvider _paths;
|
|
private string _taskId;
|
|
|
|
private readonly Dictionary<string, GameObject> _entityVisuals = new();
|
|
private readonly Dictionary<string, GameObject> _clouds = new();
|
|
private readonly HashSet<string> _knownMunitions = new();
|
|
|
|
public SimulationEngine Engine => _engine;
|
|
|
|
void Awake()
|
|
{
|
|
_paths = new UnityPathProvider();
|
|
var db = new DatabaseManager(_paths).OpenMainDb();
|
|
|
|
_scenario = new ScenarioService(
|
|
new SimTaskRepository(db), new CombatSceneRepository(db),
|
|
new ControlZoneRepository(db), new TargetConfigRepository(db),
|
|
new EquipmentDeploymentRepository(db), new CloudDispersalRepository(db),
|
|
new RoutePlanRepository(db), new WaypointRepository(db));
|
|
|
|
_frameStore = new FrameDataStore(_paths);
|
|
_engine = new SimulationEngine(_scenario, _frameStore, new DamageModelRouter(), _paths);
|
|
}
|
|
|
|
public void LoadAndStart(string taskId)
|
|
{
|
|
_taskId = taskId;
|
|
var detail = _scenario.GetTaskDetail(taskId);
|
|
if (detail == null) { Debug.LogError("Task not found"); return; }
|
|
|
|
var cloud = detail.Cloud;
|
|
if (cloud.RecommendedTiming > 0)
|
|
{
|
|
var db = new DatabaseManager(_paths).OpenMainDb();
|
|
var ammoCatalog = db.Table<AmmunitionSpec>().ToList();
|
|
db.Close();
|
|
var schedule = BuildFireSchedule(detail, ammoCatalog);
|
|
_engine.SetFireSchedule(schedule);
|
|
}
|
|
|
|
_engine.Initialize(taskId);
|
|
|
|
foreach (var drone in _engine.Drones)
|
|
SpawnDroneVisual(drone);
|
|
}
|
|
|
|
void Update()
|
|
{
|
|
if (_engine == null || _engine.State != SimulationState.Running) return;
|
|
|
|
if (!_loggedStart) { Debug.Log($"Ticking: drones={_engine.Drones.Count}, timescale={_engine.TimeScale}"); _loggedStart = true; }
|
|
|
|
var result = _engine.Tick(Time.deltaTime);
|
|
|
|
// 每秒输出无人机位置
|
|
if (Time.frameCount % 60 == 0 && _engine.Drones.Count > 0)
|
|
{
|
|
var d = _engine.Drones[0];
|
|
Debug.Log($"Drone: ({d.PosX:F0}, {d.PosY:F0}, {d.PosZ:F0}) Status={d.Status}");
|
|
}
|
|
|
|
foreach (var snap in result.EntitySnapshots)
|
|
{
|
|
if (!_entityVisuals.TryGetValue(snap.EntityId, out var go) || go == null) continue;
|
|
go.transform.position = new UVector3(snap.PosX, snap.PosY, snap.PosZ);
|
|
|
|
if (snap.EntityType == EntityType.Cloud)
|
|
{
|
|
float r = ParseCloudRadius(snap.StateData);
|
|
float opacity = ParseCloudOpacity(snap.StateData);
|
|
go.transform.localScale = UVector3.one * r;
|
|
var mat = go.GetComponent<Renderer>().material;
|
|
mat.color = new Color(1, 0, 0, opacity);
|
|
}
|
|
}
|
|
|
|
foreach (var cloud in _engine.Clouds)
|
|
{
|
|
if (!_clouds.ContainsKey(cloud.Id))
|
|
{
|
|
var pos = new UVector3(cloud.Dispersion.Center.X, cloud.Dispersion.Center.Y, cloud.Dispersion.Center.Z);
|
|
var prefab = _cloudPrefab != null ? _cloudPrefab : GameObject.CreatePrimitive(PrimitiveType.Sphere);
|
|
var go = Instantiate(prefab, pos, Quaternion.identity);
|
|
go.name = cloud.Id;
|
|
float r = cloud.Dispersion.Radius;
|
|
go.transform.localScale = UVector3.one * r;
|
|
var mat = go.GetComponent<Renderer>().material;
|
|
mat.color = new Color(1, 0, 0, 0.3f);
|
|
_clouds[cloud.Id] = go;
|
|
_entityVisuals[cloud.Id] = go;
|
|
}
|
|
}
|
|
|
|
// 炮弹飞行轨迹
|
|
foreach (var m in _engine.Munitions)
|
|
{
|
|
if (!_knownMunitions.Contains(m.Id))
|
|
{
|
|
var go = GameObject.CreatePrimitive(PrimitiveType.Cylinder);
|
|
go.name = m.Id;
|
|
go.transform.localScale = new UVector3(2, 10, 2);
|
|
_knownMunitions.Add(m.Id);
|
|
_entityVisuals[m.Id] = go;
|
|
}
|
|
if (_entityVisuals.TryGetValue(m.Id, out var mgo))
|
|
mgo.transform.position = new UVector3(m.PosX, m.PosY, m.PosZ);
|
|
}
|
|
|
|
// 清理已到达的炮弹(已转为云团)
|
|
foreach (var id in _knownMunitions.ToArray())
|
|
{
|
|
if (!_engine.Munitions.Any(m => m.Id == id) && _entityVisuals.TryGetValue(id, out var go))
|
|
{
|
|
Destroy(go);
|
|
_knownMunitions.Remove(id);
|
|
_entityVisuals.Remove(id);
|
|
}
|
|
}
|
|
|
|
if (result.State == SimulationState.Completed)
|
|
Debug.Log($"Simulation done. Drone: {_engine.Drones[0].Status}, HP: {_engine.Drones[0].Hp:F2}");
|
|
}
|
|
|
|
public void Stop() => _engine?.Stop();
|
|
void OnDestroy() => Stop();
|
|
|
|
private void SpawnDroneVisual(DroneEntity drone)
|
|
{
|
|
var prefab = _dronePrefab != null ? _dronePrefab : GameObject.CreatePrimitive(PrimitiveType.Cube);
|
|
var go = Instantiate(prefab, new UVector3(drone.PosX, drone.PosY, drone.PosZ), Quaternion.identity);
|
|
go.name = drone.Id;
|
|
go.transform.localScale = UVector3.one * 50f;
|
|
_entityVisuals[drone.Id] = go;
|
|
}
|
|
|
|
private float ParseCloudOpacity(string stateData)
|
|
{
|
|
try
|
|
{
|
|
using var doc = JsonDocument.Parse(stateData);
|
|
if (doc.RootElement.TryGetProperty("opacity", out var o))
|
|
return o.GetSingle();
|
|
}
|
|
catch { }
|
|
return 0.5f;
|
|
}
|
|
{
|
|
try
|
|
{
|
|
using var doc = JsonDocument.Parse(stateData);
|
|
if (doc.RootElement.TryGetProperty("radius", out var r))
|
|
return r.GetSingle();
|
|
}
|
|
catch { }
|
|
return 1f;
|
|
}
|
|
|
|
private List<FireEvent> BuildFireSchedule(TaskFullConfig detail, List<AmmunitionSpec> ammoCatalog)
|
|
{
|
|
var schedule = new List<FireEvent>();
|
|
var cloud = detail.Cloud;
|
|
if (!cloud.RecommendedTiming.HasValue || cloud.RecommendedTiming <= 0) return schedule;
|
|
|
|
var platforms = detail.Equipment
|
|
.FindAll(e => e.EquipmentRole == (int)EquipmentRole.LaunchPlatform);
|
|
if (platforms.Count == 0) return schedule;
|
|
|
|
var ammo = ammoCatalog.Find(a => a.AerosolType == cloud.AerosolType);
|
|
if (ammo == null) return schedule;
|
|
|
|
var target = detail.Targets[0];
|
|
var avgSpeed = (float)target.TypicalSpeed / 3.6f;
|
|
var neededExposure = cloud.AerosolType == (int)AerosolType.ActiveMaterial ? 2f : 6f;
|
|
var spacing = (float)ammo.InitialRadius * 1.5f;
|
|
var diameter = 2f * (float)ammo.InitialRadius;
|
|
var roundsNeeded = Mathf.Max(1, Mathf.CeilToInt((neededExposure * avgSpeed - diameter) / spacing) + 1);
|
|
|
|
var p0 = platforms[0];
|
|
var mv = (float)(p0.MuzzleVelocity ?? 800);
|
|
var releaseAlt = (float)cloud.DisperseHeight;
|
|
|
|
for (int r = 0; r < roundsNeeded; r++)
|
|
{
|
|
var offset = (r - (roundsNeeded - 1) / 2f) * spacing;
|
|
var tx = (float)cloud.PositionX + offset; var ty = (float)cloud.PositionY; var tz = (float)cloud.PositionZ;
|
|
var dx = tx - (float)p0.PositionX; var dz = tz - (float)p0.PositionZ;
|
|
var dist = Mathf.Sqrt(dx * dx + dz * dz);
|
|
|
|
var angle = Kinematics.CalculateLaunchAngle(dist, mv, releaseAlt);
|
|
var sinA = Mathf.Sin(angle);
|
|
var tUp = mv * sinA / 9.81f;
|
|
var peakH = mv * mv * sinA * sinA / (2f * 9.81f);
|
|
var dropH = Mathf.Max(0, peakH - releaseAlt);
|
|
var flightTime = tUp + Mathf.Sqrt(2f * dropH / 9.81f);
|
|
|
|
var fireTime = (float)cloud.RecommendedTiming.Value - flightTime;
|
|
if (fireTime < 0) fireTime = 0.1f;
|
|
|
|
schedule.Add(new FireEvent
|
|
{
|
|
FireTime = fireTime, PlatformIndex = r % platforms.Count,
|
|
TargetX = tx, TargetY = ty, TargetZ = tz, MuzzleVelocity = mv,
|
|
});
|
|
}
|
|
|
|
return schedule;
|
|
}
|
|
}
|
|
}
|