< Summary

Class:InterceptorBase
Assembly:bamlab.micromissiles
File(s):/github/workspace/Assets/Scripts/Interceptors/InterceptorBase.cs
Covered lines:0
Uncovered lines:195
Coverable lines:195
Total lines:365
Line coverage:0% (0 of 195)
Covered branches:0
Total branches:0
Covered methods:0
Total methods:30
Method coverage:0% (0 of 30)

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity NPath complexity Sequence coverage
InterceptorBase()0%2100%
EvaluateReassignedTarget(...)0%30500%
AssignSubInterceptor(...)0%20400%
ReassignTarget(...)0%12300%
Start()0%2100%
FixedUpdate()0%1321100%
OnDestroy()0%6200%
UpdateAgentConfig()0%2721600%
OnDrawGizmos()0%20400%
OnTriggerEnter(...)0%1101000%
RegisterMiss(...)0%6200%
RegisterDestroyed(...)0%2100%
RequestTargetReassignment(...)0%30500%
RequestReassignment(...)0%12300%
UnassignedTargetsManager()0%1821300%

File(s)

/github/workspace/Assets/Scripts/Interceptors/InterceptorBase.cs

#LineLine coverage
 1using System.Collections;
 2using System.Collections.Generic;
 3using System.Linq;
 4using UnityEngine;
 5
 6// Base implementation of an interceptor.
 7public abstract class InterceptorBase : AgentBase, IInterceptor {
 8  public event InterceptorEventHandler OnHit;
 9  public event InterceptorEventHandler OnMiss;
 10  public event InterceptorEventHandler OnDestroyed;
 11  public event InterceptorEventHandler OnAssignSubInterceptor;
 12  public event TargetReassignEventHandler OnReassignTarget;
 13
 14  // Default proportional navigation controller gain.
 15  private const float _proportionalNavigationGain = 5f;
 16
 17  // Time to accumulate unassigned targets before launching additional sub-interceptors.
 18  private const float _unassignedTargetsLaunchPeriod = 2.5f;
 19
 020  public IEscapeDetector EscapeDetector { get; set; }
 21
 22  // Maximum number of threats that this interceptor can target.
 23  [SerializeField]
 24  private int _capacity;
 25
 26  // Capacity of each sub-interceptor.
 27  [SerializeField]
 28  private int _capacityPerSubInterceptor;
 29
 30  // Number of sub-interceptors.
 31  [SerializeField]
 32  private int _numSubInterceptors;
 33
 34  // Number of sub-interceptors remaining that can be planned to launch.
 35  [SerializeField]
 36  private int _numSubInterceptorsPlannedRemaining;
 37
 38  // Number of sub-interceptors remaining.
 39  [SerializeField]
 40  private int _numSubInterceptorsRemaining;
 41
 42  public int Capacity {
 043    get => _capacity;
 44  protected
 045    set { _capacity = value; }
 46  }
 47  public int CapacityPerSubInterceptor {
 048    get => _capacityPerSubInterceptor;
 49  protected
 050    set { _capacityPerSubInterceptor = value; }
 51  }
 052  public virtual int CapacityPlannedRemaining => CapacityPerSubInterceptor *
 53                                                 NumSubInterceptorsPlannedRemaining;
 054  public virtual int CapacityRemaining => CapacityPerSubInterceptor * NumSubInterceptorsRemaining;
 55  public int NumSubInterceptors {
 056    get => _numSubInterceptors;
 57  protected
 058    set { _numSubInterceptors = value; }
 59  }
 60  public int NumSubInterceptorsPlannedRemaining {
 061    get => _numSubInterceptorsPlannedRemaining;
 62  protected
 063    set { _numSubInterceptorsPlannedRemaining = value; }
 64  }
 65  public int NumSubInterceptorsRemaining {
 066    get => _numSubInterceptorsRemaining;
 67  protected
 068    set { _numSubInterceptorsRemaining = value; }
 69  }
 70
 71  // If true, the interceptor can be reassigned to other targets.
 072  public virtual bool IsReassignable => true;
 73
 74  // Set of unassigned targets for which an additional sub-interceptor should be launched.
 075  private HashSet<IHierarchical> _unassignedTargets = new HashSet<IHierarchical>();
 76
 77  // Coroutine for handling unassigned targets.
 78  private Coroutine _unassignedTargetsCoroutine;
 79
 080  public bool EvaluateReassignedTarget(IHierarchical target) {
 81    // Continue searching for targets if no target was found.
 082    if (target == null) {
 083      return false;
 84    }
 85
 86    // If the interceptor has no target, always accept the new target.
 087    if (HierarchicalAgent.Target == null || HierarchicalAgent.Target.IsTerminated) {
 088      HierarchicalAgent.Target = target;
 089      return true;
 90    }
 91
 92    // Accept the new target if the intercept speed is higher.
 093    float currentFractionalSpeed =
 94        FractionalSpeed.Calculate(this, HierarchicalAgent.Target.Position);
 095    float newFractionalSpeed = FractionalSpeed.Calculate(this, target.Position);
 096    if (newFractionalSpeed > currentFractionalSpeed) {
 097      HierarchicalAgent.Target = target;
 098      return true;
 99    }
 0100    return false;
 0101  }
 102
 0103  public void AssignSubInterceptor(IInterceptor subInterceptor) {
 0104    if (subInterceptor.CapacityRemaining <= 0) {
 0105      return;
 106    }
 107
 108    // Find a new target for the sub-interceptor within the parent interceptor's assigned targets.
 0109    IHierarchical target = HierarchicalAgent.FindNewTarget(subInterceptor.HierarchicalAgent,
 110                                                           subInterceptor.CapacityRemaining);
 111    // Evaluate the new target and decide whether to continue searching for other targets.
 0112    if (!subInterceptor.EvaluateReassignedTarget(target)) {
 113      // Propagate the sub-interceptor target assignment to the parent interceptor above.
 0114      OnAssignSubInterceptor?.Invoke(subInterceptor);
 0115    }
 0116  }
 117
 0118  public void ReassignTarget(IHierarchical target) {
 119    // If a target needs to be re-assigned, the interceptor should in the following order:
 120    //  1. Queue up the unassigned targets in preparation of launching an additional
 121    //  sub-interceptor.
 122    //  2. If no existing sub-interceptor has been assigned to pursue the queued target(s), launch
 123    //  another sub-interceptor(s) to pursue the target(s).
 124    //  3. Propagate the target re-assignment to the parent interceptor above.
 0125    if (CapacityPlannedRemaining <= 0) {
 0126      OnReassignTarget?.Invoke(target);
 0127      return;
 128    }
 129
 0130    _unassignedTargets.Add(target);
 0131  }
 132
 0133  protected override void Start() {
 0134    base.Start();
 0135    _unassignedTargetsCoroutine =
 136        StartCoroutine(UnassignedTargetsManager(_unassignedTargetsLaunchPeriod));
 0137    OnMiss += RegisterMiss;
 0138    OnDestroyed += RegisterDestroyed;
 0139  }
 140
 0141  protected override void FixedUpdate() {
 0142    base.FixedUpdate();
 143
 144    // Check whether the interceptor has a target. If not, request a new target from the parent
 145    // interceptor.
 0146    if (HierarchicalAgent.Target == null || HierarchicalAgent.Target.IsTerminated) {
 0147      RequestReassignment(this);
 0148    }
 149
 150    // Check whether any targets are escaping from the interceptor.
 0151    if (EscapeDetector != null && HierarchicalAgent.Target != null &&
 0152        !HierarchicalAgent.Target.IsTerminated) {
 0153      List<IHierarchical> targetHierarchicals =
 154          HierarchicalAgent.Target.LeafHierarchicals(activeOnly: true, withTargetOnly: false);
 0155      List<IHierarchical> escapingTargets =
 156          targetHierarchicals.Where(EscapeDetector.IsEscaping).ToList();
 0157      foreach (var target in escapingTargets) {
 0158        OnReassignTarget?.Invoke(target);
 0159      }
 0160      if (escapingTargets.Count == targetHierarchicals.Count) {
 0161        RequestReassignment(this);
 0162      }
 0163    }
 164
 165    // Update the planned number of sub-interceptors remaining.
 166    // TODO(titan): Update the planned number of sub-interceptors remaining when the number of leaf
 167    // hierarchical objects changes, such as when a new target is added.
 0168    List<IHierarchical> leafHierarchicals =
 169        HierarchicalAgent.LeafHierarchicals(activeOnly: false, withTargetOnly: false);
 0170    NumSubInterceptorsPlannedRemaining =
 171        Mathf.Min(NumSubInterceptorsRemaining, NumSubInterceptors - leafHierarchicals.Count);
 172
 173    // Navigate towards the target.
 0174    AccelerationInput = Controller?.Plan() ?? Vector3.zero;
 0175    Acceleration = Movement?.Act(AccelerationInput) ?? Vector3.zero;
 0176    _rigidbody.AddForce(Acceleration, ForceMode.Acceleration);
 0177  }
 178
 0179  protected override void OnDestroy() {
 0180    base.OnDestroy();
 181
 0182    if (_unassignedTargetsCoroutine != null) {
 0183      StopCoroutine(_unassignedTargetsCoroutine);
 0184      _unassignedTargetsCoroutine = null;
 0185    }
 0186  }
 187
 0188  protected override void UpdateAgentConfig() {
 0189    base.UpdateAgentConfig();
 190
 191    // Calculate the capacity.
 0192    int NumAgents(Configs.AgentConfig config) {
 0193      if (config == null || config.SubAgentConfig == null) {
 0194        return 1;
 195      }
 0196      return (int)config.SubAgentConfig.NumSubAgents * NumAgents(config.SubAgentConfig.AgentConfig);
 0197    }
 0198    Capacity = NumAgents(AgentConfig);
 0199    CapacityPerSubInterceptor = NumAgents(AgentConfig.SubAgentConfig?.AgentConfig);
 0200    NumSubInterceptors = (int)(AgentConfig.SubAgentConfig?.NumSubAgents ?? 0);
 0201    NumSubInterceptorsPlannedRemaining = NumSubInterceptors;
 0202    NumSubInterceptorsRemaining = NumSubInterceptors;
 203
 204    // Set the controller.
 0205    switch (AgentConfig.DynamicConfig?.FlightConfig?.ControllerType) {
 0206      case Configs.ControllerType.ProportionalNavigation: {
 0207        Controller = new PnController(this, _proportionalNavigationGain);
 0208        break;
 209      }
 0210      case Configs.ControllerType.AugmentedProportionalNavigation: {
 0211        Controller = new ApnController(this, _proportionalNavigationGain);
 0212        break;
 213      }
 0214      default: {
 0215        Debug.LogWarning(
 216            $"Controller type {AgentConfig.DynamicConfig?.FlightConfig?.ControllerType} not found.");
 0217        Controller = null;
 0218        break;
 219      }
 220    }
 0221  }
 222
 0223  protected override void OnDrawGizmos() {
 224    const float axisLength = 10f;
 225
 0226    base.OnDrawGizmos();
 227
 0228    if (Application.isPlaying) {
 229      // Target.
 0230      if (HierarchicalAgent.Target != null && !HierarchicalAgent.Target.IsTerminated) {
 0231        Gizmos.color = new Color(1, 1, 1, 0.15f);
 0232        Gizmos.DrawLine(Position, HierarchicalAgent.Target.Position);
 0233      }
 234
 235      // Up direction.
 0236      Gizmos.color = Color.yellow;
 0237      Gizmos.DrawRay(Position, Up * axisLength);
 238
 239      // Forward direction.
 0240      Gizmos.color = Color.blue;
 0241      Gizmos.DrawRay(Position, Forward * axisLength);
 242
 243      // Right direction.
 0244      Gizmos.color = Color.red;
 0245      Gizmos.DrawRay(Position, Right * axisLength);
 0246    }
 0247  }
 248
 249  // If the interceptor collides with the ground or another agent, it will be terminated. It is
 250  // possible for an interceptor to collide with another interceptor or with a non-target threat.
 251  // The interceptor records a hit only if it collides with a threat and destroys it with the
 252  // threat's kill probability.
 0253  private void OnTriggerEnter(Collider other) {
 0254    if (CheckGroundCollision(other)) {
 0255      OnDestroyed?.Invoke(this);
 0256      Terminate();
 0257    }
 258
 0259    IAgent otherAgent = other.gameObject.GetComponentInParent<IAgent>();
 0260    if (ShouldIgnoreCollision(otherAgent)) {
 0261      return;
 262    }
 263    // Check if the collision is with a threat.
 0264    if (otherAgent is IThreat threat) {
 265      // Check the kill probability.
 0266      float killProbability = threat.StaticConfig.HitConfig?.KillProbability ?? 1;
 0267      bool isHit = Random.value <= killProbability;
 0268      if (isHit) {
 0269        threat.HandleIntercept();
 0270        OnHit?.Invoke(this);
 0271        Terminate();
 0272      } else {
 0273        OnMiss?.Invoke(this);
 0274      }
 0275    }
 0276  }
 277
 0278  private void RegisterMiss(IInterceptor interceptor) {
 0279    RequestTargetReassignment(interceptor);
 280
 281    // Request a new target from the parent interceptor.
 0282    OnAssignSubInterceptor?.Invoke(interceptor);
 0283  }
 284
 0285  private void RegisterDestroyed(IInterceptor interceptor) {
 0286    RequestTargetReassignment(interceptor);
 0287  }
 288
 0289  private void RequestTargetReassignment(IInterceptor interceptor) {
 290    // Request the parent interceptor to re-assign the target to another interceptor if there are no
 291    // other pursuers.
 0292    IHierarchical target = interceptor.HierarchicalAgent.Target;
 0293    if (target == null || target.IsTerminated) {
 0294      return;
 295    }
 0296    List<IHierarchical> targetHierarchicals =
 297        target.LeafHierarchicals(activeOnly: true, withTargetOnly: false);
 0298    foreach (var targetHierarchical in targetHierarchicals) {
 0299      OnReassignTarget?.Invoke(targetHierarchical);
 0300    }
 301
 0302    RequestReassignment(interceptor);
 0303  }
 304
 0305  private void RequestReassignment(IInterceptor interceptor) {
 0306    if (interceptor.IsReassignable) {
 307      // Request a new target from the parent interceptor.
 0308      OnAssignSubInterceptor?.Invoke(interceptor);
 0309    }
 0310  }
 311
 0312  private IEnumerator UnassignedTargetsManager(float period) {
 0313    while (true) {
 0314      yield return new WaitUntil(() => _unassignedTargets.Count > 0);
 0315      yield return new WaitForSeconds(period);
 316
 0317      IEnumerable<IHierarchical> unassignedTargets = _unassignedTargets.ToList();
 0318      _unassignedTargets.Clear();
 319
 320      // Check whether the unassigned targets are still unassigned or are escaping the assigned
 321      // pursuers.
 0322      var filteredTargets =
 323          unassignedTargets
 0324              .Where(target => !target.IsTerminated && target.ActivePursuers.All(pursuer => {
 0325                var pursuerAgent = pursuer as HierarchicalAgent;
 0326                var interceptor = pursuerAgent?.Agent as IInterceptor;
 0327                return interceptor == null || interceptor.CapacityRemaining == 0 ||
 328                       (interceptor.EscapeDetector?.IsEscaping(target) ?? true);
 0329              }))
 330              .ToList();
 0331      if (filteredTargets.Count > CapacityPlannedRemaining) {
 332        // If there are more unassigned targets than the capacity remaining, propagate the target
 333        // re-assignment to the parent interceptor for the excess targets.
 0334        var orderedTargets =
 0335            filteredTargets.OrderBy(target => Vector3.Distance(Position, target.Position));
 0336        var excessTargets = orderedTargets.Skip(CapacityPlannedRemaining);
 0337        foreach (var target in excessTargets) {
 0338          OnReassignTarget?.Invoke(target);
 0339        }
 0340        unassignedTargets = orderedTargets.Take(CapacityPlannedRemaining);
 0341      } else {
 0342        unassignedTargets = filteredTargets;
 0343      }
 0344      if (!unassignedTargets.Any()) {
 0345        continue;
 346      }
 347
 348      // Create a new hierarchical object with the cluster of unassigned targets as the target.
 0349      var newTargetSubHierarchical = new HierarchicalBase();
 0350      int numUnassignedTargets = 0;
 0351      foreach (var target in unassignedTargets) {
 0352        newTargetSubHierarchical.AddSubHierarchical(target);
 0353        ++numUnassignedTargets;
 0354      }
 0355      var newSubHierarchical = new HierarchicalBase { Target = newTargetSubHierarchical };
 0356      HierarchicalAgent.AddSubHierarchical(newSubHierarchical);
 0357      Debug.Log($"Reclustered {numUnassignedTargets} target(s) into a new cluster for {this}.");
 0358      UIManager.Instance.LogActionMessage(
 359          $"[IADS] Reclustered {numUnassignedTargets} target(s) into a new cluster for {this}.");
 360
 361      // Recursively cluster the newly assigned targets.
 0362      newSubHierarchical.RecursiveCluster(maxClusterSize: CapacityPerSubInterceptor);
 0363    }
 364  }
 365}