using UnityEngine; using System.Collections.Generic; using System; namespace Watermelon { /// /// Manages a flat array of objects for a single Unity update loop /// (, , or ). /// /// Design highlights: /// /// Killed tweens are collected in a deferred list and removed after the update loop /// to avoid modifying the array during iteration. /// The array is compacted lazily — gaps left by removed tweens are filled only when a new tween /// is added or at the start of the next update. /// The backing array doubles in capacity when full, avoiding frequent reallocations. /// /// /// public class TweensHolder { protected TweenCase[] tweens; /// /// Raw backing array of active tweens. Slots may be null if a tween was recently removed /// but the array has not yet been compacted. /// public TweenCase[] Tweens => tweens; protected int tweensCount; protected bool hasActiveTweens = false; protected bool requiresActiveReorganization = false; protected int reorganizeFromID = -1; protected int maxActiveLookupID = -1; protected List killingTweens = new List(); #if UNITY_EDITOR protected int maxTweensAmount = 0; #endif protected UpdateMethod updateMethod; /// /// Creates a new holder for the specified update loop with an initial backing array of size . /// The array doubles automatically when capacity is exceeded. /// /// Unity update loop this holder is tied to. /// Initial capacity of the tween array. Tune to the expected peak active count to avoid reallocations. public TweensHolder(UpdateMethod updateMethod, int defaultAmount) { this.updateMethod = updateMethod; tweens = new TweenCase[defaultAmount]; } /// /// Registers a in this holder and marks it as active. /// Doubles the backing array if it is full, and compacts any pending gaps before inserting. /// public void AddTween(TweenCase tween) { if (tweensCount >= tweens.Length) { Array.Resize(ref tweens, tweens.Length * 2); LogManager.LogWarning("[Tween]: The amount of the tweens (" + updateMethod + ") was adjusted. Current size - " + tweens.Length + ". Change the default amount to prevent performance leak!", LogCategory.Systems); } if (requiresActiveReorganization) ReorganizeUpdateActiveTweens(); tween.IsActive = true; tween.ActiveID = (maxActiveLookupID = tweensCount); tweens[tweensCount] = tween; tweensCount++; hasActiveTweens = true; #if UNITY_EDITOR if (maxTweensAmount < tweensCount) maxTweensAmount = tweensCount; #endif } /// /// Compacts the tween array by shifting active tweens left to fill gaps left by removed tweens. /// Called lazily before or at the start of when required. /// private void ReorganizeUpdateActiveTweens() { if (tweensCount <= 0) { maxActiveLookupID = -1; reorganizeFromID = -1; requiresActiveReorganization = false; return; } if (reorganizeFromID == maxActiveLookupID) { maxActiveLookupID--; reorganizeFromID = -1; requiresActiveReorganization = false; return; } int defaultOffset = 1; int tweensTempCount = maxActiveLookupID + 1; maxActiveLookupID = reorganizeFromID - 1; for (int i = reorganizeFromID + 1; i < tweensTempCount; i++) { TweenCase tween = tweens[i]; if (tween != null) { tween.ActiveID = (maxActiveLookupID = i - defaultOffset); tweens[i - defaultOffset] = tween; tweens[i] = null; } else { defaultOffset++; } } requiresActiveReorganization = false; reorganizeFromID = -1; } /// /// Advances all active, non-paused tweens by the appropriate delta time, validates each tween, /// and processes natural completions. At the end of the loop, all tweens marked for killing are removed. /// /// Scaled delta time (Time.deltaTime or Time.fixedDeltaTime). /// Unscaled delta time (Time.unscaledDeltaTime or Time.fixedUnscaledDeltaTime). public void Update(float deltaTime, float unscaledDeltaTime) { if (!hasActiveTweens) return; if (requiresActiveReorganization) ReorganizeUpdateActiveTweens(); for (int i = 0; i < tweensCount; i++) { TweenCase tween = tweens[i]; if (tween != null) { if (!tween.Validate()) { tween.Kill(); } else { if (tween.IsActive && !tween.IsPaused) { if (!tween.IsUnscaled) { if (Time.timeScale == 0) continue; if (tween.Delay > 0 && tween.Delay > tween.CurrentDelay) { tween.UpdateDelay(deltaTime); } else { tween.UpdateState(deltaTime); tween.Invoke(deltaTime); } } else { if (tween.Delay > 0 && tween.Delay > tween.CurrentDelay) { tween.UpdateDelay(unscaledDeltaTime); } else { tween.UpdateState(unscaledDeltaTime); tween.Invoke(unscaledDeltaTime); } } // !IsKilling: skip if Complete() was already called inside Invoke() if (tween.IsCompleted && !tween.IsKilling) { tween.DefaultComplete(); tween.InvokeCompleteEvent(); tween.Kill(); } } } } } int killingTweensCount = killingTweens.Count; for (int i = 0; i < killingTweensCount; i++) { RemoveActiveTween(killingTweens[i]); } killingTweens.Clear(); } /// /// Physically removes a tween from the array, decrements the active count, schedules a compaction pass, /// and calls so the tween can be restarted if needed. /// private void RemoveActiveTween(TweenCase tween) { int activeId = tween.ActiveID; if (activeId < 0) return; tween.ActiveID = -1; tween.ResetKillingState(); requiresActiveReorganization = true; if (reorganizeFromID == -1 || reorganizeFromID > activeId) { reorganizeFromID = activeId; } tweens[activeId] = null; tweensCount--; hasActiveTweens = (tweensCount > 0); } /// Pauses all currently active tweens in this holder. public void Pause() { for (int i = 0; i < tweensCount; i++) { TweenCase tween = tweens[i]; if (tween != null) { tween.Pause(); } } } /// Resumes all paused tweens in this holder. public void Resume() { for (int i = 0; i < tweensCount; i++) { TweenCase tween = tweens[i]; if (tween != null) { tween.Resume(); } } } /// Kills all active tweens in this holder. OnComplete callbacks are not fired. public void Kill() { for (int i = 0; i < tweensCount; i++) { TweenCase tween = tweens[i]; if (tween != null) { tween.Kill(); } } } /// /// Enqueues for removal at the end of the current call. /// Deferred removal prevents modifying the array while it is being iterated. /// Called by . /// public void MarkForKilling(TweenCase tween) { killingTweens.Add(tween); } /// /// Clears all tween references and resets all counters, returning the holder to its initial state. /// Called when the scene unloads or the MonoBehaviour is destroyed. /// public void Unload() { if(!tweens.IsNullOrEmpty()) { int clearCount = maxActiveLookupID + 1; for (int i = 0; i < clearCount; i++) { if (tweens[i] != null) { // Reset so any deferred Kill() calls (e.g. from OnDisable after KillAll) // hit the activeId < 0 early-return in RemoveActiveTween and don't corrupt // the next session's tween array. tweens[i].IsActive = false; tweens[i].ActiveID = -1; tweens[i] = null; } } } killingTweens.Clear(); tweensCount = 0; hasActiveTweens = false; requiresActiveReorganization = false; reorganizeFromID = -1; maxActiveLookupID = -1; } } }