// 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); } } }