618 lines
26 KiB
C#

#if PRIME_TWEEN_SAFETY_CHECKS && UNITY_ASSERTIONS
#define SAFETY_CHECKS
#endif
#if PRIME_TWEEN_INSPECTOR_DEBUGGING && UNITY_EDITOR
#define ENABLE_SERIALIZATION
#endif
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using UnityEngine;
namespace PrimeTween {
/// <summary>An ordered group of tweens and callbacks. Tweens in a sequence can run in parallel to one another with <see cref="Group"/> and sequentially with <see cref="Chain"/>.<br/>
/// To make tweens in a Sequence overlap each other, use <see cref="TweenSettings.startDelay"/> and <see cref="TweenSettings.endDelay"/>.</summary>
/// <example><code>
/// Sequence.Create()
/// .Group(Tween.PositionX(transform, endValue: 10f, duration: 1.5f))
/// .Group(Tween.Scale(transform, endValue: 2f, duration: 0.5f)) // position and localScale tweens will run in parallel because they are 'grouped'
/// .Chain(Tween.Rotation(transform, endValue: new Vector3(0f, 0f, 45f), duration: 1f)) // rotation tween is 'chained' so it will start when both previous tweens are finished (after 1.5 seconds)
/// .ChainCallback(() =&gt; Debug.Log("Sequence completed"));
/// </code></example>
#if ENABLE_SERIALIZATION
[Serializable]
#endif
public
#if !ENABLE_SERIALIZATION && UNITY_2020_3_OR_NEWER
readonly // duration setter produces error in Unity <= 2019.4.40: error CS1604: Cannot assign to 'this' because it is read-only
#endif
partial struct Sequence : IEquatable<Sequence> {
const int emptySequenceTag = -43;
internal
#if !ENABLE_SERIALIZATION && UNITY_2020_3_OR_NEWER
readonly
#endif
Tween root;
internal bool IsCreated => root.IsCreated;
long id => root.id;
/// Sequence is 'alive' when any of its tweens is 'alive'.
public bool isAlive => root.isAlive;
/// Elapsed time of the current cycle.
public float elapsedTime {
get => root.elapsedTime;
set => root.elapsedTime = value;
}
/// The total number of cycles. Returns -1 to indicate infinite number cycles.
public int cyclesTotal => root.cyclesTotal;
public int cyclesDone => root.cyclesDone;
/// The duration of one cycle.
public float duration {
get => root.duration;
private set {
Assert.IsTrue(isAlive);
Assert.IsTrue(root.tween.isMainSequenceRoot());
var rootTween = root.tween;
Assert.AreEqual(0f, elapsedTimeTotal);
Assert.IsTrue(value >= rootTween.cycleDuration);
Assert.IsTrue(value >= rootTween.settings.duration);
Assert.AreEqual(0f, rootTween.settings.startDelay);
Assert.AreEqual(0f, rootTween.settings.endDelay);
rootTween.settings.duration = value;
rootTween.cycleDuration = value;
}
}
/// Elapsed time of all cycles.
public float elapsedTimeTotal {
get => root.elapsedTimeTotal;
set => root.elapsedTimeTotal = value;
}
/// <summary>The duration of all cycles. If cycles == -1, returns <see cref="float.PositiveInfinity"/>.</summary>
public float durationTotal => root.durationTotal;
/// Normalized progress of the current cycle expressed in 0..1 range.
public float progress {
get => root.progress;
set => root.progress = value;
}
/// Normalized progress of all cycles expressed in 0..1 range.
public float progressTotal {
get => root.progressTotal;
set => root.progressTotal = value;
}
bool tryManipulate() => root.tryManipulate();
bool ValidateCanManipulateSequence() {
if (!tryManipulate()) {
return false;
}
if (root.elapsedTimeTotal != 0f) {
Debug.LogError(Constants.animationAlreadyStarted);
return false;
}
return true;
}
public static Sequence Create(int cycles = 1, CycleMode cycleMode = CycleMode.Restart, Ease sequenceEase = Ease.Linear, bool useUnscaledTime = false, UpdateType updateType = default) {
#if UNITY_EDITOR
if (Constants.warnNoInstance) {
return default;
}
#endif
var tween = PrimeTweenManager.fetchTween();
if (cycleMode == CycleMode.Incremental) {
Debug.LogError($"Sequence doesn't support CycleMode.Incremental. Parameter {nameof(sequenceEase)} is applied to the sequence's 'timeline', and incrementing the 'timeline' doesn't make sense. For the same reason, {nameof(sequenceEase)} is clamped to [0:1] range.");
cycleMode = CycleMode.Restart;
}
if (sequenceEase == Ease.Custom) {
Debug.LogError("Sequence doesn't support Ease.Custom.");
sequenceEase = Ease.Linear;
}
if (sequenceEase == Ease.Default) { // todo this is questionable
sequenceEase = Ease.Linear;
}
var settings = new TweenSettings(0f, sequenceEase, cycles, cycleMode, 0f, 0f, useUnscaledTime, updateType);
tween.Setup(PrimeTweenManager.dummyTarget, ref settings, _ => {}, null, false, TweenType.MainSequence);
tween.intParam = emptySequenceTag;
var root = PrimeTweenManager.addTween(tween);
Assert.IsTrue(root.isAlive);
return new Sequence(root);
}
public static Sequence Create(Tween firstTween) {
#if UNITY_EDITOR
if (Constants.warnNoInstance) {
return default;
}
#endif
return Create().Group(firstTween);
}
Sequence(Tween rootTween) {
root = rootTween;
setSequence(rootTween);
Assert.IsTrue(isAlive);
Assert.AreEqual(0f, duration);
Assert.IsTrue(durationTotal == 0f || float.IsPositiveInfinity(durationTotal));
}
/// <summary>Groups <paramref name="tween"/> with the 'previous' animation in this Sequence.<br/>
/// The 'previous' animation is the animation used in the preceding Group/Chain/Insert() method call.<br/>
/// Grouped animations start at the same time and run in parallel.</summary>
public Sequence Group(Tween tween) {
if (tryManipulate()) {
Insert(getLastInSelfOrRoot().tween.waitDelay, tween);
}
return this;
}
void addLinkedReference(Tween tween) {
Tween last;
if (root.tween.next.IsCreated) {
last = getLast();
var lastInSelf = getLastInSelfOrRoot();
Assert.AreNotEqual(root.id, lastInSelf.id);
Assert.IsFalse(lastInSelf.tween.nextSibling.IsCreated);
lastInSelf.tween.nextSibling = tween;
Assert.IsFalse(tween.tween.prevSibling.IsCreated);
tween.tween.prevSibling = lastInSelf;
} else {
last = root;
}
Assert.IsFalse(last.tween.next.IsCreated);
Assert.IsFalse(tween.tween.prev.IsCreated);
last.tween.next = tween;
tween.tween.prev = last;
root.tween.intParam = emptySequenceTag - emptySequenceTag; // set to 0 in a way to be able to search the code better
}
Tween getLast() {
Tween result = default;
foreach (var current in getAllTweens()) {
result = current;
}
Assert.IsTrue(result.IsCreated);
Assert.IsFalse(result.tween.next.IsCreated);
return result;
}
/// <summary>Places <paramref name="tween"/> after all previously added animations in this sequence. Chained animations run sequentially after one another.</summary>
public Sequence Chain(Tween tween) {
if (tryManipulate()) {
Insert(duration, tween);
}
return this;
}
/// <summary>Places <paramref name="tween"/> inside this Sequence at time <paramref name="atTime"/>, overlapping with other animations.<br/>
/// The total sequence duration is increased if the inserted <paramref name="tween"/> doesn't fit inside the current sequence duration.</summary>
public Sequence Insert(float atTime, Tween tween) {
if (!ValidateCanAdd(tween)) {
return this;
}
if (tween.tween.sequence.IsCreated) {
Debug.LogError($"{Constants.nestTweenTwiceError} Tween: {tween.tween.GetDescription()}");
return this;
}
setSequence(tween);
Insert_internal(atTime, tween);
return this;
}
void Insert_internal(float atTime, Tween other) {
Assert.AreEqual(0f, other.tween.waitDelay);
other.tween.waitDelay = atTime;
duration = Mathf.Max(duration, other.durationWithWaitDelay);
addLinkedReference(other);
}
/// <summary>Schedules <see cref="callback"/> after all previously added tweens.</summary>
/// <param name="warnIfTargetDestroyed">https://github.com/KyryloKuzyk/PrimeTween/discussions/4</param>
public Sequence ChainCallback([NotNull] Action callback, bool warnIfTargetDestroyed = true) {
if (tryManipulate()) {
InsertCallback(duration, callback, warnIfTargetDestroyed);
}
return this;
}
public Sequence InsertCallback(float atTime, Action callback, bool warnIfTargetDestroyed = true) {
if (!tryManipulate()) {
return this;
}
var delay = PrimeTweenManager.delayWithoutDurationCheck(PrimeTweenManager.dummyTarget, 0f, false);
Assert.IsTrue(delay.HasValue);
delay.Value.tween.OnComplete(callback, warnIfTargetDestroyed);
return Insert(atTime, delay.Value);
}
/// <summary>Schedules <see cref="callback"/> after all previously added tweens. Passing 'target' allows to write a non-allocating callback.</summary>
/// <param name="warnIfTargetDestroyed">https://github.com/KyryloKuzyk/PrimeTween/discussions/4</param>
public Sequence ChainCallback<T>([NotNull] T target, [NotNull] Action<T> callback, bool warnIfTargetDestroyed = true) where T: class {
if (tryManipulate()) {
InsertCallback(duration, target, callback, warnIfTargetDestroyed);
}
return this;
}
public Sequence InsertCallback<T>(float atTime, [NotNull] T target, Action<T> callback, bool warnIfTargetDestroyed = true) where T: class {
if (!tryManipulate()) {
return this;
}
var delay = PrimeTweenManager.delayWithoutDurationCheck(target, 0f, false);
if (!delay.HasValue) {
return this;
}
delay.Value.tween.OnComplete(target, callback, warnIfTargetDestroyed);
return Insert(atTime, delay.Value);
}
/// <summary>Schedules delay after all previously added tweens.</summary>
public Sequence ChainDelay(float duration) {
return Chain(Tween.Delay(duration));
}
Tween getLastInSelfOrRoot() {
Assert.IsTrue(isAlive);
var result = root;
foreach (var current in getSelfChildren()) {
result = current;
}
Assert.IsTrue(result.IsCreated);
Assert.IsFalse(result.tween.nextSibling.IsCreated);
return result;
}
void setSequence(Tween handle) {
Assert.IsTrue(IsCreated);
Assert.IsTrue(handle.isAlive);
var tween = handle.tween;
Assert.IsFalse(tween.sequence.IsCreated);
tween.sequence = this;
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator")]
bool ValidateCanAdd(Tween other) {
if (!ValidateCanManipulateSequence()) {
return false;
}
if (!other.isAlive) {
Debug.LogError(Constants.addDeadTweenToSequenceError);
return false;
}
var tween = other.tween;
if (tween.settings.cycles == -1) {
Debug.LogError(Constants.infiniteTweenInSequenceError);
return false;
}
var rootTween = root.tween;
if (tween._isPaused && tween._isPaused != rootTween._isPaused) {
warnIgnoredChildrenSetting(nameof(isPaused));
}
if (tween.timeScale != 1f && tween.timeScale != rootTween.timeScale) {
warnIgnoredChildrenSetting(nameof(timeScale));
}
if (tween.settings.useUnscaledTime && tween.settings.useUnscaledTime != rootTween.settings.useUnscaledTime) {
warnIgnoredChildrenSetting(nameof(TweenSettings.useUnscaledTime));
}
if (tween.settings._updateType != _UpdateType.Default && tween.settings._updateType != rootTween.settings._updateType) {
warnIgnoredChildrenSetting(nameof(TweenSettings.updateType));
}
void warnIgnoredChildrenSetting(string settingName) {
Debug.LogError($"'{settingName}' was ignored after adding child animation to the Sequence. Parent Sequence controls '{settingName}' of all its children animations.\n" +
"To prevent this error:\n" +
$"- Use the default value of '{settingName}' in child animation.\n" +
$"- OR use the same '{settingName}' in child animation.\n\n");
}
return true;
}
/// Stops all tweens in the Sequence, ignoring callbacks.
public void Stop() {
if (isAlive && tryManipulate()) {
Assert.IsTrue(root.tween.isMainSequenceRoot());
releaseTweens();
Assert.IsFalse(isAlive);
}
}
/// <summary>Immediately completes the sequence.<br/>
/// If the sequence has infinite cycles (cycles == -1), completes only the current cycle. To choose where the sequence should stop (at the 'start' or at the 'end') in the case of infinite cycles, use <see cref="SetRemainingCycles(bool stopAtEndValue)"/> before calling Complete().</summary>
public void Complete() {
if (isAlive && tryManipulate()) {
if (cyclesTotal == -1 || root.tween.settings.cycleMode == CycleMode.Restart) {
SetRemainingCycles(1);
} else {
int cyclesLeft = cyclesTotal - cyclesDone;
SetRemainingCycles(cyclesLeft % 2 == 1 ? 1 : 2);
}
root.isPaused = false;
Assert.IsTrue(root.tween.isMainSequenceRoot());
root.tween.updateSequence(float.MaxValue, false, allowSkipChildrenUpdate: false);
Assert.IsFalse(isAlive);
}
}
internal void emergencyStop() {
Assert.IsTrue(isAlive);
Assert.IsTrue(root.tween.isMainSequenceRoot());
releaseTweens(t => t.warnOnCompleteIgnored(false));
}
internal void releaseTweens([CanBeNull] Action<ReusableTween> beforeKill = null) {
var enumerator = getAllTweens();
enumerator.MoveNext();
var current = enumerator.Current;
Assert.IsTrue(current.isAlive);
while (true) {
// ReSharper disable once RedundantCast
Tween? next = enumerator.MoveNext() ? enumerator.Current : (Tween?)null;
var tween = current.tween;
Assert.IsTrue(tween._isAlive);
beforeKill?.Invoke(tween);
tween.kill();
Assert.IsFalse(tween._isAlive);
releaseTween(tween);
if (!next.HasValue) {
break;
}
current = next.Value;
}
Assert.IsFalse(isAlive); // not IsCreated because this may be a local variable in the user's codebase
}
static void releaseTween([NotNull] ReusableTween tween) {
// Debug.Log($"[{Time.frameCount}] releaseTween {tween.id}");
Assert.AreNotEqual(0, tween.sequence.root.id);
tween.next = default;
tween.prev = default;
tween.prevSibling = default;
tween.nextSibling = default;
tween.sequence = default;
if (tween.isSequenceRoot()) {
tween.tweenType = TweenType.None;
Assert.IsFalse(tween.isSequenceRoot());
}
}
internal SequenceChildrenEnumerator getAllChildren() {
var enumerator = getAllTweens();
var movedNext = enumerator.MoveNext(); // skip self
Assert.IsTrue(movedNext);
Assert.AreEqual(root, enumerator.Current);
return enumerator;
}
/// <summary>Stops the sequence when it reaches the 'end' or returns to the 'start' for the next time.<br/>
/// For example, if you have an infinite sequence (cycles == -1) with CycleMode.Yoyo/Rewind, and you wish to stop it when it reaches the 'end', then set <see cref="stopAtEndValue"/> to true.
/// To stop the animation at the 'beginning', set <see cref="stopAtEndValue"/> to false.</summary>
public void SetRemainingCycles(bool stopAtEndValue) {
root.SetRemainingCycles(stopAtEndValue);
}
/// <summary>Sets the number of remaining cycles.<br/>
/// This method modifies the <see cref="cyclesTotal"/> so that the sequence will complete after the number of <see cref="cycles"/>.<br/>
/// To set the initial number of cycles, use Sequence.Create(cycles: numCycles) instead.<br/><br/>
/// Setting cycles to -1 will repeat the sequence indefinitely.<br/>
/// </summary>
public void SetRemainingCycles(int cycles) {
root.SetRemainingCycles(cycles);
}
public bool isPaused {
get => root.isPaused;
set => root.isPaused = value;
}
internal SequenceDirectEnumerator getSelfChildren(bool isForward = true) => new SequenceDirectEnumerator(this, isForward);
internal SequenceChildrenEnumerator getAllTweens() => new SequenceChildrenEnumerator(this);
public override string ToString() => root.ToString();
internal struct SequenceDirectEnumerator {
readonly Sequence sequence;
Tween current;
readonly bool isEmpty;
readonly bool isForward;
bool isStarted;
internal SequenceDirectEnumerator(Sequence s, bool isForward) {
Assert.IsTrue(s.isAlive, s.id);
sequence = s;
this.isForward = isForward;
isStarted = false;
isEmpty = isSequenceEmpty(s);
if (isEmpty) {
current = default;
return;
}
current = sequence.root.tween.next;
Assert.IsTrue(current.IsCreated && current.id != sequence.root.tween.nextSibling.id);
if (!isForward) {
while (true) {
var next = current.tween.nextSibling;
if (!next.IsCreated) {
break;
}
current = next;
}
}
Assert.IsTrue(current.IsCreated);
}
static bool isSequenceEmpty(Sequence s) {
// tests: SequenceNestingDifferentSettings(), TestSequenceEnumeratorWithEmptySequences()
return s.root.tween.intParam == emptySequenceTag;
}
public
#if UNITY_2020_2_OR_NEWER
readonly
#endif
SequenceDirectEnumerator GetEnumerator() {
Assert.IsTrue(sequence.isAlive);
return this;
}
public
#if UNITY_2020_2_OR_NEWER
readonly
#endif
Tween Current {
get {
Assert.IsTrue(sequence.isAlive);
Assert.IsTrue(current.IsCreated);
Assert.IsNotNull(current.tween);
Assert.AreEqual(current.id, current.tween.id);
Assert.IsTrue(current.tween.sequence.IsCreated);
return current;
}
}
public bool MoveNext() {
if (isEmpty) {
return false;
}
Assert.IsTrue(current.isAlive);
if (!isStarted) {
isStarted = true;
return true;
}
current = isForward ? current.tween.nextSibling : current.tween.prevSibling;
return current.IsCreated;
}
}
internal struct SequenceChildrenEnumerator {
readonly Sequence sequence;
Tween current;
bool isStarted;
internal SequenceChildrenEnumerator(Sequence s) {
Assert.IsTrue(s.isAlive);
Assert.IsTrue(s.root.tween.isMainSequenceRoot());
sequence = s;
current = default;
isStarted = false;
}
public
#if UNITY_2020_2_OR_NEWER
readonly
#endif
SequenceChildrenEnumerator GetEnumerator() {
Assert.IsTrue(sequence.isAlive);
return this;
}
public
#if UNITY_2020_2_OR_NEWER
readonly
#endif
Tween Current {
get {
Assert.IsTrue(current.IsCreated);
Assert.IsNotNull(current.tween);
Assert.AreEqual(current.id, current.tween.id);
Assert.IsTrue(current.tween.sequence.IsCreated);
return current;
}
}
public bool MoveNext() {
if (!isStarted) {
Assert.IsFalse(current.IsCreated);
current = sequence.root;
isStarted = true;
return true;
}
Assert.IsTrue(current.isAlive);
current = current.tween.next;
return current.IsCreated;
}
}
/// <summary>Places <paramref name="sequence"/> after all previously added animations in this sequence. Chained animations run sequentially after one another.</summary>
public Sequence Chain(Sequence sequence) {
if (tryManipulate()) {
Insert(duration, sequence);
}
return this;
}
/// <summary>Groups <paramref name="sequence"/> with the 'previous' animation in this Sequence.<br/>
/// The 'previous' animation is the animation used in the preceding Group/Chain/Insert() method call.<br/>
/// Grouped animations start at the same time and run in parallel.</summary>
public Sequence Group(Sequence sequence) {
if (tryManipulate()) {
Insert(getLastInSelfOrRoot().tween.waitDelay, sequence);
}
return this;
}
/// <summary>Places <paramref name="sequence"/> inside this Sequence at time <paramref name="atTime"/>, overlapping with other animations.<br/>
/// The total sequence duration is increased if the inserted <paramref name="sequence"/> doesn't fit inside the current sequence duration.</summary>
public Sequence Insert(float atTime, Sequence sequence) {
if (!ValidateCanAdd(sequence.root)) {
return this;
}
ref var otherTweenType = ref sequence.root.tween.tweenType;
if (otherTweenType != TweenType.MainSequence) {
Debug.LogError(Constants.nestSequenceTwiceError);
return this;
}
otherTweenType = TweenType.NestedSequence;
Insert_internal(atTime, sequence.root);
validateSequenceEnumerator();
return this;
}
/// <summary>Custom timeScale. To smoothly animate timeScale over time, use <see cref="Tween.TweenTimeScale"/> method.</summary>
public float timeScale {
get => root.timeScale;
set => root.timeScale = value;
}
[System.Diagnostics.Conditional("SAFETY_CHECKS")]
void validateSequenceEnumerator() {
var buffer = new List<ReusableTween> {
root.tween
};
foreach (var t in getAllTweens()) {
// Debug.Log($"----- {t}");
if (t.tween.isSequenceRoot()) {
foreach (var ch in t.tween.sequence.getSelfChildren()) {
// Debug.Log(ch);
buffer.Add(ch.tween);
}
}
}
if (buffer.Count != buffer.Select(_ => _.id).Distinct().Count()) {
Debug.LogError($"{root.id}, duplicates in validateSequenceEnumerator():\n{string.Join("\n", buffer)}");
}
}
public Sequence OnComplete(Action onComplete, bool warnIfTargetDestroyed = true) {
root.OnComplete(onComplete, warnIfTargetDestroyed);
return this;
}
public Sequence OnComplete<T>(T target, Action<T> onComplete, bool warnIfTargetDestroyed = true) where T : class {
root.OnComplete(target, onComplete, warnIfTargetDestroyed);
return this;
}
public override int GetHashCode() => root.GetHashCode();
public bool Equals(Sequence other) => root.Equals(other.root);
}
}