< Summary

Class:CameraController
Assembly:bamlab.micromissiles
File(s):/github/workspace/Assets/Scripts/UI/CameraController.cs
Covered lines:0
Uncovered lines:246
Coverable lines:246
Total lines:429
Line coverage:0% (0 of 246)
Covered branches:0
Total branches:0
Covered methods:0
Total methods:35
Method coverage:0% (0 of 35)

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity NPath complexity Sequence coverage
CameraController()0%2100%
Follow(...)0%2100%
Snap(...)0%2100%
OrbitCamera(...)0%6200%
RotateCamera(...)0%2100%
TranslateCamera(...)0%6200%
ZoomCamera(...)0%2100%
Awake()0%12300%
Start()0%2100%
Update()0%6200%
SetCameraRotation(...)0%2100%
UpdateCameraPosition(...)0%6200%
SetCameraTargetPosition(...)0%2100%
ResetCameraTarget()0%6200%
UpdateTargetAlpha()0%2100%
ClampAngle(...)0%12300%
StartCentroidUpdateCoroutine()0%6200%
UpdateCentroidCoroutine()0%12300%
UpdateTargetCentroid()0%2100%
AutoPlayRoutine()0%12300%
UpdateDirectionVectors()0%90900%
GetAgentGroupInfo(...)0%20400%
FindCentroid(...)0%20400%

File(s)

/github/workspace/Assets/Scripts/UI/CameraController.cs

