// ReSharper disable UnusedMember.Global
// ReSharper disable MemberCanBePrivate.Global
// ReSharper disable UnusedMethodReturnValue.Global
using System;
using Random = System.Random;
using SuppressMessage = System.Diagnostics.CodeAnalysis.SuppressMessageAttribute;
using AnimationCurve = UnityEngine.AnimationCurve;
using Transform = UnityEngine.Transform;
using Camera = UnityEngine.Camera;
using Vector3 = UnityEngine.Vector3;
using Quaternion = UnityEngine.Quaternion;
using NotNull = JetBrains.Annotations.NotNullAttribute;
using Debug = UnityEngine.Debug;
using Object = UnityEngine.Object;
using TweenType = PrimeTween.TweenAnimation.TweenType;
namespace PrimeTween {
public partial struct Tween {
/// Shakes the camera.
/// If the camera is perspective, shakes all angles.
/// If the camera is orthographic, shakes the z angle and x/y coordinates.
/// Reference strengthFactor values - light: 0.2, medium: 0.5, heavy: 1.0.
public static Sequence ShakeCamera([NotNull] Camera camera, float strengthFactor, float duration = 0.5f, float frequency = ShakeSettings.defaultFrequency, float startDelay = 0, float endDelay = 0, bool useUnscaledTime = PrimeTweenConfig.defaultUseUnscaledTimeForShakes) {
var transform = camera.transform;
if (camera.orthographic) {
float orthoPosStrength = strengthFactor * camera.orthographicSize * 0.03f;
return Sequence.Create()
.Group(ShakeLocalPosition(transform, new ShakeSettings(new Vector3(orthoPosStrength, orthoPosStrength), duration, frequency, startDelay: startDelay, endDelay: endDelay, useUnscaledTime: useUnscaledTime)))
.Group(ShakeLocalRotation(transform, new ShakeSettings(new Vector3(0, 0, strengthFactor * 0.6f), duration, frequency, startDelay: startDelay, endDelay: endDelay, useUnscaledTime: useUnscaledTime)));
}
return Sequence.Create()
.Group(ShakeLocalRotation(transform, new ShakeSettings(strengthFactor * Vector3.one, duration, frequency, startDelay: startDelay, endDelay: endDelay, useUnscaledTime: useUnscaledTime)));
}
public static Tween ShakeLocalPosition([NotNull] Transform target, Vector3 strength, float duration, float frequency = ShakeSettings.defaultFrequency, bool enableFalloff = true, Ease easeBetweenShakes = Ease.Default, float asymmetryFactor = 0f, int cycles = 1,
float startDelay = 0, float endDelay = 0, bool useUnscaledTime = PrimeTweenConfig.defaultUseUnscaledTimeForShakes)
=> ShakeLocalPosition(target, new ShakeSettings(strength, duration, frequency, enableFalloff, easeBetweenShakes, asymmetryFactor, cycles, startDelay, endDelay, useUnscaledTime));
public static Tween ShakeLocalPosition([NotNull] Transform target, ShakeSettings settings)
=> ShakeTransform(TweenType.ShakeLocalPosition, target, settings);
public static Tween PunchLocalPosition([NotNull] Transform target, Vector3 strength, float duration, float frequency = ShakeSettings.defaultFrequency, bool enableFalloff = true, Ease easeBetweenShakes = Ease.Default, float asymmetryFactor = 0f, int cycles = 1,
float startDelay = 0, float endDelay = 0, bool useUnscaledTime = PrimeTweenConfig.defaultUseUnscaledTimeForShakes)
=> PunchLocalPosition(target, new ShakeSettings(strength, duration, frequency, enableFalloff, easeBetweenShakes, asymmetryFactor, cycles, startDelay, endDelay, useUnscaledTime));
public static Tween PunchLocalPosition([NotNull] Transform target, ShakeSettings settings) => ShakeLocalPosition(target, settings.WithPunch());
public static Tween ShakeLocalRotation([NotNull] Transform target, Vector3 strength, float duration, float frequency = ShakeSettings.defaultFrequency, bool enableFalloff = true, Ease easeBetweenShakes = Ease.Default, float asymmetryFactor = 0f, int cycles = 1,
float startDelay = 0, float endDelay = 0, bool useUnscaledTime = PrimeTweenConfig.defaultUseUnscaledTimeForShakes)
=> ShakeLocalRotation(target, new ShakeSettings(strength, duration, frequency, enableFalloff, easeBetweenShakes, asymmetryFactor, cycles, startDelay, endDelay, useUnscaledTime));
public static Tween ShakeLocalRotation([NotNull] Transform target, ShakeSettings settings)
=> ShakeTransform(TweenType.ShakeLocalRotation, target, settings);
public static Tween PunchLocalRotation([NotNull] Transform target, Vector3 strength, float duration, float frequency = ShakeSettings.defaultFrequency, bool enableFalloff = true, Ease easeBetweenShakes = Ease.Default, float asymmetryFactor = 0f, int cycles = 1,
float startDelay = 0, float endDelay = 0, bool useUnscaledTime = PrimeTweenConfig.defaultUseUnscaledTimeForShakes)
=> PunchLocalRotation(target, new ShakeSettings(strength, duration, frequency, enableFalloff, easeBetweenShakes, asymmetryFactor, cycles, startDelay, endDelay, useUnscaledTime));
public static Tween PunchLocalRotation([NotNull] Transform target, ShakeSettings settings) => ShakeLocalRotation(target, settings.WithPunch());
public static Tween ShakeScale([NotNull] Transform target, Vector3 strength, float duration, float frequency = ShakeSettings.defaultFrequency, bool enableFalloff = true, Ease easeBetweenShakes = Ease.Default, float asymmetryFactor = 0f, int cycles = 1,
float startDelay = 0, float endDelay = 0, bool useUnscaledTime = PrimeTweenConfig.defaultUseUnscaledTimeForShakes)
=> ShakeScale(target, new ShakeSettings(strength, duration, frequency, enableFalloff, easeBetweenShakes, asymmetryFactor, cycles, startDelay, endDelay, useUnscaledTime));
public static Tween ShakeScale([NotNull] Transform target, ShakeSettings settings)
=> ShakeTransform(TweenType.ShakeScale, target, settings);
public static Tween PunchScale([NotNull] Transform target, Vector3 strength, float duration, float frequency = ShakeSettings.defaultFrequency, bool enableFalloff = true, Ease easeBetweenShakes = Ease.Default, float asymmetryFactor = 0f, int cycles = 1,
float startDelay = 0, float endDelay = 0, bool useUnscaledTime = PrimeTweenConfig.defaultUseUnscaledTimeForShakes)
=> PunchScale(target, new ShakeSettings(strength, duration, frequency, enableFalloff, easeBetweenShakes, asymmetryFactor, cycles, startDelay, endDelay, useUnscaledTime));
public static Tween PunchScale([NotNull] Transform target, ShakeSettings settings) => ShakeScale(target, settings.WithPunch());
static Tween ShakeTransform(TweenType tweenType, [NotNull] Transform target, ShakeSettings settings) {
if (PrimeTweenManager.Instance.isDestroyed) {
return default;
}
var tween = PrimeTweenManager.FetchTween(settings._updateType);
ref var rt = ref tween.managedData;
ref var d = ref tween.data;
prepareShakeData(settings, ref rt, ref d, target);
var tweenSettings = settings.tweenSettings;
tween.Setup(target, ref tweenSettings, true, tweenType, ref rt, ref d);
return PrimeTweenManager.Animate(ref rt, ref d);
}
public static Tween ShakeCustom([NotNull] T target, Vector3 startValue, ShakeSettings settings, [NotNull] Action onValueChange) where T : class {
Assert.IsNotNull(onValueChange);
if (PrimeTweenManager.Instance.isDestroyed) {
return default;
}
var tween = PrimeTweenManager.FetchTween(settings._updateType);
ref var rt = ref tween.managedData;
ref var d = ref tween.data;
d.startValue.CopyFrom(ref startValue);
prepareShakeData(settings, ref rt, ref d, target);
tween.customOnValueChange = onValueChange;
var tweenSettings = settings.tweenSettings;
tween.Setup(target, ref tweenSettings, false, TweenType.ShakeCustom, ref rt, ref d);
tween.onValueChange = (ref TweenData rt2, ref UnmanagedTweenData d2) => {
var _onValueChange = rt2.cold.customOnValueChange as Action;
Assert.IsNotNull(_onValueChange);
var val = d2.startValue.vector3 + getShakeVal(ref rt2, ref d2);
_onValueChange(rt2.target as T, val);
};
return PrimeTweenManager.Animate(ref rt, ref d);
}
public static Tween PunchCustom([NotNull] T target, Vector3 startValue, ShakeSettings settings, [NotNull] Action onValueChange) where T : class => ShakeCustom(target, startValue, settings.WithPunch(), onValueChange);
static void prepareShakeData(ShakeSettings settings, ref TweenData rt, ref UnmanagedTweenData d, object target) {
rt.endValueOrDiff.Reset(); // not used
rt.cold.shakeData.Setup(settings, ref rt, ref d, target);
}
internal static Vector3 getShakeVal(ref TweenData rt, ref UnmanagedTweenData d) {
float fadeInOutFactor = calcFadeInOutFactor(ref rt, ref d);
return rt.shakeData.getNextVal(ref rt, ref d) * fadeInOutFactor;
}
static float calcFadeInOutFactor(ref TweenData tween, ref UnmanagedTweenData d) {
float animationDuration = d.animationDuration;
var elapsedTimeInterpolating = d.easedInterpolationFactor * animationDuration;
Assert.IsTrue(elapsedTimeInterpolating >= 0f);
if (animationDuration == 0f) {
return 0f;
}
Assert.IsTrue(animationDuration > 0f);
float halfDuration = animationDuration * 0.5f;
var oneShakeDuration = 1f / tween.cold.shakeData.frequency;
if (oneShakeDuration > halfDuration) {
oneShakeDuration = halfDuration;
}
float fadeInDuration = oneShakeDuration * 0.5f;
if (elapsedTimeInterpolating < fadeInDuration) {
return Mathf.InverseLerp(0f, fadeInDuration, elapsedTimeInterpolating);
}
var fadeoutStartTime = animationDuration - oneShakeDuration;
Assert.IsTrue(fadeoutStartTime > 0f, tween.cold.id);
if (elapsedTimeInterpolating > fadeoutStartTime) {
return Mathf.InverseLerp(animationDuration, fadeoutStartTime, elapsedTimeInterpolating);
}
return 1f;
}
}
#if PRIME_TWEEN_INSPECTOR_DEBUGGING && UNITY_EDITOR
[Serializable]
#endif
internal struct ShakeData {
float t;
Vector3 from, to;
float symmetryFactor;
int falloffEaseInt;
AnimationCurve customStrengthOverTime;
Ease easeBetweenShakes;
internal Vector3 strengthPerAxis { get; private set; }
internal float frequency { get; private set; }
float prevInterpolationFactor;
int prevCyclesDone;
const int disabledFalloff = -42;
internal bool isAlive => frequency != 0f;
internal void Setup(ShakeSettings settings, ref TweenData rt, ref UnmanagedTweenData d, object target) {
d.isPunch = settings.isPunch;
symmetryFactor = Mathf.Clamp01(1 - settings.asymmetry);
{
var _strength = settings.strength;
if (_strength == default) {
Debug.LogError("Shake's strength is (0, 0, 0).");
}
strengthPerAxis = _strength;
}
{
var _frequency = settings.frequency;
if (_frequency <= 0) {
Debug.LogError($"Shake's frequency should be > 0f, but was {_frequency}.", target as Object);
_frequency = ShakeSettings.defaultFrequency;
}
frequency = _frequency;
}
{
if (settings.enableFalloff) {
var _falloffEase = settings.falloffEase;
var _customStrengthOverTime = settings.strengthOverTime;
if (_falloffEase == Ease.Default) {
_falloffEase = Ease.Linear;
}
if (_falloffEase == Ease.Custom) {
if (_customStrengthOverTime == null || !TweenSettings.ValidateCustomCurve(_customStrengthOverTime)) {
Debug.LogError($"Shake falloff is Ease.Custom, but {nameof(ShakeSettings.strengthOverTime)} is not configured correctly. Using Ease.Linear instead.", target as Object);
_falloffEase = Ease.Linear;
}
}
falloffEaseInt = (int)_falloffEase;
customStrengthOverTime = _customStrengthOverTime;
} else {
falloffEaseInt = disabledFalloff;
}
}
{
var _easeBetweenShakes = settings.easeBetweenShakes;
if (_easeBetweenShakes == Ease.Custom) {
Debug.LogError($"{nameof(ShakeSettings.easeBetweenShakes)} doesn't support Ease.Custom.", target as Object);
_easeBetweenShakes = Ease.OutQuad;
}
if (_easeBetweenShakes == Ease.Default) {
_easeBetweenShakes = PrimeTweenManager.defaultShakeEase;
}
easeBetweenShakes = _easeBetweenShakes;
}
onCycleComplete(ref rt, ref d);
}
internal void onCycleComplete(ref TweenData rt, ref UnmanagedTweenData d) {
Assert.IsTrue(isAlive);
resetAfterCycle();
d.shakeSign = d.isPunch || PrimeTweenManager.random.NextDouble() < 0.5;
to = generateShakePoint(ref d);
}
static int getMainAxisIndex(Vector3 strengthByAxis) {
int mainAxisIndex = -1;
float maxStrength = float.NegativeInfinity;
for (int i = 0; i < 3; i++) {
var strength = Mathf.Abs(strengthByAxis[i]);
if (strength > maxStrength) {
maxStrength = strength;
mainAxisIndex = i;
}
}
Assert.IsTrue(mainAxisIndex >= 0);
return mainAxisIndex;
}
internal Vector3 getNextVal(ref TweenData rt, ref UnmanagedTweenData d) {
var interpolationFactor = d.easedInterpolationFactor;
Assert.IsTrue(interpolationFactor <= 1);
int cyclesDiff = d.getCyclesDone() - prevCyclesDone;
prevCyclesDone = d.getCyclesDone();
if (interpolationFactor == 0f || (cyclesDiff > 0 && d.getCyclesDone() != d.cyclesTotal)) {
onCycleComplete(ref rt, ref d);
prevInterpolationFactor = interpolationFactor;
}
float animationDuration = d.animationDuration;
var dt = (interpolationFactor - prevInterpolationFactor) * animationDuration;
prevInterpolationFactor = interpolationFactor;
var strengthOverTime = calcStrengthOverTime(interpolationFactor);
var frequencyFactor = Mathf.Clamp01(strengthOverTime * 3f); // handpicked formula that describes the relationship between strength and frequency
// The initial velocity should twice as big because the first shake starts from zero (twice as short as total range).
var elapsedTimeInterpolating = d.easedInterpolationFactor * animationDuration;
var halfShakeDuration = 0.5f / rt.shakeData.frequency;
float iniVelFactor = elapsedTimeInterpolating < halfShakeDuration ? 2f : 1f;
t += frequency * dt * frequencyFactor * iniVelFactor;
if (t < 0f || t >= 1f) {
d.shakeSign = !d.shakeSign;
if (t < 0f) {
t = 1f;
to = from;
from = generateShakePoint(ref d);
} else {
t = 0f;
from = to;
to = generateShakePoint(ref d);
}
}
Vector3 result = default;
for (int i = 0; i < 3; i++) {
result[i] = Mathf.Lerp(from[i], to[i], StandardEasing.Evaluate(t, easeBetweenShakes)) * strengthOverTime;
}
return result;
}
Vector3 generateShakePoint(ref UnmanagedTweenData d) {
var mainAxisIndex = getMainAxisIndex(strengthPerAxis);
Vector3 result = default;
float signFloat = d.shakeSign ? 1f : -1f;
for (int i = 0; i < 3; i++) {
var strength = strengthPerAxis[i];
if (d.isPunch) {
result[i] = clampBySymmetryFactor(strength * signFloat, strength, symmetryFactor);
} else {
result[i] = i == mainAxisIndex ? calcMainAxisEndVal(signFloat, strength, symmetryFactor) : calcNonMainAxisEndVal(strength, symmetryFactor);
}
}
return result;
}
float calcStrengthOverTime(float interpolationFactor) {
if (falloffEaseInt == disabledFalloff) {
return 1;
}
var falloffEase = (Ease)falloffEaseInt;
if (falloffEase != Ease.Custom) {
return 1 - StandardEasing.Evaluate(interpolationFactor, falloffEase);
}
Assert.IsNotNull(customStrengthOverTime);
return customStrengthOverTime.Evaluate(interpolationFactor);
}
static float calcMainAxisEndVal(float velocity, float strength, float symmetryFactor) {
float result = Mathf.Sign(velocity) * strength * RandomRange(0.6f, 1f); // doesn't matter if we're using strength or its abs because velocity alternates
return clampBySymmetryFactor(result, strength, symmetryFactor);
}
static float clampBySymmetryFactor(float val, float strength, float symmetryFactor) {
if (strength > 0) {
return Mathf.Clamp(val, -strength * symmetryFactor, strength);
}
return Mathf.Clamp(val, strength, -strength * symmetryFactor);
}
static float calcNonMainAxisEndVal(float strength, float symmetryFactor) {
if (strength > 0) {
return RandomRange(-strength * symmetryFactor, strength);
}
return RandomRange(strength, -strength * symmetryFactor);
}
static float RandomRange(float minInclusive, float max) {
double val = PrimeTweenManager.random.NextDouble();
return (float)(minInclusive + val * (max - minInclusive));
}
internal static bool TryTakeStartValueFromOtherShake(ref TweenData newTween, ref UnmanagedTweenData newTweenData) {
if (!newTween.shakeData.isAlive) {
return false;
}
var shakeTransform = newTween.target as Transform;
if (shakeTransform == null) {
return false;
}
var shakes = PrimeTweenManager.Instance.shakes;
var key = (shakeTransform, newTweenData.tweenType);
if (!shakes.TryGetValue(key, out var data)) {
var startValue = Utils.GetAnimatedValue(newTween.target, newTweenData.tweenType, newTween.cold.longParam);
shakes.Add(key, (startValue, 1));
return false;
}
Assert.IsTrue(data.count >= 1);
newTweenData.startValue = data.startValue;
// Debug.Log($"tryTakeStartValueFromOtherShake {data.startValue.vector3}");
data.count++;
shakes[key] = data;
return true;
}
internal void Reset(object target, TweenType tweenType) {
Assert.IsTrue(isAlive);
var shakeTransform = target as Transform;
if (shakeTransform != null) {
var key = (shakeTransform, tweenType);
var shakes = PrimeTweenManager.Instance.shakes;
if (shakes.TryGetValue(key, out var data)) {
// no key present if it's a ShakeCustom() with Transform target because custom shakes have startFromCurrent == false and aren't added to shakes dict
Assert.IsTrue(data.count >= 1);
data.count--;
if (data.count == 0) {
bool isRemoved = shakes.Remove(key);
Assert.IsTrue(isRemoved);
} else {
shakes[key] = data;
}
}
}
resetAfterCycle();
customStrengthOverTime = null;
frequency = 0f;
prevInterpolationFactor = 0f;
prevCyclesDone = 0;
Assert.IsFalse(isAlive);
}
void resetAfterCycle() {
t = 0f;
from = default;
}
}
}