854 lines
36 KiB
C#

#if PRIME_TWEEN_SAFETY_CHECKS && UNITY_ASSERTIONS
#define SAFETY_CHECKS
#endif
using System;
using JetBrains.Annotations;
using UnityEngine;
using Debug = UnityEngine.Debug;
namespace PrimeTween {
[Serializable]
internal class ReusableTween {
#if UNITY_EDITOR
[SerializeField, HideInInspector] internal string debugDescription;
[SerializeField, CanBeNull, UsedImplicitly] internal UnityEngine.Object unityTarget;
#endif
internal long id = -1;
/// Holds a reference to tween's target. If the target is UnityEngine.Object, the tween will gracefully stop when the target is destroyed. That is, destroying object with running tweens is perfectly ok.
/// Keep in mind: when animating plain C# objects (not derived from UnityEngine.Object), the plugin will hold a strong reference to the object for the entire tween duration.
/// If plain C# target holds a reference to UnityEngine.Object and animates its properties, then it's user's responsibility to ensure that UnityEngine.Object still exists.
[CanBeNull] internal object target;
[SerializeField] internal bool _isPaused;
internal bool _isAlive;
[SerializeField] internal float elapsedTimeTotal;
[SerializeField] internal float easedInterpolationFactor;
internal float cycleDuration;
[SerializeField] internal ValueContainerStartEnd startEndValue;
internal PropType propType => Utils.TweenTypeToTweenData(startEndValue.tweenType).Item1;
internal ref TweenType tweenType => ref startEndValue.tweenType;
internal ref ValueContainer startValue => ref startEndValue.startValue;
internal ref ValueContainer endValue => ref startEndValue.endValue;
internal ValueContainer diff;
internal bool isAdditive;
internal ValueContainer prevVal;
[SerializeField] internal TweenSettings settings;
[SerializeField] int cyclesDone;
const int iniCyclesDone = -1;
internal object customOnValueChange;
internal long longParam;
internal int intParam {
get => (int)longParam;
set => longParam = value;
}
Action<ReusableTween> onValueChange;
[CanBeNull] Action<ReusableTween> onComplete;
[CanBeNull] object onCompleteCallback;
[CanBeNull] object onCompleteTarget;
internal float waitDelay;
internal Sequence sequence;
internal Tween prev;
internal Tween next;
internal Tween prevSibling;
internal Tween nextSibling;
internal Func<ReusableTween, ValueContainer> getter;
internal ref bool startFromCurrent => ref startEndValue.startFromCurrent;
bool stoppedEmergently;
internal readonly TweenCoroutineEnumerator coroutineEnumerator = new TweenCoroutineEnumerator();
internal float timeScale = 1f;
bool warnIgnoredOnCompleteIfTargetDestroyed = true;
internal ShakeData shakeData;
State state;
bool warnEndValueEqualsCurrent;
internal bool updateAndCheckIfRunning(float dt) {
if (!_isAlive) {
return sequence.IsCreated; // don't release a tween until sequence.releaseTweens()
}
if (!_isPaused) {
SetElapsedTimeTotal(elapsedTimeTotal + dt * timeScale);
} else if (isUnityTargetDestroyed()) {
EmergencyStop(true);
return false;
}
return _isAlive;
}
bool isUpdating; // todo place this check only on calls that come from Tween.Custom()? no, then it would not be possible to call .Complete() on custom tweens
internal void SetElapsedTimeTotal(float newElapsedTimeTotal, bool earlyExitSequenceIfPaused = true) {
if (isUpdating) {
Debug.LogError(Constants.recursiveCallError);
return;
}
isUpdating = true;
if (!sequence.IsCreated) {
setElapsedTimeTotal(newElapsedTimeTotal, out int cyclesDiff);
if (!stoppedEmergently && _isAlive && isDone(cyclesDiff)) {
if (!_isPaused) {
kill();
}
ReportOnComplete();
}
} else {
Assert.IsTrue(sequence.isAlive, id);
if (isMainSequenceRoot()) {
Assert.IsTrue(sequence.root.id == id, id);
updateSequence(newElapsedTimeTotal, false, earlyExitSequenceIfPaused);
}
}
isUpdating = false;
}
internal void updateSequence(float _elapsedTimeTotal, bool isRestart, bool earlyExitSequenceIfPaused = true, bool allowSkipChildrenUpdate = true) {
Assert.IsTrue(isSequenceRoot());
float prevEasedT = easedInterpolationFactor;
if (!setElapsedTimeTotal(_elapsedTimeTotal, out int cyclesDiff) && allowSkipChildrenUpdate) { // update sequence root
return;
}
bool isRestartToBeginning = isRestart && cyclesDiff < 0;
Assert.IsTrue(!isRestartToBeginning || cyclesDone == 0 || cyclesDone == iniCyclesDone);
if (cyclesDiff != 0 && !isRestartToBeginning) {
// print($" sequence cyclesDiff: {cyclesDiff}");
if (isRestart) {
Assert.IsTrue(cyclesDiff > 0 && cyclesDone == settings.cycles);
cyclesDiff = 1;
}
int cyclesDiffAbs = Mathf.Abs(cyclesDiff);
int newCyclesDone = cyclesDone;
cyclesDone -= cyclesDiff;
int cyclesDelta = cyclesDiff > 0 ? 1 : -1;
var interpolationFactor = cyclesDelta > 0 ? 1f : 0f;
for (int i = 0; i < cyclesDiffAbs; i++) {
Assert.IsTrue(!isRestart || i == 0);
if (cyclesDone == settings.cycles || cyclesDone == iniCyclesDone) {
// do nothing when moving backward from the last cycle or forward from the -1 cycle
cyclesDone += cyclesDelta;
continue;
}
var easedT = calcEasedT(interpolationFactor, cyclesDone);
var isForwardCycle = easedT > 0.5f;
const float negativeElapsedTime = -1000f;
if (!forceChildrenToPos()) {
return;
}
bool forceChildrenToPos() {
// complete the previous cycles by forcing all children tweens to 0f or 1f
// print($" (i:{i}) force to pos: {isForwardCycle}");
var simulatedSequenceElapsedTime = isForwardCycle ? float.MaxValue : negativeElapsedTime;
foreach (var t in getSequenceSelfChildren(isForwardCycle)) {
var tween = t.tween;
tween.updateSequenceChild(simulatedSequenceElapsedTime, isRestart);
if (isEarlyExitAfterChildUpdate()) {
return false;
}
}
return true;
}
cyclesDone += cyclesDelta;
var sequenceCycleMode = settings.cycleMode;
if (sequenceCycleMode == CycleMode.Restart && cyclesDone != settings.cycles && cyclesDone != iniCyclesDone) { // '&& cyclesDone != 0' check is wrong because we should do the restart when moving from 1 to 0 cyclesDone
if (!restartChildren()) {
return;
}
bool restartChildren() {
// print($"restart to pos: {!isForwardCycle}");
var simulatedSequenceElapsedTime = !isForwardCycle ? float.MaxValue : negativeElapsedTime;
prevEasedT = simulatedSequenceElapsedTime;
foreach (var t in getSequenceSelfChildren(!isForwardCycle)) {
var tween = t.tween;
tween.updateSequenceChild(simulatedSequenceElapsedTime, true);
if (isEarlyExitAfterChildUpdate()) {
return false;
}
Assert.IsTrue(isForwardCycle || tween.cyclesDone == tween.settings.cycles, id);
Assert.IsTrue(!isForwardCycle || tween.cyclesDone <= 0, id);
Assert.IsTrue(isForwardCycle || tween.state == State.After, id);
Assert.IsTrue(!isForwardCycle || tween.state == State.Before, id);
}
return true;
}
}
}
Assert.IsTrue(newCyclesDone == cyclesDone, id);
if (isDone(cyclesDiff)) {
if (isMainSequenceRoot() && !_isPaused) {
sequence.releaseTweens();
}
ReportOnComplete();
return;
}
}
easedInterpolationFactor = Mathf.Clamp01(easedInterpolationFactor);
bool isForward = easedInterpolationFactor > prevEasedT;
float sequenceElapsedTime = easedInterpolationFactor * cycleDuration;
foreach (var t in getSequenceSelfChildren(isForward)) {
t.tween.updateSequenceChild(sequenceElapsedTime, isRestart);
if (isEarlyExitAfterChildUpdate()) {
return;
}
}
bool isEarlyExitAfterChildUpdate() {
if (!sequence.isAlive) {
return true;
}
return earlyExitSequenceIfPaused && sequence.root.tween._isPaused; // access isPaused via root tween to bypass the cantManipulateNested check
}
}
Sequence.SequenceDirectEnumerator getSequenceSelfChildren(bool isForward) {
Assert.IsTrue(sequence.isAlive, id);
return sequence.getSelfChildren(isForward);
}
bool isDone(int cyclesDiff) {
Assert.IsTrue(settings.cycles == -1 || cyclesDone <= settings.cycles);
if (timeScale > 0f) {
return cyclesDiff > 0 && cyclesDone == settings.cycles;
}
return cyclesDiff < 0 && cyclesDone == iniCyclesDone;
}
void updateSequenceChild(float encompassingElapsedTime, bool isRestart) {
if (isSequenceRoot()) {
updateSequence(encompassingElapsedTime, isRestart);
} else {
setElapsedTimeTotal(encompassingElapsedTime, out var cyclesDiff);
if (!stoppedEmergently && _isAlive && isDone(cyclesDiff)) {
ReportOnComplete();
}
}
}
internal bool isMainSequenceRoot() => tweenType == TweenType.MainSequence;
internal bool isSequenceRoot() => tweenType == TweenType.MainSequence || tweenType == TweenType.NestedSequence;
bool setElapsedTimeTotal(float _elapsedTimeTotal, out int cyclesDiff) {
elapsedTimeTotal = _elapsedTimeTotal;
int oldCyclesDone = cyclesDone;
float t = calcTFromElapsedTimeTotal(_elapsedTimeTotal, out var newState);
cyclesDiff = cyclesDone - oldCyclesDone;
if (newState == State.Running || state != newState) {
if (isUnityTargetDestroyed()) {
EmergencyStop(true);
return false;
}
float easedT = calcEasedT(t, cyclesDone);
// print($"state: {state}/{newState}, cycles: {cyclesDone}/{settings.cycles} (diff: {cyclesDiff}), elapsedTimeTotal: {elapsedTimeTotal}, interpolation: {t}/{easedT}");
state = newState;
ReportOnValueChange(easedT);
return true;
}
return false;
}
float calcTFromElapsedTimeTotal(float _elapsedTimeTotal, out State newState) {
// key timeline points: 0 | startDelay | duration | 1 | endDelay | onComplete
var cyclesTotal = settings.cycles;
// ReSharper disable once CompareOfFloatsByEqualityOperator
if (_elapsedTimeTotal == float.MaxValue) {
Assert.AreNotEqual(-1, cyclesTotal);
Assert.IsTrue(cyclesDone <= cyclesTotal);
cyclesDone = cyclesTotal;
newState = State.After;
return 1f;
}
_elapsedTimeTotal -= waitDelay; // waitDelay is applied before calculating cycles
if (_elapsedTimeTotal < 0f) {
cyclesDone = iniCyclesDone;
newState = State.Before;
return 0f;
}
Assert.IsTrue(_elapsedTimeTotal >= 0f);
Assert.AreNotEqual(float.MaxValue, _elapsedTimeTotal);
var duration = settings.duration;
if (duration == 0f) {
if (cyclesTotal == -1) {
// add max one cycle per frame
if (timeScale > 0f) {
if (cyclesDone == iniCyclesDone) {
cyclesDone = 1;
} else {
cyclesDone++;
}
} else if (timeScale != 0f) {
cyclesDone--;
if (cyclesDone == iniCyclesDone) {
newState = State.Before;
return 0f;
}
}
newState = State.Running;
return 1f;
}
Assert.AreNotEqual(-1, cyclesTotal);
if (_elapsedTimeTotal == 0f) {
cyclesDone = iniCyclesDone;
newState = State.Before;
return 0f;
}
Assert.IsTrue(cyclesDone <= cyclesTotal);
cyclesDone = cyclesTotal;
newState = State.After;
return 1f;
}
Assert.AreNotEqual(0f, cycleDuration);
cyclesDone = (int) (_elapsedTimeTotal / cycleDuration);
if (cyclesTotal != -1 && cyclesDone > cyclesTotal) {
cyclesDone = cyclesTotal;
}
if (cyclesTotal != -1 && cyclesDone == cyclesTotal) {
newState = State.After;
return 1f;
}
var elapsedTimeInCycle = _elapsedTimeTotal - cycleDuration * cyclesDone - settings.startDelay;
if (elapsedTimeInCycle < 0f) {
newState = State.Before;
return 0f;
}
Assert.IsTrue(elapsedTimeInCycle >= 0f);
Assert.AreNotEqual(0f, duration);
var result = elapsedTimeInCycle / duration;
if (result > 1f) {
newState = State.After;
return 1f;
}
newState = State.Running;
Assert.IsTrue(result >= 0f);
return result;
}
// void print(string msg) => Debug.Log($"[{Time.frameCount}] id {id} {msg}");
internal void Reset() {
Assert.IsFalse(isUpdating);
Assert.IsFalse(_isAlive);
Assert.IsFalse(sequence.IsCreated);
Assert.IsFalse(prev.IsCreated);
Assert.IsFalse(next.IsCreated);
Assert.IsFalse(prevSibling.IsCreated);
Assert.IsFalse(nextSibling.IsCreated);
Assert.IsFalse(IsInSequence());
if (shakeData.isAlive) {
shakeData.Reset(this);
}
#if UNITY_EDITOR
debugDescription = null;
unityTarget = null;
#endif
id = -1;
target = null;
settings.customEase = null;
customOnValueChange = null;
onValueChange = null;
onComplete = null;
onCompleteCallback = null;
onCompleteTarget = null;
getter = null;
stoppedEmergently = false;
waitDelay = 0f;
coroutineEnumerator.resetEnumerator();
tweenType = TweenType.None;
timeScale = 1f;
warnIgnoredOnCompleteIfTargetDestroyed = true;
clearOnUpdate();
}
/// <param name="warnIfTargetDestroyed">https://github.com/KyryloKuzyk/PrimeTween/discussions/4</param>
internal void OnComplete([NotNull] Action _onComplete, bool warnIfTargetDestroyed) {
Assert.IsNotNull(_onComplete);
validateOnCompleteAssignment();
warnIgnoredOnCompleteIfTargetDestroyed = warnIfTargetDestroyed;
onCompleteCallback = _onComplete;
onComplete = tween => {
var callback = tween.onCompleteCallback as Action;
Assert.IsNotNull(callback);
try {
callback();
} catch (Exception e) {
tween.handleOnCompleteException(e);
}
};
}
internal void OnComplete<T>([CanBeNull] T _target, [NotNull] Action<T> _onComplete, bool warnIfTargetDestroyed) where T : class {
if (_target == null || isDestroyedUnityObject(_target)) {
Debug.LogError($"{nameof(_target)} is null or has been destroyed. {Constants.onCompleteCallbackIgnored}");
return;
}
Assert.IsNotNull(_onComplete);
validateOnCompleteAssignment();
warnIgnoredOnCompleteIfTargetDestroyed = warnIfTargetDestroyed;
onCompleteTarget = _target;
onCompleteCallback = _onComplete;
onComplete = tween => {
var callback = tween.onCompleteCallback as Action<T>;
Assert.IsNotNull(callback);
var _onCompleteTarget = tween.onCompleteTarget as T;
if (isDestroyedUnityObject(_onCompleteTarget)) {
tween.warnOnCompleteIgnored(true);
return;
}
try {
callback(_onCompleteTarget);
} catch (Exception e) {
tween.handleOnCompleteException(e);
}
};
}
void handleOnCompleteException(Exception e) {
// Design decision: if a tween is inside a Sequence and user's tween.OnComplete() throws an exception, the Sequence should continue
Assert.LogError($"Tween's onComplete callback raised exception, tween: {GetDescription()}, exception:\n{e}\n", id, target as UnityEngine.Object);
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("ReSharper", "ConditionIsAlwaysTrueOrFalse")]
static bool isDestroyedUnityObject<T>(T obj) where T: class => obj is UnityEngine.Object unityObject && unityObject == null;
void validateOnCompleteAssignment() {
const string msg = "Tween already has an onComplete callback. Adding more callbacks is not allowed.\n" +
"Workaround: wrap a tween in a Sequence by calling Sequence.Create(tween) and use multiple ChainCallback().\n";
Assert.IsNull(onCompleteTarget, msg);
Assert.IsNull(onCompleteCallback, msg);
Assert.IsNull(onComplete, msg);
}
/// _getter is null for custom tweens
internal void Setup([CanBeNull] object _target, ref TweenSettings _settings, [NotNull] Action<ReusableTween> _onValueChange, [CanBeNull] Func<ReusableTween, ValueContainer> _getter, bool _startFromCurrent, TweenType _tweenType) {
Assert.IsTrue(_settings.cycles >= -1);
Assert.IsNotNull(_onValueChange);
Assert.IsNull(getter);
tweenType = _tweenType;
var propertyType = propType;
Assert.AreNotEqual(PropType.None, propertyType);
#if UNITY_EDITOR
if (Constants.noInstance) {
return;
}
#endif
if (_settings.ease == Ease.Default) {
_settings.ease = PrimeTweenManager.Instance.defaultEase;
} else if (_settings.ease == Ease.Custom && _settings.parametricEase == ParametricEase.None) {
if (_settings.customEase == null || !TweenSettings.ValidateCustomCurveKeyframes(_settings.customEase)) {
Debug.LogError($"Ease type is Ease.Custom, but {nameof(TweenSettings.customEase)} is not configured correctly.");
_settings.ease = PrimeTweenManager.Instance.defaultEase;
}
}
state = State.Before;
target = _target;
setUnityTarget(_target);
elapsedTimeTotal = 0f;
easedInterpolationFactor = float.MinValue;
_isPaused = false;
revive();
cyclesDone = iniCyclesDone;
_settings.SetValidValues();
settings.CopyFrom(ref _settings);
recalculateTotalDuration();
Assert.IsTrue(cycleDuration >= 0);
onValueChange = _onValueChange;
Assert.IsFalse(_startFromCurrent && _getter == null);
startFromCurrent = _startFromCurrent;
getter = _getter;
if (!_startFromCurrent) {
cacheDiff();
}
if (propertyType == PropType.Quaternion) {
prevVal.QuaternionVal = Quaternion.identity;
} else {
prevVal.Reset();
}
warnEndValueEqualsCurrent = PrimeTweenManager.Instance.warnEndValueEqualsCurrent;
}
internal void setUnityTarget(object _target) {
#if UNITY_EDITOR
unityTarget = _target as UnityEngine.Object;
#endif
}
/// Tween.Custom and Tween.ShakeCustom try-catch the <see cref="onValueChange"/> and calls <see cref="ReusableTween.EmergencyStop"/> if an exception occurs.
/// <see cref="ReusableTween.EmergencyStop"/> sets <see cref="stoppedEmergently"/> to true.
internal void ReportOnValueChange(float _easedInterpolationFactor) {
// Debug.Log($"id {id}, ReportOnValueChange {_easedInterpolationFactor}");
Assert.IsFalse(isUnityTargetDestroyed());
if (startFromCurrent) {
startFromCurrent = false;
if (!ShakeData.TryTakeStartValueFromOtherShake(this)) {
startValue = getter(this);
}
if (startValue.Vector4Val == endValue.Vector4Val && warnEndValueEqualsCurrent && !shakeData.isAlive) {
Assert.LogWarning($"Tween's 'endValue' equals to the current animated value: {startValue.Vector4Val}, tween: {GetDescription()}.\n" +
$"{Constants.buildWarningCanBeDisabledMessage(nameof(PrimeTweenConfig.warnEndValueEqualsCurrent))}\n", id);
}
cacheDiff();
}
easedInterpolationFactor = _easedInterpolationFactor;
onValueChange(this);
if (stoppedEmergently || !_isAlive) {
return;
}
onUpdate?.Invoke(this);
}
void ReportOnComplete() {
// Debug.Log($"[{Time.frameCount}] id {id} ReportOnComplete() {easedInterpolationFactor}");
Assert.IsFalse(startFromCurrent);
Assert.IsTrue(timeScale < 0 || cyclesDone == settings.cycles);
Assert.IsTrue(timeScale >= 0 || cyclesDone == iniCyclesDone);
onComplete?.Invoke(this);
}
internal bool isUnityTargetDestroyed() {
// must use target here instead of unityTarget
// unityTarget has the SerializeField attribute, so if ReferenceEquals(unityTarget, null), then Unity will populate the field with non-null UnityEngine.Object when a new scene is loaded in the Editor
// https://github.com/KyryloKuzyk/PrimeTween/issues/32
return isDestroyedUnityObject(target);
}
internal bool HasOnComplete => onComplete != null;
[NotNull]
internal string GetDescription() {
string result = "";
if (!_isAlive) {
result += " - ";
}
if (target != PrimeTweenManager.dummyTarget) {
// ReSharper disable once ConditionIsAlwaysTrueOrFalse
result += $"{(target is UnityEngine.Object unityObject && unityObject != null ? unityObject.name : target?.GetType().Name)} / ";
}
var duration = settings.duration;
if (tweenType == TweenType.Delay) {
if (duration == 0f && onComplete != null) {
result += "Callback";
} else {
result += $"Delay / duration {duration}";
}
} else {
if (tweenType == TweenType.MainSequence) {
result += $"Sequence {id}";
} else if (tweenType == TweenType.NestedSequence) {
result += $"Sequence {id} (nested)";
} else {
result += tweenType.ToString() ;
}
result += " / duration ";
/*if (waitDelay != 0f) {
result += $"{waitDelay}+";
}*/
result += $"{duration}";
}
result += $" / id {id}";
if (sequence.IsCreated && tweenType != TweenType.MainSequence) {
result += $" / sequence {sequence.root.id}";
}
return result;
}
internal float calcDurationWithWaitDependencies() {
var cycles = settings.cycles;
Assert.AreNotEqual(-1, cycles, "It's impossible to calculate the duration of an infinite tween (cycles == -1).");
Assert.AreNotEqual(0, cycles);
return waitDelay + cycleDuration * cycles;
}
internal void recalculateTotalDuration() {
cycleDuration = settings.startDelay + settings.duration + settings.endDelay;
}
internal float FloatVal => startValue.x + diff.x * easedInterpolationFactor;
internal double DoubleVal => startValue.DoubleVal + diff.DoubleVal * easedInterpolationFactor;
internal Vector2 Vector2Val {
get {
var easedT = easedInterpolationFactor;
return new Vector2(
startValue.x + diff.x * easedT,
startValue.y + diff.y * easedT);
}
}
internal Vector3 Vector3Val {
get {
var easedT = easedInterpolationFactor;
return new Vector3(
startValue.x + diff.x * easedT,
startValue.y + diff.y * easedT,
startValue.z + diff.z * easedT);
}
}
internal Vector4 Vector4Val {
get {
var easedT = easedInterpolationFactor;
return new Vector4(
startValue.x + diff.x * easedT,
startValue.y + diff.y * easedT,
startValue.z + diff.z * easedT,
startValue.w + diff.w * easedT);
}
}
internal Color ColorVal {
get {
var easedT = easedInterpolationFactor;
return new Color(
startValue.x + diff.x * easedT,
startValue.y + diff.y * easedT,
startValue.z + diff.z * easedT,
startValue.w + diff.w * easedT);
}
}
internal Rect RectVal {
get {
var easedT = easedInterpolationFactor;
return new Rect(
startValue.x + diff.x * easedT,
startValue.y + diff.y * easedT,
startValue.z + diff.z * easedT,
startValue.w + diff.w * easedT);
}
}
internal Quaternion QuaternionVal => Quaternion.SlerpUnclamped(startValue.QuaternionVal, endValue.QuaternionVal, easedInterpolationFactor);
float calcEasedT(float t, int cyclesDone) {
switch (settings.cycleMode) {
case CycleMode.Restart:
return evaluate(t);
case CycleMode.Incremental:
return evaluate(t) + clampCyclesDone();
case CycleMode.Yoyo: {
var isForwardCycle = clampCyclesDone() % 2 == 0;
return isForwardCycle ? evaluate(t) : 1 - evaluate(t);
}
case CycleMode.Rewind: {
var isForwardCycle = clampCyclesDone() % 2 == 0;
return isForwardCycle ? evaluate(t) : evaluate(1 - t);
}
default:
throw new Exception();
}
int clampCyclesDone() {
if (cyclesDone == iniCyclesDone) {
return 0;
}
int cyclesTotal = settings.cycles;
if (cyclesDone == cyclesTotal) {
Assert.AreNotEqual(-1, cyclesTotal);
return cyclesTotal - 1;
}
return cyclesDone;
}
}
float evaluate(float t) {
if (settings.ease == Ease.Custom) {
if (settings.parametricEase != ParametricEase.None) {
return Easing.Evaluate(t, this);
}
return settings.customEase.Evaluate(t);
}
return StandardEasing.Evaluate(t, settings.ease);
}
internal void cacheDiff() {
Assert.IsFalse(startFromCurrent);
var propertyType = propType;
Assert.AreNotEqual(PropType.None, propertyType);
switch (propertyType) {
case PropType.Quaternion:
startValue.QuaternionVal.Normalize();
endValue.QuaternionVal.Normalize();
break;
case PropType.Double:
diff.DoubleVal = endValue.DoubleVal - startValue.DoubleVal;
diff.z = 0;
diff.w = 0;
break;
default:
diff.x = endValue.x - startValue.x;
diff.y = endValue.y - startValue.y;
diff.z = endValue.z - startValue.z;
diff.w = endValue.w - startValue.w;
break;
}
}
internal void ForceComplete() {
Assert.IsFalse(sequence.IsCreated);
kill(); // protects from recursive call
if (isUnityTargetDestroyed()) {
warnOnCompleteIgnored(true);
return;
}
var cyclesTotal = settings.cycles;
if (cyclesTotal == -1) {
// same as SetRemainingCycles(1)
cyclesTotal = getCyclesDone() + 1;
settings.cycles = cyclesTotal;
}
cyclesDone = cyclesTotal;
ReportOnValueChange(calcEasedT(1f, cyclesTotal));
if (stoppedEmergently) {
return;
}
ReportOnComplete();
Assert.IsFalse(_isAlive);
}
internal void warnOnCompleteIgnored(bool isTargetDestroyed) {
if (HasOnComplete && warnIgnoredOnCompleteIfTargetDestroyed) {
onComplete = null;
var msg = $"{Constants.onCompleteCallbackIgnored} Tween: {GetDescription()}.\n";
if (isTargetDestroyed) {
msg += "\nIf you use tween.OnComplete(), Tween.Delay(), or sequence.ChainDelay() only for cosmetic purposes, you can turn off this error by passing 'warnIfTargetDestroyed: false' to the method.\n" +
"More info: https://github.com/KyryloKuzyk/PrimeTween/discussions/4\n";
}
Assert.LogError(msg, id, target as UnityEngine.Object);
}
}
internal void EmergencyStop(bool isTargetDestroyed = false) {
if (sequence.IsCreated) {
var mainSequence = sequence;
while (true) {
var _prev = mainSequence.root.tween.prev;
if (!_prev.IsCreated) {
break;
}
var parent = _prev.tween.sequence;
if (!parent.IsCreated) {
break;
}
mainSequence = parent;
}
Assert.IsTrue(mainSequence.isAlive);
Assert.IsTrue(mainSequence.root.tween.isMainSequenceRoot());
mainSequence.emergencyStop();
} else if (_isAlive) {
// EmergencyStop() can be called after ForceComplete() and a caught exception in Tween.Custom()
kill();
}
stoppedEmergently = true;
warnOnCompleteIgnored(isTargetDestroyed);
Assert.IsFalse(_isAlive);
Assert.IsFalse(sequence.isAlive);
}
internal void kill() {
// print($"kill {GetDescription()}");
Assert.IsTrue(_isAlive);
_isAlive = false;
#if UNITY_EDITOR
debugDescription = null;
#endif
}
void revive() {
// print($"revive {GetDescription()}");
Assert.IsFalse(_isAlive);
_isAlive = true;
#if UNITY_EDITOR
debugDescription = null;
#endif
}
internal bool IsInSequence() {
var result = sequence.IsCreated;
Assert.IsTrue(result || !nextSibling.IsCreated);
return result;
}
internal bool canManipulate() => !IsInSequence() || isMainSequenceRoot();
internal bool trySetPause(bool isPaused) {
if (_isPaused == isPaused) {
return false;
}
_isPaused = isPaused;
return true;
}
[CanBeNull] object onUpdateTarget;
object onUpdateCallback;
Action<ReusableTween> onUpdate;
internal void SetOnUpdate<T>(T _target, [NotNull] Action<T, Tween> _onUpdate) where T : class {
Assert.IsNull(onUpdate, "Only one OnUpdate() is allowed for one tween.");
Assert.IsNotNull(_onUpdate, nameof(_onUpdate) + " is null!");
onUpdateTarget = _target;
onUpdateCallback = _onUpdate;
onUpdate = reusableTween => reusableTween.invokeOnUpdate<T>();
}
void invokeOnUpdate<T>() where T : class {
var callback = onUpdateCallback as Action<T, Tween>;
Assert.IsNotNull(callback);
var _onUpdateTarget = onUpdateTarget as T;
if (isDestroyedUnityObject(_onUpdateTarget)) {
Assert.LogError($"OnUpdate() will not be called again because OnUpdate()'s target has been destroyed, tween: {GetDescription()}", id, target as UnityEngine.Object);
clearOnUpdate();
return;
}
try {
callback(_onUpdateTarget, new Tween(this));
} catch (Exception e) {
Assert.LogError($"OnUpdate() will not be called again because it thrown exception, tween: {GetDescription()}, exception:\n{e}", id, target as UnityEngine.Object);
clearOnUpdate();
}
}
void clearOnUpdate() {
onUpdateTarget = null;
onUpdateCallback = null;
onUpdate = null;
}
public override string ToString() {
return GetDescription();
}
enum State : byte {
Before, Running, After
}
internal float getElapsedTimeTotal() {
var result = elapsedTimeTotal;
var durationTotal = getDurationTotal();
// ReSharper disable once CompareOfFloatsByEqualityOperator
if (result == float.MaxValue) {
return durationTotal;
}
return Mathf.Clamp(result, 0f, durationTotal);
}
internal float getDurationTotal() {
var cyclesTotal = settings.cycles;
if (cyclesTotal == -1) {
return float.PositiveInfinity;
}
Assert.AreNotEqual(0, cyclesTotal);
return cycleDuration * cyclesTotal;
}
internal int getCyclesDone() {
int result = cyclesDone;
if (result == iniCyclesDone) {
return 0;
}
Assert.IsTrue(result >= 0);
return result;
}
}
}