You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

294 lines
6.4 KiB
C#

using System;
using UnityEngine;
using System.Collections.Generic;
using UnityEasing;
namespace Projectiles
{
using Random = UnityEngine.Random;
[Serializable]
public class ShakeSetup
{
public float Duration = 0.5f;
public float Magnitude = 0.05f;
public float Frequency = 10f;
public float FadeIn = 0.1f;
public float FadeOut = 0.2f;
public Ease Ease = Ease.Linear;
public Vector3 Axis = new(1f, 1f, 1f);
public EShakeTarget Target = EShakeTarget.Position;
}
public enum EShakeTarget
{
None,
Position,
Rotation,
}
public enum EShakeForce
{
None,
ReplaceSame,
Add,
}
public class ShakeEffect : CoreBehaviour
{
// PUBLIC MEMBERS
public bool IsPlaying => _activeShakes.Count > 0f;
// PRIVATE MEMBERS
[SerializeField]
private ShakeSetup _defaultSetup;
private List<ShakeData> _activeShakes = new(32);
private Vector3 _defaultPosition;
private Quaternion _defaultRotation;
// PUBLIC MEMBERS
public void Play(ShakeSetup setup, EShakeForce force = EShakeForce.Add)
{
if (setup == null || setup.Target == EShakeTarget.None || setup.Duration <= 0f || setup.Magnitude <= 0f)
return;
if (IsPlaying == false || force == EShakeForce.Add)
{
AddShake(setup);
}
else if (force == EShakeForce.ReplaceSame)
{
for (int i = 0; i < _activeShakes.Count; i++)
{
var shake = _activeShakes[i];
if (shake.Setup == setup)
{
shake.Cooldown = Mathf.Max(setup.Duration - setup.FadeIn, shake.Cooldown);
return;
}
}
AddShake(setup);
}
}
public void Play(EShakeForce force = EShakeForce.Add)
{
Play(_defaultSetup, force);
}
public void Stop(ShakeSetup setup, bool immediate = false)
{
if (IsPlaying == false)
return;
for (int i = 0; i < _activeShakes.Count; i++)
{
var shake = _activeShakes[i];
if (shake.Setup != setup)
continue;
if (immediate == true || shake.Setup.FadeOut <= 0f)
{
RemoveShake(i);
return;
}
shake.Cooldown = Mathf.Min(shake.Cooldown, shake.Setup.FadeOut);
}
}
public void Stop(bool immediate = false)
{
if (IsPlaying == false)
return;
for (int i = _activeShakes.Count - 1; i >= 0; i--)
{
Stop(_activeShakes[i].Setup, immediate);
}
}
// MONOBEHAVIOUR
protected void Awake()
{
_defaultPosition = transform.localPosition;
_defaultRotation = transform.localRotation;
}
protected void Update()
{
if (IsPlaying == false)
{
transform.localPosition = _defaultPosition;
transform.localRotation = _defaultRotation;
return;
}
var positionOffset = Vector3.zero;
var rotationOffset = Vector3.zero;
for (int i = 0; i < _activeShakes.Count; i++)
{
var shake = _activeShakes[i];
if (shake.Setup.Target == EShakeTarget.Position)
{
positionOffset += shake.GetOffset(Time.deltaTime);
}
else
{
rotationOffset += shake.GetOffset(Time.deltaTime);
}
}
transform.localPosition = _defaultPosition + positionOffset;
transform.localRotation = _defaultRotation * Quaternion.Euler(rotationOffset);
for (int i = _activeShakes.Count - 1; i >= 0; i--)
{
if (_activeShakes[i].IsFinished == true)
{
RemoveShake(i);
}
}
}
// PRIVATE METHODS
private void AddShake(ShakeSetup setup)
{
var shake = Pool.Get<ShakeData>();
shake.Reset(setup);
_activeShakes.Add(shake);
}
private void RemoveShake(int index)
{
var shakeData = _activeShakes[index];
_activeShakes.RemoveAt(index);
Pool.Return(shakeData);
}
// HELPERS
private class ShakeData
{
public ShakeSetup Setup;
public float Cooldown;
public bool IsFinished => Cooldown <= 0f;
[NonSerialized]
private float _elapsedTime;
[NonSerialized]
private float _normalChangeDuration;
[NonSerialized]
private float _changeDuration;
[NonSerialized]
private Vector3 _startPosition;
[NonSerialized]
private Vector3 _targetPosition;
[NonSerialized]
private float _changeCooldown;
[NonSerialized]
private Vector3 _lastOffset;
private float _changeDurationMultiplier;
public void Reset(ShakeSetup setup)
{
Setup = setup;
Cooldown = setup.Duration;
_elapsedTime = 0f;
_normalChangeDuration = 1f / setup.Frequency;
_changeDuration = _normalChangeDuration;
_changeCooldown = 0f;
_startPosition = Vector3.zero;
_targetPosition = Vector3.zero;
_lastOffset = Vector3.zero;
}
public Vector3 GetOffset(float deltaTime)
{
bool isStart = _elapsedTime == 0f;
bool wasEnd = Cooldown <= _normalChangeDuration * 0.5f;
_elapsedTime += deltaTime;
Cooldown -= deltaTime;
_changeCooldown -= deltaTime;
bool isEnd = wasEnd == false && Cooldown <= _normalChangeDuration * 0.5f;
if (_changeCooldown <= 0f || isEnd == true)
{
float magnitudeProgress = 1f;
if (Setup.FadeIn > 0f && _elapsedTime < Setup.FadeIn)
{
magnitudeProgress = _elapsedTime / Setup.FadeIn;
}
else if (Setup.FadeOut > 0f && Cooldown < Setup.FadeOut)
{
magnitudeProgress = Cooldown / Setup.FadeOut;
}
float magnitude = Setup.Magnitude * magnitudeProgress;
// Recalculate change duration in case the frequency changed
_normalChangeDuration = 1f / Setup.Frequency;
if (isEnd == true)
{
_startPosition = _lastOffset;
_targetPosition = Vector3.zero;
_changeDuration = Cooldown + Time.deltaTime;
_changeCooldown = Cooldown;
}
else if (isStart == true)
{
_startPosition = Vector3.zero;
_targetPosition = Vector3.Scale(Random.onUnitSphere, Setup.Axis).normalized * magnitude;
// We are covering only half of the shake distance on start
_changeDuration = _normalChangeDuration * 0.5f;
_changeCooldown += _changeDuration;
}
else
{
_startPosition = _targetPosition;
var randomRotation = Quaternion.Euler(Random.Range(-60, 60), Random.Range(-60, 60), Random.Range(-60, 60));
_targetPosition = Vector3.Scale(randomRotation * -_targetPosition, Setup.Axis).normalized * magnitude;
_changeDuration = _normalChangeDuration;
_changeCooldown += _changeDuration;
}
}
float progress = 1 - _changeCooldown / _changeDuration;
_lastOffset = Vector3.Lerp(_startPosition, _targetPosition, Setup.Ease.Get(progress));
return _lastOffset;
}
}
}
}