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