// ReSharper disable CompareOfFloatsByEqualityOperator
#if PRIME_TWEEN_INSPECTOR_DEBUGGING && UNITY_EDITOR
#define ENABLE_SERIALIZATION
#endif
using System;
using JetBrains.Annotations;
using UnityEngine;
namespace PrimeTween {
/// The main API of the PrimeTween library.
/// Use static Tween methods to start animations (tweens).
/// Use the returned Tween struct to control the running tween and access its properties.
/// Tweens are non-reusable. That is, when a tween completes (or is stopped manually), it becomes 'dead' ( == false) and can no longer be used to control the tween or access its properties.
/// To restart the animation from the beginning (or play in the opposite direction), simply start a new Tween. Starting tweens is very fast and doesn't allocate garbage,
/// so you can start hundreds of tweens per second with no performance overhead.
///
/// var tween = Tween.LocalPositionX(transform, endValue: 1.5f, duration: 1f);
/// // Let the tween run for some time...
/// if (tween.isAlive) {
/// Debug.Log($"Animation is still running, elapsed time: {tween.elapsedTime}.");
/// } else {
/// Debug.Log("Animation is already completed.");
/// }
///
#if ENABLE_SERIALIZATION
[Serializable]
#endif
public
#if !ENABLE_SERIALIZATION
readonly
#endif
partial struct Tween : IEquatable {
/// Uniquely identifies the tween.
/// Can be observed from the Debug Inspector if PRIME_TWEEN_INSPECTOR_DEBUGGING is defined. Use only for debugging purposes.
internal
#if !ENABLE_SERIALIZATION
readonly
#endif
long id;
internal readonly ColdData tween;
internal bool IsCreated => id != 0;
internal Tween([NotNull] ColdData tween) {
Assert.IsNotNull(tween);
Assert.AreNotEqual(-1, tween.id);
id = tween.id;
this.tween = tween;
}
/// A tween is 'alive' when it has been created and has not stopped and has not completed yet. Paused tween is also considered 'alive'.
public bool isAlive => id != 0 && tween.id == id && tween.hasData && tween.data.isAlive;
/// Elapsed time of the current cycle.
public float elapsedTime {
get {
if (!ValidateIsAlive()) {
return 0;
}
if (cyclesDone == cyclesTotal) {
return duration;
}
float result = elapsedTimeTotal - duration * cyclesDone;
if (result < 0f) {
return 0f;
}
Assert.IsTrue(result >= 0f);
return result;
}
set => SetElapsedTime(value);
}
void SetElapsedTime(float value) {
if (!TryManipulate()) {
return;
}
if (value < 0f || float.IsNaN(value)) {
Debug.LogError($"Invalid elapsedTime value: {value}, tween: {ToString()}");
return;
}
var cycleDuration = duration;
if (value > cycleDuration) {
value = cycleDuration;
}
var _cyclesDone = cyclesDone;
if (_cyclesDone == cyclesTotal) {
_cyclesDone -= 1;
}
SetElapsedTimeTotal(value + cycleDuration * _cyclesDone);
}
/// The total number of cycles. Returns -1 to indicate an infinite number of cycles.
public int cyclesTotal => ValidateIsAlive() ? tween.data.cyclesTotal : 0;
public int cyclesDone => ValidateIsAlive() ? tween.data.getCyclesDone() : 0;
/// The duration of one cycle.
public float duration {
get {
if (!ValidateIsAlive()) {
return 0;
}
var result = tween.data.cycleDuration;
TweenSettings.validateFiniteDuration(ref result);
return result;
}
}
[NotNull]
public override string ToString() {
if (isAlive && tween.hasData) {
return tween.managedData.GetDescription();
} else {
return $"DEAD / id {id}";
}
}
/// Elapsed time of all cycles.
public float elapsedTimeTotal {
get {
if (!ValidateIsAlive()) {
return 0;
}
// ReSharper disable once CompareOfFloatsByEqualityOperator
if (tween.data.elapsedTimeTotal == float.MaxValue) {
return durationTotal;
}
return Mathf.Clamp(tween.data.elapsedTimeTotal - tween.data.waitDelay, 0f, durationTotal);
}
set => SetElapsedTimeTotal(value);
}
void SetElapsedTimeTotal(float value) {
if (!TryManipulate()) {
return;
}
if (value < 0f || float.IsNaN(value) || (cyclesTotal == -1 && value >= float.MaxValue)) { // >= tests for positive infinity, see SetInfiniteTweenElapsedTime() test
Debug.LogError($"Invalid elapsedTimeTotal value: {value}, tween: {ToString()}");
return;
}
ref var t = ref tween.managedData;
ref var d = ref tween.data;
t.SetElapsedTimeTotal(value, false, ref d);
// SetElapsedTimeTotal may complete the tween, so isAlive check is needed
if (d.isAlive) {
float durationTotalCached = durationTotal;
if (value > durationTotalCached) {
d.elapsedTimeTotal = durationTotalCached;
}
}
}
/// The duration of all cycles. If cycles == -1, returns .
public float durationTotal {
get {
if (!ValidateIsAlive()) {
return 0;
}
int cycles = tween.data.cyclesTotal;
if (cycles == -1) {
return float.PositiveInfinity;
}
Assert.AreNotEqual(0, cycles);
return tween.data.cycleDuration * cycles;
}
}
/// Normalized progress of the current cycle expressed in 0..1 range.
public float progress {
get {
if (!ValidateIsAlive()) {
return 0;
}
if (duration == 0) {
return GetProgressFromState();
}
return Mathf.Min(elapsedTime / duration, 1f);
}
set {
value = Mathf.Clamp01(value);
if (value == 1f) {
bool isLastCycle = cyclesDone == cyclesTotal - 1;
if (isLastCycle) {
SetElapsedTimeTotal(float.MaxValue);
return;
}
}
SetElapsedTime(value * duration);
}
}
/// Normalized progress of all cycles expressed in 0..1 range.
public float progressTotal {
get {
if (!ValidateIsAlive()) {
return 0;
}
if (cyclesTotal == -1) {
return 0;
}
var _totalDuration = durationTotal;
Assert.IsFalse(float.IsInfinity(_totalDuration));
if (_totalDuration == 0) {
return GetProgressFromState();
}
return Mathf.Min(elapsedTimeTotal / _totalDuration, 1f);
}
set {
if (!ValidateIsAlive()) {
return;
}
if (cyclesTotal == -1) {
Debug.LogError($"It's not allowed to set progressTotal on infinite tween (cyclesTotal == -1), tween: {ToString()}.");
return;
}
value = Mathf.Clamp01(value);
if (value == 1f) {
SetElapsedTimeTotal(float.MaxValue);
return;
}
SetElapsedTimeTotal(value * durationTotal);
}
}
float GetProgressFromState() => (tween.data.flags & Flags.StateAfter) != 0 ? 1f : 0f;
/// The current percentage of change between 'startValue' and 'endValue' values in 0..1 range.
public float interpolationFactor => ValidateIsAlive() ? Mathf.Max(0f, tween.data.easedInterpolationFactor) : 0f;
public bool isPaused {
get => TryManipulate() && tween.data.isPaused;
set {
if (TryManipulate()) {
ref var rt = ref tween.managedData;
ref var d = ref tween.data;
if (d.trySetPause(value)) {
if (value) {
return;
}
if ((timeScale > 0 && progressTotal >= 1f) ||
(timeScale < 0 && progressTotal == 0f)) {
if (d.IsMainSequenceRoot()) {
new Sequence(rt.cold.sequence).ReleaseTweens();
} else {
rt.Kill(ref d);
}
}
}
}
}
}
/// Interrupts the tween, ignoring onComplete callback.
public void Stop() {
if (isAlive && TryManipulate(false)) {
tween.managedData.Kill(ref tween.data);
}
}
/// Immediately completes the tween.
/// If the tween has infinite cycles (cycles == -1), completes only the current cycle. To choose between 'startValue' and 'endValue' in the case of infinite cycles, use before calling Complete().
public void Complete() {
// don't warn that a tween is dead because a dead tween means that it's already 'completed'
if (isAlive && TryManipulate(false)) {
tween.managedData.ForceComplete(ref tween.data);
}
}
internal bool TryManipulate(bool checkRecursive = true) {
if (!ValidateIsAlive()) {
return false;
}
ref var d = ref tween.data;
if (!d.canManipulate()) {
tween.managedData.LogErrorWithStackTrace(Constants.cantManipulateNested);
return false;
}
if (d.isInSequence) {
Assert.IsTrue(d.IsMainSequenceRoot());
if (checkRecursive) {
foreach (var child in new Sequence(tween).GetAllTweens()) {
if (child.data.isUpdating) {
Debug.LogError(Constants.recursiveCallError);
return false;
}
}
} else {
foreach (var child in new Sequence(tween).GetAllTweens()) {
child.data.isUpdating = false;
}
}
} else {
if (checkRecursive) {
if (d.isUpdating) {
Debug.LogError(Constants.recursiveCallError);
return false;
}
} else {
d.isUpdating = false;
}
}
return true;
}
/// Stops the tween when it reaches 'startValue' or 'endValue' for the next time.
/// For example, if you have an infinite tween (cycles == -1) with CycleMode.Yoyo/Rewind, and you wish to stop it when it reaches the 'endValue', then set to true.
/// To stop the animation at the 'startValue', set to false.
public void SetRemainingCycles(bool stopAtEndValue) {
if (!TryManipulate()) {
return;
}
ref var d = ref tween.data;
if (d.cycleMode == CycleMode.Restart || d.cycleMode == CycleMode.Incremental) {
Debug.LogWarning(nameof(SetRemainingCycles) + "(bool " + nameof(stopAtEndValue) + ") is meant to be used with CycleMode.Yoyo or Rewind. Please consider using the overload that accepts int instead.");
}
bool isOneCycleLeft = d.getCyclesDone() % 2 == 0 == stopAtEndValue;
if (tween.data.isSequenceInverted) {
isOneCycleLeft = !isOneCycleLeft;
}
SetRemainingCycles(isOneCycleLeft ? 1 : 2);
}
/// Sets the number of remaining cycles.
/// This method modifies the so that the tween will complete after the number of .
/// In case of negative , it modifies and so that the tween will rewind to the beginning after the number of .
/// To set the initial number of cycles, pass the 'cycles' parameter to 'Tween.' methods instead.
/// Setting cycles to -1 will repeat the tween indefinitely.
public void SetRemainingCycles(int cycles) {
Assert.IsTrue(cycles >= -1);
if (!TryManipulate()) {
return;
}
ref var d = ref tween.data;
if (d.tweenType == TweenAnimation.TweenType.Delay && tween.managedData.HasOnComplete) {
Debug.LogError("Applying cycles to Delay will not repeat the OnComplete() callback, but instead will increase the Delay duration.\n" +
"OnComplete() is called only once when ALL tween cycles complete. To repeat the OnComplete() callback, please use the Sequence.Create(cycles: numCycles) and put the tween inside a Sequence.\n" +
"More info: https://discussions.unity.com/t/926420/101\n");
}
if (cycles == -1) {
if (d.timeScale > 0f) {
d.cyclesTotal = -1;
} else {
Debug.LogError($"'{nameof(SetRemainingCycles)}()' doesn't work with negative '{nameof(d.timeScale)}' and infinite(-1) '{nameof(cycles)}'.");
}
} else {
TweenSettings.setCyclesTo1If0(ref cycles);
if (d.timeScale > 0f) {
d.cyclesTotal = d.getCyclesDone() + cycles;
} else {
int targetCyclesDone = cycles - 1;
d.elapsedTimeTotal = targetCyclesDone * d.cycleDuration + elapsedTime;
d.cyclesDone = targetCyclesDone;
if (d.cyclesTotal < targetCyclesDone) {
d.cyclesTotal = targetCyclesDone + 1;
}
}
}
}
/// Adds completion callback. Please consider using to prevent a possible capture of variable into a closure.
/// Set to 'false' to disable the error about target's destruction. Please note that the callback will be silently ignored in the case of target's destruction. More info: https://github.com/KyryloKuzyk/PrimeTween/discussions/4
public Tween OnComplete([CanBeNull] Action onComplete, bool? warnIfTargetDestroyed = null) {
if (ValidateIsAlive()) {
tween.managedData.OnComplete(onComplete, warnIfTargetDestroyed);
}
return this;
}
/// Adds completion callback.
/// Set to 'false' to disable the error about target's destruction. Please note that the callback will be silently ignored in the case of target's destruction. More info: https://github.com/KyryloKuzyk/PrimeTween/discussions/4
/// The example shows how to destroy the object after the completion of a tween.
/// Please note: we're using the '_transform' variable from the onComplete callback to prevent garbage allocation. Using the 'transform' variable directly will capture it into a closure and generate garbage.
///
/// Tween.PositionX(transform, endValue: 1.5f, duration: 1f)
/// .OnComplete(transform, _transform => Destroy(_transform.gameObject));
///
public Tween OnComplete([NotNull] T target, [CanBeNull] Action onComplete, bool? warnIfTargetDestroyed = null) where T : class {
if (ValidateIsAlive()) {
tween.managedData.OnComplete(target, onComplete, warnIfTargetDestroyed);
}
return this;
}
public Sequence Group(Tween _tween) => TryManipulate() ? Sequence.Create(this).Group(_tween) : default;
public Sequence Chain(Tween _tween) => TryManipulate() ? Sequence.Create(this).Chain(_tween) : default;
public Sequence Group(Sequence sequence) => TryManipulate() ? Sequence.Create(this).Group(sequence) : default;
public Sequence Chain(Sequence sequence) => TryManipulate() ? Sequence.Create(this).Chain(sequence) : default;
internal bool ValidateIsAlive() {
if (!IsCreated) {
if (!PrimeTweenManager.Instance.isDestroyed) {
Debug.LogError(Constants.defaultCtorError);
}
} else if (!isAlive) {
Assert.LogErrorWithStackTrace(Constants.isDeadMessage, id, null);
}
return isAlive;
}
/// Custom timeScale. To smoothly animate timeScale over time, use method.
public float timeScale {
get => TryManipulate() ? tween.data.timeScale : 1;
set {
if (TryManipulate()) {
if (float.IsNaN(value) || float.IsInfinity(value)) {
throw new ArgumentException($"Invalid {nameof(timeScale)}: {value}.");
}
tween.data.timeScale = value;
}
}
}
public Tween OnUpdate(T target, Action onUpdate) where T : class {
if (ValidateIsAlive()) {
tween.managedData.SetOnUpdate(target, onUpdate);
}
return this;
}
internal float durationWithWaitDelay => tween.data.calcDurationWithWaitDependencies();
public override int GetHashCode() => id.GetHashCode();
/// https://www.jacksondunstan.com/articles/5148
public bool Equals(Tween other) => isAlive && other.isAlive && id == other.id;
[Obsolete(ObsoleteMessages.resetOnCompleteRenamed, true)]
#if PRIME_TWEEN_EXPERIMENTAL
public
#endif
Tween ResetOnComplete() => ResetOnCompletion();
/// Instantly resets the animation to the beginning upon completion.
#if PRIME_TWEEN_EXPERIMENTAL
public
#else
internal
#endif
Tween ResetOnCompletion() {
if (ValidateIsAlive()) {
tween.data.resetOnComplete = true;
}
return this;
}
}
}