Assignment system overhaul to fix many bugs

master
Daniel Lovell 2024-09-29 23:18:40 -07:00
parent d5ae6f97f1
commit e15b70fafc
20 changed files with 467 additions and 165 deletions

View File

@ -33,12 +33,12 @@ public abstract class Agent : MonoBehaviour {
protected StaticConfig _staticConfig;
// Define delegates
public delegate void AgentHitEventHandler(Agent agent);
public delegate void AgentMissEventHandler(Agent agent);
public delegate void InterceptHitEventHandler(Interceptor interceptor, Threat target);
public delegate void InterceptMissEventHandler(Interceptor interceptor, Threat target);
// Define events
public event AgentHitEventHandler OnAgentHit;
public event AgentMissEventHandler OnAgentMiss;
public event InterceptHitEventHandler OnInterceptHit;
public event InterceptMissEventHandler OnInterceptMiss;
public void SetFlightPhase(FlightPhase flightPhase) {
Debug.Log(
@ -105,16 +105,24 @@ public abstract class Agent : MonoBehaviour {
}
// Mark the agent as having hit the target or been hit.
public void MarkAsHit() {
public void HandleInterceptHit() {
_isHit = true;
OnAgentHit?.Invoke(this);
if (this is Interceptor interceptor && _target is Threat threat) {
OnInterceptHit?.Invoke(interceptor, threat);
} else if (this is Threat threatAgent && _target is Interceptor interceptorTarget) {
OnInterceptHit?.Invoke(interceptorTarget, threatAgent);
}
TerminateAgent();
}
public void MarkAsMiss() {
public void HandleInterceptMiss() {
_isMiss = true;
if (_target != null) {
OnAgentMiss?.Invoke(this);
if (this is Interceptor interceptor && _target is Threat threat) {
OnInterceptMiss?.Invoke(interceptor, threat);
} else if (this is Threat threatAgent && _target is Interceptor interceptorTarget) {
OnInterceptMiss?.Invoke(interceptorTarget, threatAgent);
}
_target = null;
}
TerminateAgent();

View File

@ -1,6 +1,9 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using UnityEngine;
using System.Linq;
using System.Diagnostics.Contracts;
// The assignment class is an interface for assigning a threat to each interceptor.
public interface IAssignment {
@ -8,39 +11,30 @@ public interface IAssignment {
// The first element corresponds to the interceptor index, and the second element
// corresponds to the threat index.
public struct AssignmentItem {
public int InterceptorIndex;
public int ThreatIndex;
public Interceptor Interceptor;
public Threat Threat;
public AssignmentItem(int missileIndex, int threatIndex) {
InterceptorIndex = missileIndex;
ThreatIndex = threatIndex;
public AssignmentItem(Interceptor interceptor, Threat threat) {
Interceptor = interceptor;
Threat = threat;
}
}
// A list containing the interceptor-target assignments.
// Assign a target to each interceptor that has not been assigned a target yet.
public abstract IEnumerable<AssignmentItem> Assign(List<Agent> missiles, List<Agent> targets);
[Pure]
public abstract IEnumerable<AssignmentItem> Assign(in IReadOnlyList<Interceptor> interceptors, in IReadOnlyList<ThreatData> threatTable);
// Get the list of assignable interceptor indices.
protected static List<int> GetAssignableInterceptorIndices(List<Agent> missiles) {
List<int> assignableInterceptorIndices = new List<int>();
for (int missileIndex = 0; missileIndex < missiles.Count; missileIndex++) {
if (missiles[missileIndex].IsAssignable()) {
assignableInterceptorIndices.Add(missileIndex);
}
}
return assignableInterceptorIndices;
[Pure]
protected static List<Interceptor> GetAssignableInterceptors(in IReadOnlyList<Interceptor> interceptors) {
return interceptors.Where(interceptor => interceptor.IsAssignable()).ToList();
}
// Get the list of active target indices.
protected static List<int> GetActiveThreatIndices(List<Agent> threats) {
List<int> activeThreatIndices = new List<int>();
for (int threatIndex = 0; threatIndex < threats.Count; threatIndex++) {
if (!threats[threatIndex].IsHit()) {
activeThreatIndices.Add(threatIndex);
}
}
return activeThreatIndices;
// Get the list of active threats.
[Pure]
protected static List<ThreatData> GetActiveThreats(in IReadOnlyList<ThreatData> threats) {
return threats.Where(t => t.Status != ThreatStatus.DESTROYED).ToList();
}
}

View File

@ -1,39 +1,43 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
// The round-robin assignment class assigns missiles to the targets in a
// round-robin order.
using System.Diagnostics.Contracts;
// The round-robin assignment class assigns interceptors to the targets in a
// round-robin order using the new paradigm.
public class RoundRobinAssignment : IAssignment {
// Previous target index that was assigned.
private int prevTargetIndex = -1;
// Previous target index that was assigned.
private int prevTargetIndex = -1;
// Assign a target to each interceptor that has not been assigned a target yet.
public IEnumerable<IAssignment.AssignmentItem> Assign(List<Agent> missiles, List<Agent> targets) {
List<IAssignment.AssignmentItem> assignments = new List<IAssignment.AssignmentItem>();
List<int> assignableInterceptorIndices = IAssignment.GetAssignableInterceptorIndices(missiles);
if (assignableInterceptorIndices.Count == 0) {
return assignments;
// Assign a target to each interceptor that has not been assigned a target yet.
[Pure]
public IEnumerable<IAssignment.AssignmentItem> Assign(in IReadOnlyList<Interceptor> interceptors, in IReadOnlyList<ThreatData> targets) {
List<IAssignment.AssignmentItem> assignments = new List<IAssignment.AssignmentItem>();
// Get the list of interceptors that are available for assignment.
List<Interceptor> assignableInterceptors = IAssignment.GetAssignableInterceptors(interceptors);
if (assignableInterceptors.Count == 0) {
return assignments;
}
// Get the list of active threats that need to be addressed.
List<ThreatData> activeThreats = IAssignment.GetActiveThreats(targets);
if (activeThreats.Count == 0) {
return assignments;
}
// Perform round-robin assignment.
foreach (Interceptor interceptor in assignableInterceptors) {
// Determine the next target index in a round-robin fashion.
int nextTargetIndex = (prevTargetIndex + 1) % activeThreats.Count;
ThreatData selectedThreat = activeThreats[nextTargetIndex];
// Assign the interceptor to the selected threat.
assignments.Add(new IAssignment.AssignmentItem(interceptor, selectedThreat.Threat));
// Update the previous target index.
prevTargetIndex = nextTargetIndex;
}
return assignments;
}
List<int> activeThreatIndices = IAssignment.GetActiveThreatIndices(targets);
if (activeThreatIndices.Count == 0) {
return assignments;
}
foreach (int missileIndex in assignableInterceptorIndices) {
int nextActiveTargetIndex = activeThreatIndices.FindIndex(index => index > prevTargetIndex);
if (nextActiveTargetIndex == -1) {
nextActiveTargetIndex = 0;
}
int nextTargetIndex = activeThreatIndices[nextActiveTargetIndex];
assignments.Add(new IAssignment.AssignmentItem(missileIndex, nextTargetIndex));
prevTargetIndex = nextTargetIndex;
}
return assignments;
}
}

View File

@ -3,70 +3,64 @@ using System.Collections.Generic;
using System.Linq;
using Unity.VisualScripting;
using UnityEngine;
// The threat assignment class assigns missiles to the targets based
using System.Diagnostics.Contracts;
// The threat assignment class assigns interceptors to the targets based
// on the threat level of the targets.
public class ThreatAssignment : IAssignment {
// Assign a target to each interceptor that has not been assigned a target yet.
public IEnumerable<IAssignment.AssignmentItem> Assign(List<Agent> missiles, List<Agent> targets) {
[Pure]
public IEnumerable<IAssignment.AssignmentItem> Assign(in IReadOnlyList<Interceptor> interceptors, in IReadOnlyList<ThreatData> targets) {
List<IAssignment.AssignmentItem> assignments = new List<IAssignment.AssignmentItem>();
List<int> assignableInterceptorIndices = IAssignment.GetAssignableInterceptorIndices(missiles);
if (assignableInterceptorIndices.Count == 0) {
List<Interceptor> assignableInterceptors = IAssignment.GetAssignableInterceptors(interceptors);
if (assignableInterceptors.Count == 0) {
Debug.LogWarning("No assignable interceptors found");
return assignments;
}
List<int> activeThreatIndices = IAssignment.GetActiveThreatIndices(targets);
if (activeThreatIndices.Count == 0) {
List<ThreatData> activeThreats = IAssignment.GetActiveThreats(targets);
if (activeThreats.Count == 0) {
Debug.LogWarning("No active threats found");
return assignments;
}
Vector3 positionToDefend = Vector3.zero;
List<ThreatInfo> threatInfos =
CalculateThreatLevels(targets, activeThreatIndices, positionToDefend);
CalculateThreatLevels(activeThreats, positionToDefend);
foreach (int missileIndex in assignableInterceptorIndices) {
if (missiles[missileIndex].HasAssignedTarget())
continue;
if (threatInfos.Count == 0)
break;
// Sort ThreatInfo first by ThreatData.Status (UNASSIGNED first, then ASSIGNED)
// Within each group, order by ThreatLevel descending
threatInfos = threatInfos.OrderByDescending(t => t.ThreatData.Status == ThreatStatus.UNASSIGNED)
.ThenByDescending(t => t.ThreatLevel)
.ToList();
// Find the optimal target for this interceptor based on distance and threat
ThreatInfo optimalTarget = null;
float optimalScore = float.MinValue;
foreach (ThreatInfo threat in threatInfos) {
float distance = Vector3.Distance(missiles[missileIndex].transform.position,
targets[threat.TargetIndex].transform.position);
float score = threat.ThreatLevel / distance; // Balance threat level with proximity
if (score > optimalScore) {
optimalScore = score;
optimalTarget = threat;
var assignableInterceptorsEnumerator = assignableInterceptors.GetEnumerator();
if (assignableInterceptorsEnumerator.MoveNext()) // Move to the first element
{
foreach (ThreatInfo threatInfo in threatInfos) {
assignments.Add(new IAssignment.AssignmentItem(assignableInterceptorsEnumerator.Current, threatInfo.ThreatData.Threat));
if (!assignableInterceptorsEnumerator.MoveNext()) {
break;
}
}
if (optimalTarget != null) {
assignments.Add(new IAssignment.AssignmentItem(missileIndex, optimalTarget.TargetIndex));
threatInfos.Remove(optimalTarget);
}
}
return assignments;
}
private List<ThreatInfo> CalculateThreatLevels(List<Agent> targets, List<int> activeThreatIndices,
Vector3 missilesMeanPosition) {
private List<ThreatInfo> CalculateThreatLevels(List<ThreatData> threatTable,
Vector3 defensePosition) {
List<ThreatInfo> threatInfos = new List<ThreatInfo>();
foreach (int targetIndex in activeThreatIndices) {
Agent target = targets[targetIndex];
float distanceToMean = Vector3.Distance(target.transform.position, missilesMeanPosition);
float velocityMagnitude = target.GetVelocity().magnitude;
foreach (ThreatData threatData in threatTable) {
Threat threat = threatData.Threat;
float distanceToMean = Vector3.Distance(threat.transform.position, defensePosition);
float velocityMagnitude = threat.GetVelocity().magnitude;
// Calculate threat level based on proximity and velocity
float threatLevel = (1 / distanceToMean) * velocityMagnitude;
threatInfos.Add(new ThreatInfo(targetIndex, threatLevel));
threatInfos.Add(new ThreatInfo(threatData, threatLevel));
}
// Sort threats in descending order
@ -74,11 +68,11 @@ public class ThreatAssignment : IAssignment {
}
private class ThreatInfo {
public int TargetIndex { get; }
public ThreatData ThreatData { get; }
public float ThreatLevel { get; }
public ThreatInfo(int targetIndex, float threatLevel) {
TargetIndex = targetIndex;
public ThreatInfo(ThreatData threatData, float threatLevel) {
ThreatData = threatData;
ThreatLevel = threatLevel;
}
}

View File

@ -0,0 +1,48 @@
using System.Collections.Generic;
using UnityEngine;
[System.Serializable]
public enum ThreatStatus {
UNASSIGNED,
ASSIGNED,
DESTROYED
}
[System.Serializable]
public class ThreatData
{
public Threat Threat;
[SerializeField]
private ThreatStatus _status;
public ThreatStatus Status { get { return _status; } }
public string ThreatID;
[SerializeField]
private List<Interceptor> _assignedInterceptors; // Changed from property to field
public void AssignInterceptor(Interceptor interceptor) {
if(Status == ThreatStatus.DESTROYED) {
Debug.LogError($"AssignInterceptor: Threat {ThreatID} is destroyed, cannot assign interceptor");
return;
}
_status = ThreatStatus.ASSIGNED;
_assignedInterceptors.Add(interceptor);
}
public void RemoveInterceptor(Interceptor interceptor) {
_assignedInterceptors.Remove(interceptor);
if(_assignedInterceptors.Count == 0) {
_status = ThreatStatus.UNASSIGNED;
}
}
public void MarkDestroyed() {
_status = ThreatStatus.DESTROYED;
}
// Constructor remains the same
public ThreatData(Threat threat, string threatID)
{
Threat = threat;
_status = ThreatStatus.UNASSIGNED;
ThreatID = threatID;
_assignedInterceptors = new List<Interceptor>(); // Initialize the list
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: adc0c5dbdb9dc7d498b50cf9a15c2db5

View File

@ -74,7 +74,7 @@ public class Interceptor : Agent {
private void OnTriggerEnter(Collider other) {
if (other.gameObject.name == "Floor") {
this.MarkAsMiss();
this.HandleInterceptMiss();
}
// Check if the collision is with another Agent
Agent otherAgent = other.gameObject.GetComponentInParent<Agent>();
@ -87,13 +87,13 @@ public class Interceptor : Agent {
// Set green for hit
markerObject.GetComponent<Renderer>().material.color = new Color(0, 1, 0, 0.15f);
// Mark both this agent and the other agent as hit
this.MarkAsHit();
otherAgent.MarkAsHit();
this.HandleInterceptHit();
otherAgent.HandleInterceptHit();
} else {
// Set red for miss
markerObject.GetComponent<Renderer>().material.color = new Color(1, 0, 0, 0.15f);
this.MarkAsMiss();
this.HandleInterceptMiss();
// otherAgent.MarkAsMiss();
}
}

View File

@ -28,7 +28,7 @@ public class Micromissile : Interceptor {
// Check whether the threat should be considered a miss
SensorOutput sensorOutput = GetComponent<Sensor>().Sense(_target);
if (sensorOutput.velocity.range > 1000f) {
this.MarkAsMiss();
this.HandleInterceptMiss();
}
// Calculate the acceleration input

View File

@ -0,0 +1,16 @@
{
"name": "MicromissileAssembly",
"rootNamespace": "",
"references": [
"GUID:6055be8ebefd69e48b49212b09b47b2f"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: af8bba08a36038347823e2f46bdc9857
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -41,7 +41,7 @@ public class SimMonitor : MonoBehaviour
private void ExportTelemetry()
{
float time = (float)SimManager.Instance.GetElapsedSimulationTime();
foreach (var agent in SimManager.Instance.GetActiveThreats().Cast<Agent>().Concat(SimManager.Instance.GetActiveInterceptors().Cast<Agent>()))
foreach (var agent in SimManager.Instance.GetActiveAgents())
{
Vector3 pos = agent.transform.position;
if(pos == Vector3.zero) {

View File

@ -19,11 +19,15 @@ public class SimManager : MonoBehaviour {
[SerializeField]
public SimulationConfig simulationConfig;
private List<Interceptor> _interceptors = new List<Interceptor>();
private List<Interceptor> _activeInterceptors = new List<Interceptor>();
private List<Threat> _unassignedThreats = new List<Threat>();
private List<Threat> _threats = new List<Threat>();
private List<Threat> _activeThreats = new List<Threat>();
[SerializeField]
private List<ThreatData> _threatTable = new List<ThreatData>();
private Dictionary<Threat, ThreatData> _threatDataMap = new Dictionary<Threat, ThreatData>();
private List<Interceptor> _interceptorObjects = new List<Interceptor>();
private List<Threat> _threatObjects = new List<Threat>();
private float _elapsedSimulationTime = 0f;
private float endTime = 100f; // Set an appropriate end time
private bool simulationRunning = false;
@ -47,12 +51,12 @@ public class SimManager : MonoBehaviour {
}
public List<Threat> GetActiveThreats() {
return _activeThreats;
return _threatTable.Where(threat => threat.Status != ThreatStatus.DESTROYED).Select(threat => threat.Threat).ToList();
}
public List<Agent> GetActiveAgents() {
return _activeInterceptors.ConvertAll(interceptor => interceptor as Agent)
.Concat(_activeThreats.ConvertAll(threat => threat as Agent))
.Concat(GetActiveThreats().ConvertAll(threat => threat as Agent))
.ToList();
}
@ -108,8 +112,6 @@ public class SimManager : MonoBehaviour {
foreach (var swarmConfig in simulationConfig.interceptor_swarm_configs) {
for (int i = 0; i < swarmConfig.num_agents; i++) {
var interceptor = CreateInterceptor(swarmConfig.agent_config);
interceptor.OnAgentHit += RegisterInterceptorHit;
interceptor.OnAgentMiss += RegisterInterceptorMiss;
}
}
@ -118,8 +120,6 @@ public class SimManager : MonoBehaviour {
foreach (var swarmConfig in simulationConfig.threat_swarm_configs) {
for (int i = 0; i < swarmConfig.num_agents; i++) {
var threat = CreateThreat(swarmConfig.agent_config);
threat.OnAgentHit += RegisterThreatHit;
threat.OnAgentMiss += RegisterThreatMiss;
}
}
@ -131,31 +131,35 @@ public class SimManager : MonoBehaviour {
}
public void AssignInterceptorsToThreats() {
AssignInterceptorsToThreats(_interceptors);
AssignInterceptorsToThreats(_interceptorObjects);
}
public void RegisterInterceptorHit(Agent interceptor) {
public void RegisterInterceptorHit(Interceptor interceptor, Threat threat) {
if (interceptor is Interceptor missileComponent) {
_activeInterceptors.Remove(missileComponent);
}
}
public void RegisterInterceptorMiss(Agent interceptor) {
public void RegisterInterceptorMiss(Interceptor interceptor, Threat threat) {
if (interceptor is Interceptor missileComponent) {
_activeInterceptors.Remove(missileComponent);
}
// Remove the interceptor from the threat's assigned interceptors
_threatDataMap[threat].RemoveInterceptor(interceptor);
}
public void RegisterThreatHit(Agent threat) {
if (threat is Threat targetComponent) {
_activeThreats.Remove(targetComponent);
public void RegisterThreatHit(Interceptor interceptor, Threat threat) {
ThreatData threatData = _threatDataMap[threat];
threatData.RemoveInterceptor(interceptor);
if (threatData != null) {
threatData.MarkDestroyed();
}
}
public void RegisterThreatMiss(Agent threat) {
if (threat is Threat targetComponent) {
_unassignedThreats.Add(targetComponent);
}
public void RegisterThreatMiss(Interceptor interceptor, Threat threat) {
Debug.Log($"RegisterThreatMiss: Interceptor {interceptor.name} missed threat {threat.name}");
ThreatData threatData = _threatDataMap[threat];
threatData.RemoveInterceptor(interceptor);
}
/// <summary>
@ -163,27 +167,34 @@ public class SimManager : MonoBehaviour {
/// </summary>
/// <param name="missilesToAssign">The list of missiles to assign.</param>
public void AssignInterceptorsToThreats(List<Interceptor> missilesToAssign) {
// Convert Interceptor and Threat lists to Agent lists
List<Agent> missileAgents = new List<Agent>(missilesToAssign.ConvertAll(m => m as Agent));
// Convert Threat list to Agent list, excluding already assigned targets
List<Agent> targetAgents = _unassignedThreats.ToList<Agent>();
// Perform the assignment
IEnumerable<IAssignment.AssignmentItem> assignments =
_assignmentScheme.Assign(missileAgents, targetAgents);
_assignmentScheme.Assign(missilesToAssign, _threatTable);
// Apply the assignments to the missiles
foreach (var assignment in assignments) {
if (assignment.InterceptorIndex < missilesToAssign.Count) {
Interceptor interceptor = missilesToAssign[assignment.InterceptorIndex];
Threat threat = _unassignedThreats[assignment.ThreatIndex];
interceptor.AssignTarget(threat);
Debug.Log($"Interceptor {interceptor.name} assigned to threat {threat.name}");
}
Debug.LogWarning($"Assigning interceptor {assignment.Interceptor} to threat {assignment.Threat}");
assignment.Interceptor.AssignTarget(assignment.Threat);
_threatDataMap[assignment.Threat].AssignInterceptor(assignment.Interceptor);
Debug.Log($"Interceptor {assignment.Interceptor.name} assigned to threat {assignment.Threat.name}");
}
// Check if any interceptors were not assigned
List<Interceptor> unassignedInterceptors = missilesToAssign.Where(m => !m.HasAssignedTarget()).ToList();
if (unassignedInterceptors.Count > 0)
{
string unassignedIds = string.Join(", ", unassignedInterceptors.Select(m => m.name));
int totalInterceptors = missilesToAssign.Count;
int assignedInterceptors = totalInterceptors - unassignedInterceptors.Count;
Debug.LogWarning($"Warning: {unassignedInterceptors.Count} out of {totalInterceptors} interceptors were not assigned to any threat. " +
$"Unassigned interceptor IDs: {unassignedIds}. " +
$"Total interceptors: {totalInterceptors}, Assigned: {assignedInterceptors}, Unassigned: {unassignedInterceptors.Count}");
// Log information about the assignment scheme
Debug.Log($"Current Assignment Scheme: {_assignmentScheme.GetType().Name}");
}
// TODO this whole function should be optimized
_unassignedThreats.RemoveAll(
threat => missilesToAssign.Any(interceptor => interceptor.GetAssignedTarget() == threat));
}
/// <summary>
@ -196,27 +207,32 @@ public class SimManager : MonoBehaviour {
InterceptorType.MICROMISSILE => "Micromissile",
_ => "Hydra70" };
GameObject missileObject = CreateAgent(config, prefabName);
if (missileObject == null)
GameObject interceptorObject = CreateAgent(config, prefabName);
if (interceptorObject == null)
return null;
// Interceptor-specific logic
switch (config.dynamic_config.sensor_config.type) {
case SensorType.IDEAL:
missileObject.AddComponent<IdealSensor>();
interceptorObject.AddComponent<IdealSensor>();
break;
default:
Debug.LogError($"Sensor type '{config.dynamic_config.sensor_config.type}' not found.");
break;
}
_interceptors.Add(missileObject.GetComponent<Interceptor>());
_activeInterceptors.Add(missileObject.GetComponent<Interceptor>());
Interceptor interceptor = interceptorObject.GetComponent<Interceptor>();
_interceptorObjects.Add(interceptor);
_activeInterceptors.Add(interceptor);
// Subscribe events
interceptor.OnInterceptHit += RegisterInterceptorHit;
interceptor.OnInterceptMiss += RegisterInterceptorMiss;
// Assign a unique and simple ID
int missileId = _interceptors.Count;
missileObject.name = $"{config.interceptor_type}_Interceptor_{missileId}";
return missileObject.GetComponent<Interceptor>();
int missileId = _interceptorObjects.Count;
interceptorObject.name = $"{config.interceptor_type}_Interceptor_{missileId}";
return interceptorObject.GetComponent<Interceptor>();
}
/// <summary>
@ -233,13 +249,20 @@ public class SimManager : MonoBehaviour {
if (threatObject == null)
return null;
_threats.Add(threatObject.GetComponent<Threat>());
_activeThreats.Add(threatObject.GetComponent<Threat>());
_unassignedThreats.Add(threatObject.GetComponent<Threat>());
Threat threat = threatObject.GetComponent<Threat>();
// Assign a unique and simple ID
int targetId = _threats.Count;
int targetId = _threatTable.Count;
threatObject.name = $"{config.threat_type}_Target_{targetId}";
ThreatData threatData = new ThreatData(threat, threatObject.name);
_threatDataMap.Add(threat, threatData);
_threatTable.Add(threatData);
_threatObjects.Add(threat);
// Subscribe events
threat.OnInterceptHit += RegisterThreatHit;
threat.OnInterceptMiss += RegisterThreatMiss;
return threatObject.GetComponent<Threat>();
}
@ -290,21 +313,24 @@ public class SimManager : MonoBehaviour {
simulationRunning = IsSimulationRunning();
// Clear existing missiles and targets
foreach (var interceptor in _interceptors) {
foreach (var interceptor in _interceptorObjects) {
if (interceptor != null) {
Destroy(interceptor.gameObject);
}
}
foreach (var threat in _threats) {
foreach (var threat in _threatObjects) {
if (threat != null) {
Destroy(threat.gameObject);
}
}
_interceptors.Clear();
_threats.Clear();
_unassignedThreats.Clear();
_interceptorObjects.Clear();
_activeInterceptors.Clear();
_threatObjects.Clear();
_threatTable.Clear();
StartSimulation();
}
@ -312,7 +338,7 @@ public class SimManager : MonoBehaviour {
void Update() {
// Check if all missiles have terminated
bool allInterceptorsTerminated = true;
foreach (var interceptor in _interceptors) {
foreach (var interceptor in _interceptorObjects) {
if (interceptor != null && !interceptor.IsHit() && !interceptor.IsMiss()) {
allInterceptorsTerminated = false;
break;

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: f034d27b4aab67a47865af3d624c4375
guid: 5ab47dc725c65cb4cb1fa6948fb0c9a7
folderAsset: yes
DefaultImporter:
externalObjects: {}

View File

@ -0,0 +1,25 @@
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
using System.Collections;
public class SanityTest
{
[Test]
public void SanityTestSimplePasses()
{
// Use the Assert class to test conditions
Assert.Pass("This test passes.");
}
// A UnityTest behaves like a coroutine in Play Mode. In Edit Mode you can use
// `yield return null;` to skip a frame.
[UnityTest]
public IEnumerator SanityTestWithEnumeratorPasses()
{
// Use the Assert class to test conditions.
// Use yield to skip a frame.
yield return null;
Assert.Pass("This test passes after skipping a frame.");
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b48cf6e434723a449a49186eeda32d3f

24
Assets/Tests/Tests.asmdef Normal file
View File

@ -0,0 +1,24 @@
{
"name": "Tests",
"rootNamespace": "",
"references": [
"UnityEngine.TestRunner",
"UnityEditor.TestRunner",
"MicromissileAssembly"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": true,
"precompiledReferences": [
"nunit.framework.dll"
],
"autoReferenced": false,
"defineConstraints": [
"UNITY_INCLUDE_TESTS"
],
"versionDefines": [],
"noEngineReferences": false
}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: d15c92e585e721749b63d85007276dbe
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,138 @@
using NUnit.Framework;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.TestTools;
using System.Linq;
public class ThreatAssignmentTests
{
[Test]
public void Assign_Should_Assign_All_Interceptors_And_Threats()
{
// Arrange
ThreatAssignment threatAssignment = new ThreatAssignment();
// Create interceptors
List<Interceptor> interceptors = new List<Interceptor>
{
new GameObject("Interceptor 1").AddComponent<Micromissile>(),
new GameObject("Interceptor 2").AddComponent<Micromissile>(),
new GameObject("Interceptor 3").AddComponent<Micromissile>()
};
// Create threats
Threat threat1 = new GameObject("Threat 1").AddComponent<DroneTarget>();
Threat threat2 = new GameObject("Threat 2").AddComponent<DroneTarget>();
Threat threat3 = new GameObject("Threat 3").AddComponent<DroneTarget>();
// Add Rigidbody components to threats to set velocities
Rigidbody rb1 = threat1.gameObject.AddComponent<Rigidbody>();
Rigidbody rb2 = threat2.gameObject.AddComponent<Rigidbody>();
Rigidbody rb3 = threat3.gameObject.AddComponent<Rigidbody>();
// Set positions and velocities
threat1.transform.position = Vector3.forward * -20f;
threat2.transform.position = Vector3.forward * -20f;
threat3.transform.position = Vector3.forward * -20f;
rb1.linearVelocity = Vector3.forward * 5f;
rb2.linearVelocity = Vector3.forward * 10f;
rb3.linearVelocity = Vector3.forward * 15f;
// Create threat data
List<ThreatData> threats = new List<ThreatData>
{
new ThreatData(threat1, "Threat1ID"),
new ThreatData(threat2, "Threat2ID"),
new ThreatData(threat3, "Threat3ID")
};
// Act
IEnumerable<IAssignment.AssignmentItem> assignments = threatAssignment.Assign(interceptors, threats);
// Assert
Assert.AreEqual(3, assignments.Count(), "All interceptors should be assigned");
HashSet<Interceptor> assignedInterceptors = new HashSet<Interceptor>();
HashSet<Threat> assignedThreats = new HashSet<Threat>();
foreach (var assignment in assignments)
{
Assert.IsNotNull(assignment.Interceptor, "Interceptor should not be null");
Assert.IsNotNull(assignment.Threat, "Threat should not be null");
assignedInterceptors.Add(assignment.Interceptor);
assignedThreats.Add(assignment.Threat);
}
Assert.AreEqual(3, assignedInterceptors.Count, "All interceptors should be unique");
Assert.AreEqual(3, assignedThreats.Count, "All threats should be assigned");
// Verify that threats are assigned in order of their threat level (based on velocity and distance)
var orderedAssignments = assignments.OrderByDescending(a => a.Threat.GetVelocity().magnitude / Vector3.Distance(a.Threat.transform.position, Vector3.zero)).ToList();
Assert.AreEqual(threat3, orderedAssignments[0].Threat, "Highest threat should be assigned first");
Assert.AreEqual(threat2, orderedAssignments[1].Threat, "Second highest threat should be assigned second");
Assert.AreEqual(threat1, orderedAssignments[2].Threat, "Lowest threat should be assigned last");
}
[Test]
public void Assign_Should_Handle_More_Interceptors_Than_Threats()
{
// Arrange
ThreatAssignment threatAssignment = new ThreatAssignment();
// Create interceptors
List<Interceptor> interceptors = new List<Interceptor>
{
new GameObject("Interceptor 1").AddComponent<Micromissile>(),
new GameObject("Interceptor 2").AddComponent<Micromissile>(),
new GameObject("Interceptor 3").AddComponent<Micromissile>()
};
// Create threats
Threat threat1 = new GameObject("Threat 1").AddComponent<DroneTarget>();
Threat threat2 = new GameObject("Threat 2").AddComponent<DroneTarget>();
// Add Rigidbody components to threats to set velocities
Rigidbody rb1 = threat1.gameObject.AddComponent<Rigidbody>();
Rigidbody rb2 = threat2.gameObject.AddComponent<Rigidbody>();
// Set positions and velocities
threat1.transform.position = Vector3.up * 10f;
threat2.transform.position = Vector3.right * 5f;
rb1.linearVelocity = Vector3.forward * 10f;
rb2.linearVelocity = Vector3.forward * 15f;
// Create threat data
List<ThreatData> threats = new List<ThreatData>
{
new ThreatData(threat1, "Threat1ID"),
new ThreatData(threat2, "Threat2ID")
};
// Act
IEnumerable<IAssignment.AssignmentItem> assignments = threatAssignment.Assign(interceptors, threats);
// Assert
Assert.AreEqual(2, assignments.Count(), "All threats should be assigned");
HashSet<Interceptor> assignedInterceptors = new HashSet<Interceptor>();
HashSet<Threat> assignedThreats = new HashSet<Threat>();
foreach (var assignment in assignments)
{
Assert.IsNotNull(assignment.Interceptor, "Interceptor should not be null");
Assert.IsNotNull(assignment.Threat, "Threat should not be null");
assignedInterceptors.Add(assignment.Interceptor);
assignedThreats.Add(assignment.Threat);
}
Assert.AreEqual(2, assignedInterceptors.Count, "Two interceptors should be assigned");
Assert.AreEqual(2, assignedThreats.Count, "Both threats should be assigned");
// Verify that threats are assigned in order of their threat level (based on velocity and distance)
var orderedAssignments = assignments.OrderByDescending(a => a.Threat.GetVelocity().magnitude / Vector3.Distance(a.Threat.transform.position, Vector3.zero)).ToList();
Assert.AreEqual(threat2, orderedAssignments[0].Threat, "Higher threat should be assigned first");
Assert.AreEqual(threat1, orderedAssignments[1].Threat, "Lower threat should be assigned second");
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7ddf271569a78ee4e995192d4df0ef3f

View File

@ -61,6 +61,11 @@
"type": "UnityEngine.PhysicMaterial",
"defaultInstantiationMode": 0
},
{
"userAdded": false,
"type": "UnityEngine.PhysicsMaterial",
"defaultInstantiationMode": 0
},
{
"userAdded": false,
"type": "UnityEngine.PhysicsMaterial2D",