// ReSharper disable Unity.RedundantHideInInspectorAttribute
#if PRIME_TWEEN_SAFETY_CHECKS && UNITY_ASSERTIONS
#define SAFETY_CHECKS
#endif
using System;
using System.Collections.Generic;
using System.Diagnostics;
using JetBrains.Annotations;
using UnityEngine;
using Debug = UnityEngine.Debug;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace PrimeTween {
[AddComponentMenu("")]
internal class PrimeTweenManager : MonoBehaviour {
internal static PrimeTweenManager Instance;
#if UNITY_EDITOR
static bool isHotReload = true;
#endif
internal static int customInitialCapacity = -1;
/// Item can be null if the list is accessed from the via onValueChange() or onComplete()
/// Changing list to array gives about 8% performance improvement and is possible to do in the future
/// The current implementation is simpler and PrimeTweenManagerInspector can draw tweens with no additional code
#if UNITY_2021_3_OR_NEWER
[ItemCanBeNull]
#endif
[SerializeField] internal List tweens;
[SerializeField] internal List lateUpdateTweens;
[SerializeField] internal List fixedUpdateTweens;
[NonSerialized] internal List pool;
/// startValue can't be replaced with 'Tween lastTween'
/// because the lastTween may already be dead, but the tween before it is still alive (count >= 1)
/// and we can't retrieve the startValue from the dead lastTween
internal Dictionary<(Transform, TweenType), (ValueContainer startValue, int count)> shakes;
internal int currentPoolCapacity { get; private set; }
internal int maxSimultaneousTweensCount { get; private set; }
[HideInInspector]
internal long lastId = 1;
internal Ease defaultEase = Ease.OutQuad;
internal _UpdateType defaultUpdateType = _UpdateType.Update;
internal const Ease defaultShakeEase = Ease.OutQuad;
internal bool warnTweenOnDisabledTarget = true;
internal bool warnZeroDuration = true;
internal bool warnStructBoxingAllocationInCoroutine = true;
internal bool warnBenchmarkWithAsserts = true;
internal bool validateCustomCurves = true;
internal bool warnEndValueEqualsCurrent = true;
int processedCount;
int lateUpdateTweensProcessedCount;
int fixedUpdateTweensProcessedCount;
int maxLateUpdateCount;
internal int updateDepth;
internal static readonly object dummyTarget = new object();
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
static void beforeSceneLoad() {
#if UNITY_EDITOR
isHotReload = false;
#endif
Assert.IsNull(Instance);
var go = new GameObject(nameof(PrimeTweenManager));
DontDestroyOnLoad(go);
var instance = go.AddComponent();
const int defaultInitialCapacity = 200;
instance.init(customInitialCapacity != -1 ? customInitialCapacity : defaultInitialCapacity);
Instance = instance;
}
void init(int capacity) {
tweens = new List(capacity);
lateUpdateTweens = new List(capacity);
fixedUpdateTweens = new List(capacity);
pool = new List(capacity);
for (int i = 0; i < capacity; i++) {
pool.Add(new ReusableTween());
}
shakes = new Dictionary<(Transform, TweenType), (ValueContainer, int)>(capacity);
currentPoolCapacity = capacity;
}
const string manualInstanceCreationIsNotAllowedMessage = "Please don't create the " + nameof(PrimeTweenManager) + " instance manually.";
void Awake() => Assert.IsNull(Instance, manualInstanceCreationIsNotAllowedMessage);
#if UNITY_EDITOR
[InitializeOnLoadMethod]
static void iniOnLoad() {
EditorApplication.playModeStateChanged += state => {
if (state == PlayModeStateChange.EnteredEditMode) {
Instance = null;
customInitialCapacity = -1;
}
};
if (!isHotReload) {
return;
}
if (!Application.isPlaying) {
return;
}
Assert.IsNull(Instance);
var foundInScene =
#if UNITY_2023_1_OR_NEWER
FindAnyObjectByType
#else
FindObjectOfType
#endif
();
Assert.IsNotNull(foundInScene);
#if PRIME_TWEEN_INSPECTOR_DEBUGGING
Debug.LogError("PRIME_TWEEN_INSPECTOR_DEBUGGING doesn't work with 'Recompile And Continue Playing' because Tween.id is serializable but Tween.tween is not.");
return;
#endif
var count = foundInScene.tweensCount;
if (count > 0) {
Debug.Log($"All tweens ({count}) were stopped because of 'Recompile And Continue Playing'.");
}
foundInScene.init(foundInScene.currentPoolCapacity);
Instance = foundInScene;
}
void Reset() {
Assert.IsFalse(Application.isPlaying);
Debug.LogError(manualInstanceCreationIsNotAllowedMessage);
DestroyImmediate(this);
}
#endif
void Start() {
#if SAFETY_CHECKS
// Selection.activeGameObject = gameObject;
#endif
Assert.AreEqual(Instance, this, manualInstanceCreationIsNotAllowedMessage);
}
internal void FixedUpdate() => UpdateTweens(_UpdateType.FixedUpdate);
///
/// The most common tween lifecycle:
/// 1. User's script creates a tween in Update() in frame N.
/// 2. PrimeTweenManager.LateUpdate() applies the 'startValue' to the tween in the SAME FRAME N. This guarantees that the animation is rendered at the 'startValue' in the same frame the tween is created.
/// 3. PrimeTweenManager.Update() executes the first animation step on frame N+1. PrimeTweenManager's execution order is -2000, this means that
/// all tweens created in previous frames will already be updated before user's script Update() (if user's script execution order is greater than -2000).
/// 4. PrimeTweenManager.Update() completes the tween on frame N+(duration*targetFrameRate) given that targetFrameRate is stable.
///
internal void Update() => UpdateTweens(_UpdateType.Update);
void update(List tweens, float deltaTime, float unscaledDeltaTime, out int processedCount, int? maxCount = null) {
if (updateDepth != 0) {
throw new Exception("updateDepth != 0");
}
updateDepth++;
// onComplete and onValueChange can create new tweens. Cache count to process only those tweens that were present when the update started
int oldCount = maxCount < tweens.Count ? maxCount.Value : tweens.Count;
var numRemoved = 0;
// Process tweens in the order of creation.
// This allows to create tween duplicates because the latest tween on the same value will overwrite the previous ones.
for (int i = 0; i < oldCount; i++) {
var tween = tweens[i];
var newIndex = i - numRemoved;
#if SAFETY_CHECKS
Assert.IsNotNull(tween);
if (numRemoved > 0) {
Assert.IsNull(tweens[newIndex]);
}
#endif
// ReSharper disable once PossibleNullReferenceException
// delay release for one frame if coroutineEnumerator.resetEnumerator()
if (tween.updateAndCheckIfRunning(tween.settings.useUnscaledTime ? unscaledDeltaTime : deltaTime) || tween.coroutineEnumerator.resetEnumerator()) {
if (i != newIndex) {
tweens[i] = null;
tweens[newIndex] = tween;
}
continue;
}
releaseTweenToPool(tween);
tweens[i] = null; // set to null after releaseTweenToPool() so in case of an exception, the tween will stay inspectable via Inspector
numRemoved++;
}
processedCount = oldCount - numRemoved;
#if SAFETY_CHECKS
for (int i = oldCount - numRemoved; i < oldCount; i++) { // Check removed tweens are shifted to the left and are null
Assert.IsNull(tweens[i]);
}
for (int i = oldCount; i < tweens.Count; i++) { // Check all newly created tweens are not null
Assert.IsNotNull(tweens[i]);
}
#endif
updateDepth--;
if (numRemoved != 0) {
var newCount = tweens.Count;
for (int i = oldCount; i < newCount; i++) {
var tween = tweens[i];
var newIndex = i - numRemoved;
#if SAFETY_CHECKS
Assert.IsNotNull(tween);
#endif
tweens[newIndex] = tween;
}
tweens.RemoveRange(newCount - numRemoved, numRemoved);
Assert.AreEqual(tweens.Count, newCount - numRemoved);
#if SAFETY_CHECKS
foreach (var t in tweens) {
Assert.IsNotNull(t);
}
// Check no duplicates
hashSet.Clear();
hashSet.UnionWith(tweens);
Assert.AreEqual(hashSet.Count, tweens.Count);
#endif
}
}
#if SAFETY_CHECKS
readonly HashSet hashSet = new HashSet();
#endif
internal void LateUpdate() {
UpdateTweens(_UpdateType.LateUpdate);
ApplyStartValues(_UpdateType.Update);
ApplyStartValues(_UpdateType.LateUpdate);
}
internal void ApplyStartValues(_UpdateType updateType) {
switch (updateType) {
case _UpdateType.Default:
Debug.LogError("Please provide non-default update type.");
break;
case _UpdateType.Update:
ApplyStartValuesInternal(tweens, processedCount);
break;
case _UpdateType.LateUpdate:
ApplyStartValuesInternal(lateUpdateTweens, lateUpdateTweensProcessedCount);
break;
case _UpdateType.FixedUpdate:
ApplyStartValuesInternal(fixedUpdateTweens, fixedUpdateTweensProcessedCount);
break;
default: throw new Exception($"Invalid update type: {updateType}");
}
void ApplyStartValuesInternal(List list, int processedCount) {
updateDepth++;
int cachedCount = list.Count;
for (int i = processedCount; i < cachedCount; i++) {
var tween = list[i];
// ReSharper disable once PossibleNullReferenceException
if (tween._isAlive && !tween.startFromCurrent && tween.settings.startDelay == 0 && !tween.isUnityTargetDestroyed() && !tween.isAdditive
&& tween.canManipulate()
&& tween.elapsedTimeTotal == 0f) {
tween.SetElapsedTimeTotal(0f);
}
}
updateDepth--;
}
}
internal void UpdateTweens(_UpdateType updateType, float? deltaTime = null, float? unscaledDeltaTime = null) {
switch (updateType) {
case _UpdateType.Default:
Debug.LogError("Please provide non-default update type.");
break;
case _UpdateType.Update:
update(tweens, deltaTime ?? Time.deltaTime, unscaledDeltaTime ?? Time.unscaledDeltaTime, out processedCount);
break;
case _UpdateType.LateUpdate:
update(lateUpdateTweens, deltaTime ?? Time.deltaTime, unscaledDeltaTime ?? Time.unscaledDeltaTime, out lateUpdateTweensProcessedCount, maxLateUpdateCount);
maxLateUpdateCount = lateUpdateTweens.Count;
break;
case _UpdateType.FixedUpdate:
update(fixedUpdateTweens, deltaTime ?? Time.fixedDeltaTime, unscaledDeltaTime ?? Time.fixedUnscaledDeltaTime, out fixedUpdateTweensProcessedCount);
break;
default: throw new Exception($"Invalid update type: {updateType}");
}
}
void releaseTweenToPool([NotNull] ReusableTween tween) {
#if SAFETY_CHECKS
checkNotInSequence(tweens);
checkNotInSequence(lateUpdateTweens);
checkNotInSequence(fixedUpdateTweens);
void checkNotInSequence(List list) {
foreach (var t in list) {
if (t != null) {
Assert.AreNotEqual(tween.id, t.next.id);
Assert.AreNotEqual(tween.id, t.nextSibling.id);
Assert.AreNotEqual(tween.id, t.prev.id);
}
}
}
#endif
tween.Reset();
pool.Add(tween);
}
/// Returns null if target is a destroyed UnityEngine.Object
internal static Tween? delayWithoutDurationCheck([CanBeNull] object target, float duration, bool useUnscaledTime) {
#if UNITY_EDITOR
if (Constants.warnNoInstance) {
return null;
}
#endif
var tween = fetchTween();
var settings = new TweenSettings {
duration = duration,
ease = Ease.Linear,
useUnscaledTime = useUnscaledTime
};
tween.Setup(target, ref settings, _ => {}, null, false, TweenType.Delay);
var result = addTween(tween);
// ReSharper disable once RedundantCast
return result.IsCreated ? result : (Tween?)null;
}
[NotNull]
internal static ReusableTween fetchTween() {
#if UNITY_EDITOR
if (Constants.warnNoInstance) {
return new ReusableTween();
}
#endif
return Instance.fetchTween_internal();
}
[NotNull]
ReusableTween fetchTween_internal() {
ReusableTween result;
if (pool.Count == 0) {
result = new ReusableTween();
if (tweensCount + 1 > currentPoolCapacity) {
var newCapacity = currentPoolCapacity == 0 ? 4 : currentPoolCapacity * 2;
Debug.LogWarning($"Tweens capacity has been increased from {currentPoolCapacity} to {newCapacity}. Please increase the capacity manually to prevent memory allocations at runtime by calling {Constants.setTweensCapacityMethod}.\n" +
$"To know the highest number of simultaneously running tweens, please observe the '{nameof(PrimeTweenManager)}/{Constants.maxAliveTweens}' in Inspector.\n");
currentPoolCapacity = newCapacity;
}
} else {
var lastIndex = pool.Count - 1;
result = pool[lastIndex];
pool.RemoveAt(lastIndex);
}
Assert.AreEqual(-1, result.id);
result.id = lastId;
return result;
}
internal static Tween Animate([NotNull] ReusableTween tween) {
checkDuration(tween.target, tween.settings.duration);
return addTween(tween);
}
internal static void checkDuration([CanBeNull] T target, float duration) where T : class {
#if UNITY_EDITOR
if (Constants.noInstance) {
return;
}
#endif
if (Instance.warnZeroDuration && duration <= 0) {
Debug.LogWarning($"Tween duration ({duration}) <= 0. {Constants.buildWarningCanBeDisabledMessage(nameof(warnZeroDuration))}", target as UnityEngine.Object);
}
}
internal static Tween addTween([NotNull] ReusableTween tween) {
#if UNITY_EDITOR
if (Constants.noInstance) {
return default;
}
#endif
return Instance.addTween_internal(tween);
}
Tween addTween_internal([NotNull] ReusableTween tween) {
Assert.IsNotNull(tween);
Assert.IsTrue(tween.id > 0);
if (tween.target == null || tween.isUnityTargetDestroyed()) {
Debug.LogError($"Tween's target is null: {tween.GetDescription()}. This error can mean that:\n" +
"- The target reference is null.\n" +
"- UnityEngine.Object target reference is not populated in the Inspector.\n" +
"- UnityEngine.Object target has been destroyed.\n" +
"Please ensure you're using a valid target.\n");
tween.kill();
releaseTweenToPool(tween);
return default;
}
if (warnTweenOnDisabledTarget) {
if (tween.target is Component comp && !comp.gameObject.activeInHierarchy) {
Debug.LogWarning($"Tween is started on GameObject that is not active in hierarchy: {comp.name}. {Constants.buildWarningCanBeDisabledMessage(nameof(warnTweenOnDisabledTarget))}", comp);
}
}
if (tween.settings._updateType == _UpdateType.Default) {
tween.settings._updateType = defaultUpdateType;
}
switch (tween.settings._updateType) {
case _UpdateType.Update:
tweens.Add(tween);
break;
case _UpdateType.LateUpdate:
lateUpdateTweens.Add(tween);
break;
case _UpdateType.FixedUpdate:
fixedUpdateTweens.Add(tween);
break;
default:
Debug.LogError($"Invalid update type: {tween.settings._updateType}");
return default;
}
#if SAFETY_CHECKS
// Debug.Log($"[{Time.frameCount}] created: {tween.GetDescription()}", tween.unityTarget);
StackTraces.Record(tween.id);
#endif
lastId++; // increment only when tween added successfully
#if UNITY_ASSERTIONS && !PRIME_TWEEN_DISABLE_ASSERTIONS
maxSimultaneousTweensCount = Math.Max(maxSimultaneousTweensCount, tweensCount);
if (warnBenchmarkWithAsserts && maxSimultaneousTweensCount > 50000) {
warnBenchmarkWithAsserts = false;
var msg = "PrimeTween detected more than 50000 concurrent tweens. If you're running benchmarks, please add the PRIME_TWEEN_DISABLE_ASSERTIONS to the 'ProjectSettings/Player/Script Compilation' to disable assertions. This will ensure PrimeTween runs with the release performance.\n" +
"Also disable optional convenience features: PrimeTweenConfig.warnZeroDuration and PrimeTweenConfig.warnTweenOnDisabledTarget.\n";
if (Application.isEditor) {
msg += "Please also run the tests in real builds, not in the Editor, to measure the performance correctly.\n";
}
msg += $"{Constants.buildWarningCanBeDisabledMessage(nameof(PrimeTweenConfig.warnBenchmarkWithAsserts))}\n";
Debug.LogError(msg);
}
#endif
return new Tween(tween);
}
internal static int processAll([CanBeNull] object onTarget, [NotNull] Predicate predicate, bool allowToProcessTweensInsideSequence) {
#if UNITY_EDITOR
if (Constants.warnNoInstance) {
return default;
}
#endif
return Instance.processAll_internal(onTarget, predicate, allowToProcessTweensInsideSequence);
}
internal static bool logCantManipulateError = true;
int processAll_internal([CanBeNull] object onTarget, [NotNull] Predicate predicate, bool allowToProcessTweensInsideSequence) {
return processInList(tweens) + processInList(lateUpdateTweens) + processInList(fixedUpdateTweens);
int processInList(List tweens) {
int numProcessed = 0;
int totalCount = 0;
var count = tweens.Count; // this is not an optimization, OnComplete() may create new tweens
for (var i = 0; i < count; i++) {
var tween = tweens[i];
if (tween == null) {
continue;
}
totalCount++;
if (onTarget != null) {
if (tween.target != onTarget) {
continue;
}
if (!allowToProcessTweensInsideSequence && tween.IsInSequence()) {
// To support stopping sequences by target, I can add new API 'Sequence.Create(object sequenceTarget)'.
// But 'sequenceTarget' is a different concept to tween's target, so I should not mix these two concepts together:
// 'sequenceTarget' serves the purpose of unique 'id', while tween's target is the animated object.
// In my opinion, the benefits of this new API don't outweigh the added complexity. A much more simpler approach is to store the Sequence reference and call sequence.Stop() directly.
Assert.IsFalse(tween.isMainSequenceRoot());
if (logCantManipulateError) {
Assert.LogError(Constants.cantManipulateNested, tween.id);
}
continue;
}
}
if (tween._isAlive && predicate(tween)) {
numProcessed++;
}
}
if (onTarget == null) {
return totalCount;
}
return numProcessed;
}
}
internal void SetTweensCapacity(int capacity) {
var runningTweens = tweensCount;
if (capacity < runningTweens) {
Debug.LogError($"New capacity ({capacity}) should be greater than the number of currently running tweens ({runningTweens}).\n" +
$"You can use {nameof(Tween)}.{nameof(Tween.StopAll)}() to stop all running tweens.");
return;
}
tweens.Capacity = capacity;
lateUpdateTweens.Capacity = capacity;
fixedUpdateTweens.Capacity = capacity;
#if UNITY_2021_2_OR_NEWER
shakes.EnsureCapacity(capacity);
#endif
resizeAndSetCapacity(pool, capacity - runningTweens, capacity);
currentPoolCapacity = capacity;
Assert.AreEqual(capacity, tweens.Capacity);
Assert.AreEqual(capacity, lateUpdateTweens.Capacity);
Assert.AreEqual(capacity, fixedUpdateTweens.Capacity);
Assert.AreEqual(capacity, pool.Capacity);
}
internal int tweensCount => tweens.Count + lateUpdateTweens.Count + fixedUpdateTweens.Count;
internal static void resizeAndSetCapacity([NotNull] List list, int newCount, int newCapacity) {
Assert.IsTrue(newCapacity >= newCount);
int curCount = list.Count;
if (curCount > newCount) {
var numToRemove = curCount - newCount;
list.RemoveRange(newCount, numToRemove);
list.Capacity = newCapacity;
} else {
list.Capacity = newCapacity;
if (newCount > curCount) {
var numToCreate = newCount - curCount;
for (int i = 0; i < numToCreate; i++) {
list.Add(new ReusableTween());
}
}
}
Assert.AreEqual(newCount, list.Count);
Assert.AreEqual(newCapacity, list.Capacity);
}
[Conditional("UNITY_ASSERTIONS")]
internal void warnStructBoxingInCoroutineOnce(long id) {
if (!warnStructBoxingAllocationInCoroutine) {
return;
}
warnStructBoxingAllocationInCoroutine = false;
Assert.LogWarning("Please use Tween/Sequence." + nameof(Tween.ToYieldInstruction) + "() when waiting for a Tween/Sequence in coroutines to prevent struct boxing.\n" +
Constants.buildWarningCanBeDisabledMessage(nameof(PrimeTweenConfig.warnStructBoxingAllocationInCoroutine)) + "\n", id);
}
}
}