Files
VOIDRPG/Assets/90 - Scripts/Generic/Projectile/ProjectileObject.cs
T
2026-07-09 21:33:33 +02:00

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);
}
}
}