#LineLine coverage
 1using System.Collections;
 2using System.Collections.Generic;
 3using System.Linq;
 4using UnityEngine;
 5
 6public class CameraController : MonoBehaviour {
 7  public enum TranslationInput {
 8    Forward,
 9    Left,
 10    Back,
 11    Right,
 12    Up,
 13    Down,
 14  }
 15
 16  // Mapping of translation inputs to movement vectors.
 17  private Dictionary<TranslationInput, Vector3> _translationInputToVectorMap;
 18
 019  public static CameraController Instance { get; private set; }
 20
 21  // Forward movement vector.
 022  private readonly Vector3 _wVector = Vector3.forward;
 23
 24  // Left movement vector.
 025  private readonly Vector3 _aVector = Vector3.left;
 26
 27  // Backward movement vector.
 028  private readonly Vector3 _sVector = Vector3.back;
 29
 30  // Right movement vector.
 031  private readonly Vector3 _dVector = Vector3.right;
 32
 33  // Normal speed of the camera movement.
 34  [SerializeField]
 035  private float _cameraSpeedNormal = 100f;
 36
 37  // Maximum speed of the camera movement.
 38  [SerializeField]
 039  private float _cameraSpeedMax = 1000f;
 40
 41  // Horizontal rotation speed.
 42  [SerializeField]
 043  private float _speedH = 2f;
 44
 45  // Vertical rotation speed.
 46  [SerializeField]
 047  private float _speedV = 2f;
 48
 49  // If true, the camera should auto-rotate.
 50  [SerializeField]
 051  private bool _autoRotate = false;
 52
 53  // Target transform for orbit rotation.
 54  [SerializeField]
 055  private Transform _target = null!;
 56
 57  // Distance from the camera to the orbit target.
 58  [SerializeField]
 059  private float _orbitDistance = 5f;
 60
 61  // Horizontal orbit rotation speed.
 62  [SerializeField]
 063  private float _orbitXSpeed = 120f;
 64
 65  // Vertical orbit rotation speed.
 66  [SerializeField]
 067  private float _orbitYSpeed = 120f;
 68
 69  // Minimum vertical angle limit for orbit.
 70  [SerializeField]
 071  private float _orbitYMinLimit = -20f;
 72
 73  // Maximum vertical angle limit for orbit.
 74  [SerializeField]
 075  private float _orbitYMaxLimit = 80f;
 76
 77  // Minimum distance for orbit.
 78  [SerializeField]
 079  private float _orbitDistanceMin = 10f;
 80
 81  // Maximum distance for orbit.
 82  [SerializeField]
 083  private float _orbitDistanceMax = 20000f;
 84
 85  // Speed of camera zoom.
 86  [SerializeField]
 087  private float _zoomSpeed = 500f;
 88
 89  // Renderer for the orbit target.
 90  [SerializeField]
 091  private Renderer _targetRenderer = null!;
 92
 93  // Speed of camera movement during autoplay.
 94  [SerializeField]
 095  private float _autoplayCamSpeed = 0.02f;
 96
 97  // Current horizontal orbit angle.
 098  private float _orbitX = 0f;
 99
 100  // Current vertical orbit angle.
 0101  private float _orbitY = 0f;
 102
 103  // Current yaw angle of the camera.
 0104  private float _yaw = 0f;
 105
 106  // Current pitch angle of the camera.
 0107  private float _pitch = 0f;
 108
 109  // Coroutine for autoplay functionality.
 110  private Coroutine _autoplayRoutine;
 111
 112  // Angle between forward vector and camera direction.
 113  [SerializeField]
 114  private float _forwardToCameraAngle;
 115
 116  [SerializeField]
 0117  private CameraMode _cameraMode = CameraMode.FREE;
 118
 119  [SerializeField]
 0120  private CameraFollowType _cameraFollowType = CameraFollowType.ALL_AGENTS;
 121
 122  private Vector3 _lastCentroid;
 123  private Vector3 _currentCentroid;
 124  private Vector3 _targetCentroid;
 125
 126  [SerializeField]
 0127  public float _centroidUpdateFrequency = 0.1f;
 128  [SerializeField]
 0129  public float _defaultInterpolationSpeed = 5f;
 130  [SerializeField]
 131  private float _currentInterpolationSpeed;
 132
 133  [SerializeField]
 0134  private float _iirFilterCoefficient = 0.9f;
 135
 136  private Coroutine _centroidUpdateCoroutine;
 137
 0138  public float CameraSpeed { get; set; }
 139
 0140  public float CameraSpeedMax => _cameraSpeedMax;
 141
 0142  public float CameraSpeedNormal => _cameraSpeedNormal;
 143
 144  public bool AutoRotate {
 0145    get => _autoRotate;
 0146    set {
 0147      if (value && !_autoRotate) {
 0148        _autoRotate = true;
 0149        _autoplayRoutine = StartCoroutine(AutoPlayRoutine());
 0150      } else if (!value && _autoRotate) {
 0151        _autoRotate = false;
 0152        if (_autoplayRoutine != null) {
 0153          StopCoroutine(_autoplayRoutine);
 0154          _autoplayRoutine = null;
 0155        }
 0156      }
 0157    }
 158  }
 159
 160  public CameraMode CameraMode {
 0161    get => _cameraMode;
 0162    set {
 0163      switch (value) {
 0164        case CameraMode.FREE: {
 0165          if (_centroidUpdateCoroutine != null) {
 0166            StopCoroutine(_centroidUpdateCoroutine);
 0167            _centroidUpdateCoroutine = null;
 0168          }
 0169          break;
 170        }
 0171        case CameraMode.FOLLOW: {
 0172          _currentCentroid = _target.position;
 0173          _targetCentroid = _target.position;
 0174          break;
 175        }
 0176        default: {
 0177          break;
 178        }
 179      }
 0180      _cameraMode = value;
 0181    }
 182  }
 183
 184  public CameraFollowType CameraFollowType {
 0185    get => _cameraFollowType;
 0186    set { _cameraFollowType = value; }
 187  }
 188
 0189  public void Follow(CameraFollowType type) {
 0190    var (agents, description) = GetAgentGroupInfo(type);
 0191    Vector3 centroid = FindCentroid(agents);
 0192    SetCameraTargetPosition(centroid);
 0193    CameraMode = CameraMode.FOLLOW;
 0194    CameraFollowType = type;
 0195    StartCentroidUpdateCoroutine();
 0196    UIManager.Instance.LogActionMessage($"[CAM] Follow center of {description}.");
 0197  }
 198
 0199  public void Snap(CameraFollowType type) {
 0200    var (agents, description) = GetAgentGroupInfo(type);
 0201    Vector3 centroid = FindCentroid(agents);
 0202    SetCameraTargetPosition(centroid);
 0203    UIManager.Instance.LogActionMessage($"[CAM] Snap to center of {description}.");
 0204  }
 205
 0206  public void OrbitCamera(float xOrbit, float yOrbit) {
 0207    if (_target) {
 0208      _orbitX += xOrbit * _orbitXSpeed * _orbitDistance * 0.02f;
 0209      _orbitY -= yOrbit * _orbitYSpeed * _orbitDistance * 0.02f;
 210
 0211      _orbitY = ClampAngle(_orbitY, _orbitYMinLimit, _orbitYMaxLimit);
 0212      UpdateCameraPosition(_orbitX, _orbitY);
 0213    }
 0214  }
 215
 0216  public void RotateCamera(float xRotate, float yRotate) {
 0217    _yaw += xRotate * _speedH;
 0218    _pitch -= yRotate * _speedV;
 0219    transform.eulerAngles = new Vector3(_pitch, _yaw, 0f);
 0220  }
 221
 0222  public void TranslateCamera(TranslationInput input) {
 0223    if (CameraMode != CameraMode.FREE) {
 0224      CameraMode = CameraMode.FREE;
 0225    }
 0226    UpdateDirectionVectors();
 0227    _target.Translate(_translationInputToVectorMap[input] * Time.unscaledDeltaTime * CameraSpeed);
 0228    UpdateCameraPosition(_orbitX, _orbitY);
 0229  }
 230
 0231  public void ZoomCamera(float zoom) {
 0232    _orbitDistance =
 233        Mathf.Clamp(_orbitDistance - zoom * _zoomSpeed, _orbitDistanceMin, _orbitDistanceMax);
 0234    UpdateCameraPosition(_orbitX, _orbitY);
 0235  }
 236
 0237  private void Awake() {
 0238    if (Instance != null && Instance != this) {
 0239      Destroy(gameObject);
 0240      return;
 241    }
 0242    Instance = this;
 0243    DontDestroyOnLoad(gameObject);
 244
 0245    _currentInterpolationSpeed = _defaultInterpolationSpeed;
 0246  }
 247
 0248  private void Start() {
 0249    Vector3 angles = transform.eulerAngles;
 0250    _orbitX = angles.y;
 0251    _orbitY = angles.x;
 252
 0253    UpdateTargetAlpha();
 0254    ResetCameraTarget();
 255
 0256    _translationInputToVectorMap = new Dictionary<TranslationInput, Vector3> {
 257      { TranslationInput.Forward, _wVector }, { TranslationInput.Left, _aVector },
 258      { TranslationInput.Back, _sVector },    { TranslationInput.Right, _dVector },
 259      { TranslationInput.Up, Vector3.up },    { TranslationInput.Down, Vector3.down }
 260    };
 0261    CameraMode = CameraMode.FREE;
 0262  }
 263
 0264  private void Update() {
 0265    if (CameraMode != CameraMode.FREE) {
 266      // Use MoveTowards for smoother and more predictable movement.
 0267      _currentCentroid = Vector3.MoveTowards(_currentCentroid, _targetCentroid,
 268                                             _currentInterpolationSpeed * Time.unscaledDeltaTime);
 0269      SetCameraTargetPosition(_currentCentroid);
 0270    }
 0271  }
 272
 0273  private void SetCameraRotation(in Quaternion rotation) {
 0274    transform.rotation = rotation;
 0275    _pitch = rotation.eulerAngles.x;
 0276    _yaw = rotation.eulerAngles.y;
 0277  }
 278
 0279  private void UpdateCameraPosition(float x, float y) {
 0280    Quaternion rotation = Quaternion.Euler(y, x, 0);
 0281    if (Physics.Linecast(_target.position, transform.position, out RaycastHit hit,
 0282                         ~LayerMask.GetMask("Floor"), QueryTriggerInteraction.Ignore)) {
 0283      _orbitDistance -= hit.distance;
 0284    }
 0285    Vector3 negDistance = new Vector3(0f, 0f, -_orbitDistance);
 0286    Vector3 position = rotation * negDistance + _target.position;
 0287    _orbitDistance = Mathf.Clamp(_orbitDistance, _orbitDistanceMin, _orbitDistanceMax);
 0288    UpdateTargetAlpha();
 289
 0290    SetCameraRotation(rotation);
 0291    transform.position = position;
 0292  }
 293
 0294  private void SetCameraTargetPosition(in Vector3 position) {
 0295    _target.transform.position = position;
 0296    UpdateCameraPosition(_orbitX, _orbitY);
 0297  }
 298
 0299  private void ResetCameraTarget() {
 300    RaycastHit hit;
 0301    if (Physics.Raycast(transform.position, transform.forward, out hit, float.MaxValue,
 0302                        LayerMask.GetMask("Floor"), QueryTriggerInteraction.Ignore)) {
 0303      _target.transform.position = hit.point;
 0304      _orbitDistance = hit.distance;
 0305      Vector3 angles = transform.eulerAngles;
 0306      _orbitX = angles.y;
 0307      _orbitY = angles.x;
 0308      UpdateCameraPosition(_orbitX, _orbitY);
 0309    } else {
 0310      _target.transform.position = transform.position + (transform.forward * 100);
 0311      _orbitDistance = 100;
 0312      Vector3 angles = transform.eulerAngles;
 0313      _orbitX = angles.y;
 0314      _orbitY = angles.x;
 0315    }
 0316  }
 317
 0318  private void UpdateTargetAlpha() {
 0319    float matAlpha = (_orbitDistance - _orbitDistanceMin) / (_orbitDistanceMax - _orbitDistanceMin);
 0320    matAlpha = Mathf.Max(Mathf.Sqrt(matAlpha) - 0.5f, 0);
 0321    Color matColor = _targetRenderer.material.color;
 0322    matColor.a = matAlpha;
 0323    _targetRenderer.material.color = matColor;
 0324  }
 325
 0326  private static float ClampAngle(float angle, float min, float max) {
 0327    if (angle < -360f)
 0328      angle += 360f;
 0329    if (angle > 360f)
 0330      angle -= 360f;
 0331    return Mathf.Clamp(angle, min, max);
 0332  }
 333
 0334  private void StartCentroidUpdateCoroutine() {
 0335    _centroidUpdateCoroutine ??= StartCoroutine(UpdateCentroidCoroutine());
 0336  }
 337
 0338  private IEnumerator UpdateCentroidCoroutine() {
 0339    while (true) {
 0340      UpdateTargetCentroid();
 0341      yield return new WaitForSeconds(_centroidUpdateFrequency);
 0342    }
 343  }
 344
 0345  private void UpdateTargetCentroid() {
 0346    _lastCentroid = _currentCentroid;
 0347    var (agents, _) = GetAgentGroupInfo(CameraFollowType);
 0348    _targetCentroid = FindCentroid(agents);
 349
 350    // Apply IIR filter to adjust interpolation speed.
 0351    float distance = Mathf.Abs(Vector3.Distance(_lastCentroid, _targetCentroid));
 0352    float targetSpeed = Mathf.Clamp(distance, 1f, 100000f);
 0353    _currentInterpolationSpeed = _iirFilterCoefficient * _currentInterpolationSpeed +
 354                                 (1 - _iirFilterCoefficient) * targetSpeed;
 0355  }
 356
 0357  private IEnumerator AutoPlayRoutine() {
 0358    while (true) {
 0359      _orbitX += Time.unscaledDeltaTime * _autoplayCamSpeed * _orbitDistance * 0.02f;
 0360      UpdateCameraPosition(_orbitX, _orbitY);
 0361      yield return null;
 0362    }
 363  }
 364
 0365  private void UpdateDirectionVectors() {
 0366    Vector3 cameraToTarget = _target.position - transform.position;
 0367    cameraToTarget.y = 0;
 0368    _forwardToCameraAngle = Vector3.SignedAngle(Vector3.forward, cameraToTarget, Vector3.down);
 369
 0370    if (_forwardToCameraAngle > -45f && _forwardToCameraAngle <= 45f) {
 0371      _translationInputToVectorMap[TranslationInput.Forward] = Vector3.forward;
 0372      _translationInputToVectorMap[TranslationInput.Left] = Vector3.left;
 0373      _translationInputToVectorMap[TranslationInput.Back] = Vector3.back;
 0374      _translationInputToVectorMap[TranslationInput.Right] = Vector3.right;
 0375    } else if (_forwardToCameraAngle > 45f && _forwardToCameraAngle <= 135f) {
 0376      _translationInputToVectorMap[TranslationInput.Forward] = Vector3.left;
 0377      _translationInputToVectorMap[TranslationInput.Left] = Vector3.back;
 0378      _translationInputToVectorMap[TranslationInput.Back] = Vector3.right;
 0379      _translationInputToVectorMap[TranslationInput.Right] = Vector3.forward;
 0380    } else if (_forwardToCameraAngle > 135f || _forwardToCameraAngle <= -135f) {
 0381      _translationInputToVectorMap[TranslationInput.Forward] = Vector3.back;
 0382      _translationInputToVectorMap[TranslationInput.Left] = Vector3.right;
 0383      _translationInputToVectorMap[TranslationInput.Back] = Vector3.forward;
 0384      _translationInputToVectorMap[TranslationInput.Right] = Vector3.left;
 0385    } else if (_forwardToCameraAngle > -135f && _forwardToCameraAngle <= -45f) {
 0386      _translationInputToVectorMap[TranslationInput.Forward] = Vector3.right;
 0387      _translationInputToVectorMap[TranslationInput.Left] = Vector3.forward;
 0388      _translationInputToVectorMap[TranslationInput.Back] = Vector3.left;
 0389      _translationInputToVectorMap[TranslationInput.Right] = Vector3.back;
 0390    }
 0391  }
 392
 393  private (IEnumerable<IAgent> agents, string description)
 0394      GetAgentGroupInfo(CameraFollowType type) {
 0395    switch (type) {
 396      case CameraFollowType.ALL_INTERCEPTORS:
 0397        return (SimManager.Instance.Interceptors, "all interceptors");
 398      case CameraFollowType.ALL_THREATS:
 0399        return (SimManager.Instance.Threats, "all threats");
 400      case CameraFollowType.ALL_AGENTS:
 401      default:
 0402        return (SimManager.Instance.Agents, "all agents");
 403    }
 0404  }
 405
 0406  private Vector3 FindCentroid(IEnumerable<IAgent> agents) {
 0407    var sum = Vector3.zero;
 0408    int count = 0;
 0409    foreach (var agent in agents) {
 0410      sum += agent.Position;
 0411      count++;
 0412    }
 0413    if (count == 0) {
 0414      return Vector3.zero;
 415    }
 0416    return sum / count;
 0417  }
 418}
 419
 420public enum CameraMode {
 421  FREE,
 422  FOLLOW,
 423}
 424
 425public enum CameraFollowType {
 426  ALL_AGENTS,
 427  ALL_INTERCEPTORS,
 428  ALL_THREATS,
 429}