267 lines
10 KiB
C#
267 lines
10 KiB
C#
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
|
|
{
|
|
/// <summary>
|
|
/// Moves projectile on defined <see cref="ProjectileStep"/>
|
|
/// </summary>
|
|
[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<ProjectileObject> onStart = new();
|
|
public UnityEvent<ProjectileObject, AbstractObject> onHit = new();
|
|
public UnityEvent<ProjectileObject> onNextStep = new();
|
|
public UnityEvent<ProjectileObject> 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<AbstractObject, int> _aoeCollisions = new();
|
|
|
|
private void Awake()
|
|
{
|
|
gameObject.SetActive(false);
|
|
}
|
|
|
|
private void FixedUpdate()
|
|
{
|
|
if (MoveProjectile() || CheckCollision()) StartCoroutine(NextStep());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Moves projectile by one step in trajectory.
|
|
/// </summary>
|
|
/// <returns>TRUE if current step reached end</returns>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if projectile should end early because it hit something.
|
|
/// </summary>
|
|
/// <returns>TRUE if collision ends this projectile step</returns>
|
|
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<UnitObject>() != caster);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Starts next step of projectile. If no more steps available then whole projectile is destroyed.
|
|
/// </summary>
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 1. Clear temporary data<br/>
|
|
/// 2. Initialize next Trajectory<br/>
|
|
/// 3. Initialize next Area Of Effect
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns start position for current step by defined <see cref="ProjectileStepStartFrom"/>.
|
|
/// </summary>
|
|
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)
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes and starts whole projectile.
|
|
/// </summary>
|
|
/// <param name="index">Index of this projectile from source ability</param>
|
|
/// <param name="caster">UnitObject that fired this projectile</param>
|
|
/// <param name="startPosition">Projectile starting position</param>
|
|
/// <param name="targetPosition">Projectile target position</param>
|
|
/// <param name="steps">Definition HOW projectile should move and hit</param>
|
|
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>();
|
|
projectileAoe.OnAoeEnter += OnAoeEnter;
|
|
projectileAoe.OnAoeExit += OnAoeExit;
|
|
|
|
onStart.Invoke(this);
|
|
|
|
PrepareNextStep();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ends and destroy projectile.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checking if given target should be hit and executed OnHit.
|
|
/// </summary>
|
|
private bool IsValidHit(UnitObject caster, AbstractObject target)
|
|
{
|
|
return _currentStep.validHits.Count == 0 ||
|
|
_currentStep.validHits.Any(validator => validator.IsValid(caster, target));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Collision enter detected by projectile AOE.
|
|
/// </summary>
|
|
private void OnAoeEnter(Collider other)
|
|
{
|
|
if (!LayerManager.CodeInMask(other.gameObject.layer, LayerManager.Dynamic)) return;
|
|
|
|
var hitObject = other.GetComponentInParent<AbstractObject>();
|
|
if (!hitObject) return;
|
|
|
|
if (!IsValidHit(caster, hitObject)) return;
|
|
|
|
_aoeCollisions.TryAdd(hitObject, 0);
|
|
_aoeCollisions[hitObject]++;
|
|
if (_currentStep.hitType == ProjectileHitType.Always) onHit.Invoke(this, hitObject);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Collision exit detected by projectile AOE.
|
|
/// </summary>
|
|
private void OnAoeExit(Collider other)
|
|
{
|
|
if (!LayerManager.CodeInMask(other.gameObject.layer, LayerManager.Dynamic)) return;
|
|
|
|
var hitObject = other.GetComponentInParent<AbstractObject>();
|
|
if (!hitObject) return;
|
|
|
|
if (!_aoeCollisions.ContainsKey(hitObject)) return;
|
|
|
|
_aoeCollisions[hitObject]--;
|
|
|
|
if (_aoeCollisions[hitObject] == 0) _aoeCollisions.Remove(hitObject);
|
|
}
|
|
}
|
|
} |