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; protected StaticConfig _staticConfig;
// Define delegates // Define delegates
public delegate void AgentHitEventHandler(Agent agent); public delegate void InterceptHitEventHandler(Interceptor interceptor, Threat target);
public delegate void AgentMissEventHandler(Agent agent); public delegate void InterceptMissEventHandler(Interceptor interceptor, Threat target);
// Define events // Define events
public event AgentHitEventHandler OnAgentHit; public event InterceptHitEventHandler OnInterceptHit;
public event AgentMissEventHandler OnAgentMiss; public event InterceptMissEventHandler OnInterceptMiss;
public void SetFlightPhase(FlightPhase flightPhase) { public void SetFlightPhase(FlightPhase flightPhase) {
Debug.Log( Debug.Log(
@ -105,16 +105,24 @@ public abstract class Agent : MonoBehaviour {
} }
// Mark the agent as having hit the target or been hit. // Mark the agent as having hit the target or been hit.
public void MarkAsHit() { public void HandleInterceptHit() {
_isHit = true; _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(); TerminateAgent();
} }
public void MarkAsMiss() { public void HandleInterceptMiss() {
_isMiss = true; _isMiss = true;
if (_target != null) { 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; _target = null;
} }
TerminateAgent(); TerminateAgent();

View File

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

View File

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

View File

@ -3,70 +3,64 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using Unity.VisualScripting; using Unity.VisualScripting;
using UnityEngine; using UnityEngine;
using System.Diagnostics.Contracts;
// The threat assignment class assigns missiles to the targets based // The threat assignment class assigns interceptors to the targets based
// on the threat level of the targets. // on the threat level of the targets.
public class ThreatAssignment : IAssignment { public class ThreatAssignment : IAssignment {
// Assign a target to each interceptor that has not been assigned a target yet. // 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<IAssignment.AssignmentItem> assignments = new List<IAssignment.AssignmentItem>();
List<int> assignableInterceptorIndices = IAssignment.GetAssignableInterceptorIndices(missiles); List<Interceptor> assignableInterceptors = IAssignment.GetAssignableInterceptors(interceptors);
if (assignableInterceptorIndices.Count == 0) { if (assignableInterceptors.Count == 0) {
Debug.LogWarning("No assignable interceptors found");
return assignments; return assignments;
} }
List<int> activeThreatIndices = IAssignment.GetActiveThreatIndices(targets); List<ThreatData> activeThreats = IAssignment.GetActiveThreats(targets);
if (activeThreatIndices.Count == 0) { if (activeThreats.Count == 0) {
Debug.LogWarning("No active threats found");
return assignments; return assignments;
} }
Vector3 positionToDefend = Vector3.zero; Vector3 positionToDefend = Vector3.zero;
List<ThreatInfo> threatInfos = List<ThreatInfo> threatInfos =
CalculateThreatLevels(targets, activeThreatIndices, positionToDefend); CalculateThreatLevels(activeThreats, positionToDefend);
foreach (int missileIndex in assignableInterceptorIndices) { // Sort ThreatInfo first by ThreatData.Status (UNASSIGNED first, then ASSIGNED)
if (missiles[missileIndex].HasAssignedTarget()) // Within each group, order by ThreatLevel descending
continue; threatInfos = threatInfos.OrderByDescending(t => t.ThreatData.Status == ThreatStatus.UNASSIGNED)
if (threatInfos.Count == 0) .ThenByDescending(t => t.ThreatLevel)
.ToList();
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; break;
// 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;
} }
} }
if (optimalTarget != null) {
assignments.Add(new IAssignment.AssignmentItem(missileIndex, optimalTarget.TargetIndex));
threatInfos.Remove(optimalTarget);
}
} }
return assignments; 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>(); List<ThreatInfo> threatInfos = new List<ThreatInfo>();
foreach (int targetIndex in activeThreatIndices) { foreach (ThreatData threatData in threatTable) {
Agent target = targets[targetIndex]; Threat threat = threatData.Threat;
float distanceToMean = Vector3.Distance(target.transform.position, missilesMeanPosition); float distanceToMean = Vector3.Distance(threat.transform.position, defensePosition);
float velocityMagnitude = target.GetVelocity().magnitude; float velocityMagnitude = threat.GetVelocity().magnitude;
// Calculate threat level based on proximity and velocity // Calculate threat level based on proximity and velocity
float threatLevel = (1 / distanceToMean) * velocityMagnitude; float threatLevel = (1 / distanceToMean) * velocityMagnitude;
threatInfos.Add(new ThreatInfo(targetIndex, threatLevel)); threatInfos.Add(new ThreatInfo(threatData, threatLevel));
} }
// Sort threats in descending order // Sort threats in descending order
@ -74,11 +68,11 @@ public class ThreatAssignment : IAssignment {
} }
private class ThreatInfo { private class ThreatInfo {
public int TargetIndex { get; } public ThreatData ThreatData { get; }
public float ThreatLevel { get; } public float ThreatLevel { get; }
public ThreatInfo(int targetIndex, float threatLevel) { public ThreatInfo(ThreatData threatData, float threatLevel) {
TargetIndex = targetIndex; ThreatData = threatData;
ThreatLevel = threatLevel; 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) { private void OnTriggerEnter(Collider other) {
if (other.gameObject.name == "Floor") { if (other.gameObject.name == "Floor") {
this.MarkAsMiss(); this.HandleInterceptMiss();
} }
// Check if the collision is with another Agent // Check if the collision is with another Agent
Agent otherAgent = other.gameObject.GetComponentInParent<Agent>(); Agent otherAgent = other.gameObject.GetComponentInParent<Agent>();
@ -87,13 +87,13 @@ public class Interceptor : Agent {
// Set green for hit // Set green for hit
markerObject.GetComponent<Renderer>().material.color = new Color(0, 1, 0, 0.15f); markerObject.GetComponent<Renderer>().material.color = new Color(0, 1, 0, 0.15f);
// Mark both this agent and the other agent as hit // Mark both this agent and the other agent as hit
this.MarkAsHit(); this.HandleInterceptHit();
otherAgent.MarkAsHit(); otherAgent.HandleInterceptHit();
} else { } else {
// Set red for miss // Set red for miss
markerObject.GetComponent<Renderer>().material.color = new Color(1, 0, 0, 0.15f); markerObject.GetComponent<Renderer>().material.color = new Color(1, 0, 0, 0.15f);
this.MarkAsMiss(); this.HandleInterceptMiss();
// otherAgent.MarkAsMiss(); // otherAgent.MarkAsMiss();
} }
} }

View File

@ -28,7 +28,7 @@ public class Micromissile : Interceptor {
// Check whether the threat should be considered a miss // Check whether the threat should be considered a miss
SensorOutput sensorOutput = GetComponent<Sensor>().Sense(_target); SensorOutput sensorOutput = GetComponent<Sensor>().Sense(_target);
if (sensorOutput.velocity.range > 1000f) { if (sensorOutput.velocity.range > 1000f) {
this.MarkAsMiss(); this.HandleInterceptMiss();
} }
// Calculate the acceleration input // 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() private void ExportTelemetry()
{ {
float time = (float)SimManager.Instance.GetElapsedSimulationTime(); 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; Vector3 pos = agent.transform.position;
if(pos == Vector3.zero) { if(pos == Vector3.zero) {

View File

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

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2 fileFormatVersion: 2
guid: f034d27b4aab67a47865af3d624c4375 guid: 5ab47dc725c65cb4cb1fa6948fb0c9a7
folderAsset: yes folderAsset: yes
DefaultImporter: DefaultImporter:
externalObjects: {} 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", "type": "UnityEngine.PhysicMaterial",
"defaultInstantiationMode": 0 "defaultInstantiationMode": 0
}, },
{
"userAdded": false,
"type": "UnityEngine.PhysicsMaterial",
"defaultInstantiationMode": 0
},
{ {
"userAdded": false, "userAdded": false,
"type": "UnityEngine.PhysicsMaterial2D", "type": "UnityEngine.PhysicsMaterial2D",