EscapeShooter/My project/Assets/Project Files/Modules/Tween/Scripts/Runtime/TweenCase.cs

390 lines
16 KiB
C#
Raw Normal View History

using System.Collections.Generic;
using UnityEngine;
namespace Watermelon
{
/// <summary>
/// Abstract base class for all tween animations.
/// Encapsulates lifecycle (start, pause, resume, kill, complete), easing, delay,
/// progress tracking, and callback registration.
///
/// <para><b>Lifecycle:</b>
/// <list type="number">
/// <item><term>Create</term><description>Instantiate a concrete subclass and configure it with the fluent builder methods.</description></item>
/// <item><term>Start</term><description>Call <see cref="StartTween"/> to register the tween with <see cref="Tween"/> and begin updating.</description></item>
/// <item><term>Update</term><description><see cref="TweensHolder"/> calls <see cref="UpdateState"/> and <see cref="Invoke"/> each frame.</description></item>
/// <item><term>End</term><description>Either <see cref="Complete"/> (applies the final value and fires callbacks) or <see cref="Kill"/> (no final value, no callbacks).</description></item>
/// </list>
/// </para>
///
/// <para><b>Implementing a custom tween:</b>
/// Override <see cref="Invoke"/> to interpolate the value each frame,
/// <see cref="DefaultComplete"/> to snap to the final value on completion, and
/// <see cref="Validate"/> to return <c>false</c> when the target object is no longer valid.
/// </para>
/// </summary>
public abstract class TweenCase
{
// Managed by TweensHolder — internal set only
/// <summary>Index of this tween inside its <see cref="TweensHolder"/> array. -1 when inactive.</summary>
public int ActiveID { get; internal set; }
/// <summary>Whether the tween is currently registered and updating in a <see cref="TweensHolder"/>.</summary>
public bool IsActive { get; internal set; }
// Private: only read/written inside TweenCase
private float currentDelay;
private float delay;
private float duration;
private int updateMethodIndex;
private bool isPaused;
private bool isUnscaled;
private bool isCompleted;
private bool isKilling;
private Ease.IEasingFunction easeFunction;
private SimpleCallback completedCallback;
private SimpleCallback terminatedCallback;
private List<CallbackData> callbackData;
// Protected: written by concrete case constructors
/// <summary>Optional target object. Used by <see cref="Validate"/> in subclasses to detect destroyed Unity objects.</summary>
protected GameObject parentObject;
// Protected: read by concrete Invoke() implementations via Interpolate(state)
/// <summary>
/// Normalized progress of the tween in the range [0, 1].
/// Read by concrete <see cref="Invoke"/> implementations via <see cref="Interpolate"/>.
/// </summary>
protected float state;
// Public read-only surface
/// <summary>Elapsed delay time (seconds) since the tween started. Counts up until it reaches <see cref="Delay"/>.</summary>
public float CurrentDelay => currentDelay;
/// <summary>Initial delay (seconds) before the tween begins animating.</summary>
public float Delay => delay;
/// <summary>Normalized progress [0, 1]. Advances each frame after the delay has elapsed.</summary>
public float State => state;
/// <summary>Total animation duration in seconds (excluding delay).</summary>
public float Duration => duration;
/// <summary>Whether the tween is currently paused. Paused tweens are skipped by <see cref="TweensHolder"/> but remain registered.</summary>
public bool IsPaused => isPaused;
/// <summary>When <c>true</c>, the tween uses <c>Time.unscaledDeltaTime</c> and is unaffected by <c>Time.timeScale</c>.</summary>
public bool IsUnscaled => isUnscaled;
/// <summary>Whether the tween has finished animating (state reached 1, or <see cref="Complete"/> was called).</summary>
public bool IsCompleted => isCompleted;
/// <summary>
/// Whether the tween has been marked for removal but not yet physically removed from the array.
/// While <c>true</c>, <see cref="Restart"/> is a no-op. Cleared by <see cref="ResetKillingState"/> after removal.
/// </summary>
public bool IsKilling => isKilling;
/// <summary>Array index of the owning <see cref="TweensHolder"/> inside <see cref="TweenManager.Holders"/>.</summary>
public int UpdateMethodIndex => updateMethodIndex;
/// <summary>Unity update loop in which this tween runs.</summary>
public UpdateMethod UpdateMethod => (UpdateMethod)updateMethodIndex;
/// <summary>The GameObject this tween is attached to, or <c>null</c> for system (non-transform) tweens.</summary>
public GameObject ParentObject => parentObject;
/// <summary>Initialises the tween with a <see cref="Ease.Type.Linear"/> easing function.</summary>
public TweenCase()
{
SetEasing(Ease.Type.Linear);
}
/// <summary>Registers the tween with the global <see cref="Tween"/> system and begins updating. Returns <c>this</c> for chaining.</summary>
public virtual TweenCase StartTween()
{
Tween.AddTween(this);
return this;
}
/// <summary>
/// Called each frame to check whether the tween's target object is still valid.
/// Return <c>false</c> to automatically kill the tween (e.g. when a GameObject has been destroyed).
/// </summary>
public abstract bool Validate();
// -----------------------------------------------------------
// Lifecycle
// -----------------------------------------------------------
/// <summary>
/// Stops and removes the tween without applying the final value or firing <see cref="OnComplete"/>.
/// Fires the internal terminated callback used by <see cref="TweenCaseCollection"/>.
/// Idempotent — safe to call multiple times.
/// </summary>
public TweenCase Kill()
{
if (!isKilling)
{
IsActive = false;
Tween.MarkForKilling(this);
isKilling = true;
completedCallback = null;
terminatedCallback?.Invoke();
terminatedCallback = null;
}
return this;
}
/// <summary>
/// Immediately completes the tween: snaps to the final value via <see cref="DefaultComplete"/>,
/// fires the <see cref="OnComplete"/> callback, then calls <see cref="Kill"/>.
/// Idempotent — subsequent calls are no-ops.
/// </summary>
public TweenCase Complete()
{
if (isKilling || isCompleted) return this;
if (isPaused) isPaused = false;
state = 1;
isCompleted = true;
DefaultComplete();
InvokeCompleteEvent();
Kill();
return this;
}
/// <summary>
/// Restarts the tween from the beginning (resets progress and delay to 0).
/// If the tween has already ended, re-registers it with the system via <see cref="StartTween"/>.
/// Has no effect while <see cref="Kill"/> is mid-processing (<see cref="IsKilling"/> is <c>true</c>).
/// </summary>
public TweenCase Restart()
{
if (isKilling) return this;
state = 0;
currentDelay = 0;
isCompleted = false;
isPaused = false;
if (!IsActive)
StartTween();
return this;
}
/// <summary>Pauses the tween. The tween remains registered but is skipped by the update loop until <see cref="Resume"/> is called.</summary>
public TweenCase Pause()
{
isPaused = true;
return this;
}
/// <summary>Resumes a paused tween so it continues updating from where it stopped.</summary>
public TweenCase Resume()
{
isPaused = false;
return this;
}
// -----------------------------------------------------------
// Builder (fluent API)
// -----------------------------------------------------------
/// <summary>Sets a delay (seconds) before the tween begins animating. Must be called before <see cref="StartTween"/>.</summary>
public TweenCase SetDelay(float delay)
{
this.delay = delay;
currentDelay = 0;
return this;
}
/// <summary>
/// Sets which Unity update loop drives this tween (<see cref="UpdateMethod.Update"/>,
/// <see cref="UpdateMethod.FixedUpdate"/>, or <see cref="UpdateMethod.LateUpdate"/>).
/// Must be called before <see cref="StartTween"/>.
/// </summary>
public TweenCase SetUpdateMethod(UpdateMethod updateMethod)
{
updateMethodIndex = (int)updateMethod;
return this;
}
/// <summary>
/// When <c>true</c>, the tween uses <c>Time.unscaledDeltaTime</c> and ignores <c>Time.timeScale</c>.
/// Useful for UI animations that must run during paused gameplay.
/// </summary>
public TweenCase SetUnscaledMode(bool isUnscaled)
{
this.isUnscaled = isUnscaled;
return this;
}
/// <summary>Sets one of the built-in easing curves from <see cref="Ease.Type"/>. Default is <see cref="Ease.Type.Linear"/>.</summary>
public TweenCase SetEasing(Ease.Type ease)
{
easeFunction = Ease.GetFunction(ease);
return this;
}
/// <summary>Sets a custom easing curve via an <see cref="AnimationCurve"/>. The curve is sampled over its total time range.</summary>
public TweenCase SetCurveEasing(AnimationCurve easingCurve)
{
easeFunction = new AnimationCurveEasingFunction(easingCurve);
return this;
}
/// <summary>Sets a fully custom easing function implementing <see cref="Ease.IEasingFunction"/>.</summary>
public TweenCase SetCustomEasing(Ease.IEasingFunction easeFunction)
{
this.easeFunction = easeFunction;
return this;
}
/// <summary>Sets the total animation duration in seconds (excluding delay).</summary>
public TweenCase SetDuration(float duration)
{
this.duration = duration;
return this;
}
// -----------------------------------------------------------
// Callbacks
// -----------------------------------------------------------
/// <summary>
/// Registers a callback invoked when the tween completes naturally or via <see cref="Complete"/>.
/// Only one callback is stored — each call replaces the previous one (last call wins).
/// <b>Not</b> fired when the tween is stopped via <see cref="Kill"/>.
/// </summary>
public TweenCase OnComplete(SimpleCallback callback)
{
completedCallback = callback;
return this;
}
/// <summary>
/// Registers a callback fired when the tween ends for any reason — both <see cref="Complete"/> and <see cref="Kill"/>.
/// Used internally by <see cref="TweenCaseCollection"/>. Always has at most one subscriber.
/// </summary>
internal TweenCase OnTerminated(SimpleCallback callback)
{
terminatedCallback = callback;
return this;
}
/// <summary>
/// Registers a one-shot callback fired when the normalized progress reaches <paramref name="t"/> (range 0–1).
/// Multiple thresholds can be registered; each fires exactly once and is then discarded.
/// </summary>
/// <param name="t">Normalized time threshold in [0, 1].</param>
/// <param name="callback">The callback to invoke when the threshold is crossed.</param>
public TweenCase OnTimeReached(float t, SimpleCallback callback)
{
callbackData ??= new List<CallbackData>();
callbackData.Add(new CallbackData(t, callback));
return this;
}
// -----------------------------------------------------------
// Internal — called only by TweensHolder or TweenCase itself
// -----------------------------------------------------------
/// <summary>
/// Applies the easing function to the raw progress value <paramref name="p"/>.
/// Call this inside <see cref="Invoke"/> to obtain the eased interpolation factor.
/// </summary>
/// <param name="p">Raw normalized progress in [0, 1].</param>
/// <returns>Eased value, typically in [0, 1] (may overshoot for elastic/back easings).</returns>
public float Interpolate(float p) => easeFunction.Interpolate(p);
/// <summary>Advances the delay timer by <paramref name="deltaTime"/>. Called by <see cref="TweensHolder"/> while the delay has not yet elapsed.</summary>
internal void UpdateDelay(float deltaTime)
{
currentDelay += deltaTime;
}
/// <summary>
/// Advances <see cref="State"/> by <c>deltaTime / duration</c>,
/// fires any <see cref="OnTimeReached"/> callbacks whose threshold was crossed,
/// and sets <see cref="IsCompleted"/> when state reaches 1.
/// Called by <see cref="TweensHolder"/> every frame after the delay has elapsed.
/// </summary>
internal void UpdateState(float deltaTime)
{
state += Mathf.Min(1.0f, deltaTime / duration);
// Check OnTimeReached callbacks independently — even if state jumped past 1.
if (!callbackData.IsNullOrEmpty())
{
for (int i = 0; i < callbackData.Count; i++)
{
var data = callbackData[i];
if (state >= data.t)
{
data.callback?.Invoke();
callbackData.RemoveAt(i);
i--;
}
}
}
if (state >= 1)
isCompleted = true;
}
/// <summary>Invokes the registered <see cref="OnComplete"/> callback. Called by <see cref="TweensHolder"/> after the tween completes naturally.</summary>
internal void InvokeCompleteEvent()
{
completedCallback?.Invoke();
}
/// <summary>
/// Called by <see cref="TweensHolder"/> after the tween has been fully removed from its array.
/// Resets <see cref="IsKilling"/> so the tween can be safely <see cref="Restart"/>ed.
/// </summary>
internal void ResetKillingState()
{
isKilling = false;
}
// -----------------------------------------------------------
// Abstract — implemented by concrete tween cases
// -----------------------------------------------------------
/// <summary>
/// Called every frame while the tween is active and not paused (after any delay has elapsed).
/// Implement the actual value interpolation here using <see cref="Interpolate"/>(<see cref="state"/>).
/// </summary>
/// <param name="deltaTime">Elapsed time since the last frame (scaled or unscaled, matching <see cref="IsUnscaled"/>).</param>
public abstract void Invoke(float deltaTime);
/// <summary>
/// Called when the tween finishes naturally or via <see cref="Complete"/>.
/// Snap the animated value to its final state here to guarantee the animation reaches its target.
/// <b>Not</b> called when the tween is stopped via <see cref="Kill"/>.
/// </summary>
public abstract void DefaultComplete();
// -----------------------------------------------------------
// Helpers
// -----------------------------------------------------------
private readonly struct CallbackData
{
public readonly float t;
public readonly SimpleCallback callback;
public CallbackData(float t, SimpleCallback callback)
{
this.t = t;
this.callback = callback;
}
}
}
}