using UnityEasing; using UnityEngine; namespace Projectiles { /// /// Base class for all kinematic projectile types. /// public abstract class KinematicProjectile : ProjectileBase, IBufferView { // PUBLIC MEMBERS public bool IsFinished { get; protected set; } // PROTECTED MEMBERS [SerializeField] protected float _startSpeed = 40f; [SerializeField, Tooltip("Projectile length improves hitting moving targets")] protected float _length = 0f; // PRIVATE MEMBERS [SerializeField] private float _maxDistance = 200f; [SerializeField] private float _maxTime = 5f; [SerializeField, Tooltip("Time for interpolation between barrel position and actual fire path of the projectile")] private float _interpolationDuration = 0.3f; [SerializeField] private Ease _interpolationEase = Ease.OutSine; private Vector3 _startOffset; private float _interpolationTime; protected int _lifetimeTicks = -1; // PUBLIC METHODS public virtual KinematicData GetFireData(Vector3 firePosition, Vector3 fireDirection) { if (_lifetimeTicks < 0) { int maxDistanceTicks = Mathf.RoundToInt((_maxDistance / _startSpeed) * Context.Runner.TickRate); int maxTimeTicks = Mathf.RoundToInt(_maxTime * Context.Runner.TickRate); // GetFireData is called on prefab directly, but it is safe to save // the value here as it does not change for different instances _lifetimeTicks = maxDistanceTicks > 0 && maxTimeTicks > 0 ? Mathf.Min(maxDistanceTicks, maxTimeTicks) : (maxDistanceTicks > 0 ? maxDistanceTicks : maxTimeTicks); } return new KinematicData() { Position = firePosition, Velocity = fireDirection * _startSpeed, }; } public virtual void OnFixedUpdate(ref KinematicData data) { if (Context.Runner.Tick >= data.FireTick + _lifetimeTicks) { data.IsFinished = true; } } public virtual void Activate(ref KinematicData data) { var startPosition = Context.BarrelTransforms[data.BarrelIndex].position; // Kinematic projectile visual starts at the barrel position and is slowly // interpolated to its actual path that starts directly from camera. transform.position = startPosition; transform.rotation = Quaternion.LookRotation(data.Velocity); _startOffset = startPosition - data.Position; _interpolationTime = 0f; IsFinished = false; } public virtual void Deactivate() { } // IBufferView INTERFACE public virtual void Render(ref KinematicData data, ref KinematicData fromData, float alpha) { if (data.IsFinished == true) { SpawnImpactVisual(data.ImpactPosition, data.ImpactNormal); IsFinished = true; return; } var targetPosition = GetRenderPosition(ref data, ref fromData, alpha); float interpolationProgress = 0f; if (targetPosition != (Vector3)data.Position) { // Do not start interpolation until projectile should actually move _interpolationTime += Time.deltaTime; interpolationProgress = Mathf.Clamp01(_interpolationTime / _interpolationDuration); } var offset = Vector3.Lerp(_startOffset, Vector3.zero, _interpolationEase.Get(interpolationProgress)); var previousPosition = transform.position; var nextPosition = targetPosition + offset; var direction = nextPosition - previousPosition; transform.position = nextPosition; if (direction != Vector3.zero) { transform.rotation = Quaternion.LookRotation(direction); } } // PROTECTED METHODS protected abstract Vector3 GetRenderPosition(ref KinematicData data, ref KinematicData fromData, float alpha); } }