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