Files
2026-07-09 21:33:33 +02:00

191 lines
8.1 KiB
C#

using System;
using System.Linq;
using UnityEngine;
using UnityEngine.Rendering;
using VOID.Generic.AreaOfEffect;
using VOID.Generic.Objects.Unit;
using VOID.Generic.Projectile;
using VOID.Generic.Trajectory;
namespace VOID.Generic.Player
{
/// <summary>
/// Visualizes <see cref="ProjectileObject"/> by displaying <see cref="AbstractTrajectory"/> and <see cref="AbstractAreaOfEffect"/>.<br/>
/// TODO: So far only first step of projectile is supported!
/// </summary>
[DefaultExecutionOrder(-1)]
public class ProjectileVisualizer : MonoBehaviour
{
private UnitObject _caster;
private ProjectileStep _projectileStep;
private AbstractTrajectory _trajectory;
private Transform _transformStart;
private Transform _transformEnd;
private Vector3 _positionStart;
private Vector3 _positionEnd;
private Vector3 _lastPositionStart;
private Vector3 _lastPositionEnd;
private Vector3 _collisionOnPosition;
private LineRenderer _lineRenderer;
private MeshFilter _meshFilter;
private MeshRenderer _meshRenderer;
private GameObject _visualizerGameObject;
private void Awake() => enabled = false;
/// <summary>
/// Initializes and starts projectile visualizing.
/// </summary>
/// <param name="caster">UnitObject who is casting this projectile</param>
/// <param name="projectileSteps">Definition how projectile will move and hit</param>
/// <param name="trajectoryMaterial">Material to draw Trajectory</param>
/// <param name="areaOfEffectMaterial">Material to draw AreaOfEffect</param>
/// <param name="transformEnd">Target position for projectile</param>
public void Init(
UnitObject caster,
ProjectileStep[] projectileSteps,
Material trajectoryMaterial,
Material areaOfEffectMaterial,
Transform transformEnd
)
{
// TODO: So far only first step of projectile is supported!
_projectileStep = projectileSteps.FirstOrDefault();
if (_projectileStep == default) return;
_caster = caster;
_trajectory = _projectileStep.trajectory.Clone();
_transformStart = FindStartTransform(_projectileStep);
_transformEnd = transformEnd;
// GameObject containing meshes of Trajectory and AreaOfEffect
_visualizerGameObject = new GameObject("Projectile Visualizer");
_visualizerGameObject.transform.parent = gameObject.transform;
_visualizerGameObject.transform.rotation =
_projectileStep.areaOfEffect.GetRotation(_transformStart.position, _transformEnd.position);
// Trajectory visualizing
if (_trajectory is not InstantTrajectory)
{
_lineRenderer = _visualizerGameObject.AddComponent<LineRenderer>();
_lineRenderer.enabled = false;
_lineRenderer.startWidth = _projectileStep.radius;
_lineRenderer.endWidth = _projectileStep.radius;
_lineRenderer.shadowCastingMode = ShadowCastingMode.Off;
_lineRenderer.sharedMaterial = trajectoryMaterial;
}
// AreaOfEffect visualizing
if (_projectileStep.areaOfEffect is not NoneAreaOfEffect)
{
_meshFilter = _visualizerGameObject.AddComponent<MeshFilter>();
_meshFilter.mesh = _projectileStep.areaOfEffect.GetMesh();
_meshRenderer = _visualizerGameObject.AddComponent<MeshRenderer>();
_meshRenderer.enabled = false;
_meshRenderer.shadowCastingMode = ShadowCastingMode.Off;
_meshRenderer.sharedMaterial = areaOfEffectMaterial;
}
enabled = true;
}
private void FixedUpdate()
{
_positionStart = _transformStart.position;
_positionEnd = _transformEnd.position;
// If positions didn't change then there is no need to recalculate whole Trajectory and AreaOfEffect
if (_positionStart == _lastPositionStart && _positionEnd == _lastPositionEnd) return;
_lastPositionStart = _positionStart;
_lastPositionEnd = _positionEnd;
UpdateTrajectory();
UpdateAreaOfEffect();
}
/// <summary>
/// Updates whole <see cref="AbstractTrajectory"/>
/// </summary>
private void UpdateTrajectory()
{
// Its instant, that means there is no trajectory
if (_trajectory is InstantTrajectory) return;
_trajectory.Init(_positionStart, _positionEnd);
var pointsNeeded = Mathf.FloorToInt(_trajectory.trajectoryDistance / _projectileStep.speed / Time.fixedDeltaTime);
var points = _trajectory.GetPositions(pointsNeeded);
// Predict collision on trajectory
points = GetPointsToFirstCollision(points);
_lineRenderer.positionCount = points.Length;
_lineRenderer.SetPositions(points);
_lineRenderer.enabled = true;
}
/// <summary>
/// If <see cref="ProjectileCollisionType"/> specified then try to detect collision on trajectory.
/// </summary>
private Vector3[] GetPointsToFirstCollision(Vector3[] points)
{
int layerMask;
switch (_projectileStep.collisionType)
{
case ProjectileCollisionType.StaticOrDynamicCollision:
layerMask = LayerManager.Static | LayerManager.StaticHideable | LayerManager.Dynamic;
break;
case ProjectileCollisionType.StaticCollision:
layerMask = LayerManager.Static | LayerManager.StaticHideable;
break;
default:
return points;
}
// To prevent hitting caster and ground under caster ignore everything in caster's radius
var skip = 0;
if (_projectileStep.startFrom is ProjectileStepStartFrom.FromCaster)
skip = Array.FindIndex(points, point => Vector3.Distance(point, _positionStart) >= _caster.basicData.radius);
if (skip == - 1) skip = 0;
// Check if projectile will hit anything on its trajeactory (also prevent hitting caster)
var collisionIndex = Array.FindIndex(points, skip, point =>
Physics.OverlapSphere(point, _projectileStep.radius, layerMask)
.Any(collider => collider.GetComponentInParent<UnitObject>() != _caster));
if (collisionIndex == - 1) collisionIndex = points.Length - 1;
_collisionOnPosition = points[collisionIndex];
return points.Take(collisionIndex + 1).ToArray();
}
/// <summary>
/// Updates whole <see cref="AbstractAreaOfEffect"/>
/// </summary>
private void UpdateAreaOfEffect()
{
if (_projectileStep.areaOfEffect is NoneAreaOfEffect) return;
_visualizerGameObject.transform.position = _collisionOnPosition != default ? _collisionOnPosition : _positionEnd;
_visualizerGameObject.transform.rotation = _projectileStep.areaOfEffect.GetRotation(_positionStart, _positionEnd);
_meshRenderer.enabled = true;
}
/// <summary>
/// Returns start position for current step by defined <see cref="ProjectileStepStartFrom"/>.
/// </summary>
private Transform FindStartTransform(ProjectileStep step)
{
return step.startFrom switch
{
ProjectileStepStartFrom.FromStart => _caster.unitData.castPosition,
ProjectileStepStartFrom.FromCaster => _caster.transform,
ProjectileStepStartFrom.FromHit => _caster.unitData.castPosition,
_ => throw new ArgumentOutOfRangeException(
nameof(step.startFrom), step.startFrom, null)
};
}
}
}