using System; using System.Collections; using System.Collections.Generic; using System.Linq; using Sirenix.Utilities; using UnityEngine; using UnityEngine.Events; using VOID.Generic.Objects.Abstract; using VOID.Generic.Objects.Unit; using VOID.Generic.Trajectory; namespace VOID.Generic.Projectile { /// /// Moves projectile on defined /// [DefaultExecutionOrder(-1)] public class ProjectileObject : MonoBehaviour { // Projectile Configuration public int index { private set; get; } public UnitObject caster { private set; get; } public ProjectileStep[] steps { private set; get; } // Events public UnityEvent onStart = new(); public UnityEvent onHit = new(); public UnityEvent onNextStep = new(); public UnityEvent onEnd = new(); // Step info private ProjectileStep _currentStep; private int _currentStepIndex = -1; private float _currentStepDistance; private AbstractTrajectory _currentTrajectory; private Vector3 _startPosition; private Vector3 _targetPosition; private Vector3 _lastHitPosition; // Projectile and its AOE game objects private GameObject _gameObject; private GameObject _aoeGameObject; // Storing collisions private readonly Dictionary _aoeCollisions = new(); private void Awake() { gameObject.SetActive(false); } private void FixedUpdate() { if (MoveProjectile() || CheckCollision()) StartCoroutine(NextStep()); } /// /// Moves projectile by one step in trajectory. /// /// TRUE if current step reached end private bool MoveProjectile() { _currentStepDistance += Time.fixedDeltaTime * _currentStep.speed; var newProgress = _currentTrajectory.GetProgressForDistance(_currentStepDistance); var newPosition = _currentTrajectory.GetPositionByProgress(newProgress); // Update rotation _gameObject.transform.rotation = Quaternion.LookRotation(transform.position - newPosition); // Update position _aoeGameObject.transform.position = transform.position = newPosition; return Mathf.Approximately(newProgress, 1f); } /// /// Checks if projectile should end early because it hit something. /// /// TRUE if collision ends this projectile step private bool CheckCollision() { int layerMask; switch (_currentStep.collisionType) { case ProjectileCollisionType.StaticOrDynamicCollision: layerMask = LayerManager.Static | LayerManager.StaticHideable | LayerManager.Dynamic; break; case ProjectileCollisionType.StaticCollision: layerMask = LayerManager.Static | LayerManager.StaticHideable; break; default: return false; } // Just to be sure - ignore caster :D var colliders = new Collider[2]; Physics.OverlapSphereNonAlloc(transform.position, _currentStep.radius, colliders, layerMask); return colliders.Any(collider => collider && collider.GetComponentInParent() != caster); } /// /// Starts next step of projectile. If no more steps available then whole projectile is destroyed. /// private IEnumerator NextStep() { // IMPORTANT // 1. Collisions calculate after fixed update, we need to wait 1 (or 2?) fixed updates // to be sure that we dont ignore collision that will occour later this frame in projectile's AOE // 2. Also we want to stop FixedUpdate from running when this happens otherwise this part will be executed twice enabled = false; yield return new WaitForFixedUpdate(); enabled = true; if (_currentStep.hitType == ProjectileHitType.OnEnd) _aoeCollisions.Keys.ForEach(hitGameObject => onHit.Invoke(this, hitGameObject)); if (_currentStepIndex >= steps.Length - 1) { EndProjectile(); yield break; } _lastHitPosition = gameObject.transform.position; onNextStep.Invoke(this); PrepareNextStep(); } /// /// 1. Clear temporary data
/// 2. Initialize next Trajectory
/// 3. Initialize next Area Of Effect ///
private void PrepareNextStep() { // Clear temp data about previous step _currentStepDistance = 0f; _currentStep = steps[++_currentStepIndex]; _aoeCollisions.Clear(); _currentTrajectory = _currentStep.trajectory.Clone(); // Update AOE GameObject's rotation and collider by data in AreaOfEffect // This process can be done once when starting new step, because AOE dont change during flight if (_aoeGameObject.TryGetComponent(out Collider aoeCollider)) Destroy(aoeCollider); _currentStep.areaOfEffect.AddCollider(_aoeGameObject); _aoeGameObject.transform.rotation = _currentStep.areaOfEffect.GetRotation(_startPosition, _targetPosition); // Prepare trajectory _currentTrajectory.Init(FindStartPositionForStep(), _targetPosition); } /// /// Returns start position for current step by defined . /// private Vector3 FindStartPositionForStep() { return _currentStep.startFrom switch { ProjectileStepStartFrom.FromStart => _startPosition, ProjectileStepStartFrom.FromCaster => caster.transform.position, ProjectileStepStartFrom.FromHit => _lastHitPosition == default ? _lastHitPosition : _startPosition, _ => throw new ArgumentOutOfRangeException( nameof(_currentStep.startFrom), _currentStep.startFrom, null) }; } /// /// Initializes and starts whole projectile. /// /// Index of this projectile from source ability /// UnitObject that fired this projectile /// Projectile starting position /// Projectile target position /// Definition HOW projectile should move and hit public void StartProjectile( int index, UnitObject caster, Vector3 startPosition, Vector3 targetPosition, ProjectileStep[] steps ) { this.index = index; this.caster = caster; this.steps = steps; _startPosition = startPosition; _targetPosition = targetPosition; // Projectile GameObject _gameObject = gameObject; _gameObject.layer = LayerManager.TriggerIndex; _gameObject.transform.position = _startPosition; _gameObject.SetActive(true); // Projectile AOE GameObject _aoeGameObject = new GameObject("Projectile AOE collider"); _aoeGameObject.layer = LayerManager.TriggerIndex; _aoeGameObject.transform.position = gameObject.transform.position; // Projectile AOE controlling component var projectileAoe = _aoeGameObject.AddComponent(); projectileAoe.OnAoeEnter += OnAoeEnter; projectileAoe.OnAoeExit += OnAoeExit; onStart.Invoke(this); PrepareNextStep(); } /// /// Ends and destroy projectile. /// private void EndProjectile() { onEnd.Invoke(this); // Destroying only projectile's AOE and script, not whole projectile yet! Destroy(this); Destroy(_aoeGameObject); // Wait a little bit for other things like VFX to finish then we can safely destroy whole projectile Destroy(_gameObject, 3); } /// /// Checking if given target should be hit and executed OnHit. /// private bool IsValidHit(UnitObject caster, AbstractObject target) { return _currentStep.validHits.Count == 0 || _currentStep.validHits.Any(validator => validator.IsValid(caster, target)); } /// /// Collision enter detected by projectile AOE. /// private void OnAoeEnter(Collider other) { if (!LayerManager.CodeInMask(other.gameObject.layer, LayerManager.Dynamic)) return; var hitObject = other.GetComponentInParent(); if (!hitObject) return; if (!IsValidHit(caster, hitObject)) return; _aoeCollisions.TryAdd(hitObject, 0); _aoeCollisions[hitObject]++; if (_currentStep.hitType == ProjectileHitType.Always) onHit.Invoke(this, hitObject); } /// /// Collision exit detected by projectile AOE. /// private void OnAoeExit(Collider other) { if (!LayerManager.CodeInMask(other.gameObject.layer, LayerManager.Dynamic)) return; var hitObject = other.GetComponentInParent(); if (!hitObject) return; if (!_aoeCollisions.ContainsKey(hitObject)) return; _aoeCollisions[hitObject]--; if (_aoeCollisions[hitObject] == 0) _aoeCollisions.Remove(hitObject); } } }