Tween Sequencing
Compose tweens into ordered and parallel, pause-aware timelines with one fluent chain.
Chain tweens, delays, and callbacks into one pause-aware timeline.
| Interface | ITweenSequenceService |
| Off switch | NullTweenSequenceService |
| Assembly | CommonGameSystem.Core |
| Startup | Registered automatically at boot (one of the 23 services) — no setup needed |
What it does
The Tween Sequencing service builds multi-step animation timelines with a single fluent chain. You can append tweens one after another, overlap them in parallel, insert delays, and fire callbacks along the way. Each step's duration is declared up front, matching the duration you pass to the Tween service. The sequence computes its exact timing when it starts — it never polls the individual tweens.
Playback follows a time clock: pause the game, and a sequence bound to Clock.Gameplay (the default) pauses with it. You can see sequencing in motion in the visible demo scene, Demo/MotionLab.unity, which ships with the package.
Quick start
using System;
using CommonGameSystem.Core;
using UnityEngine;
[DefaultExecutionOrder(100)] // Run after the framework has started.
public class MyFirstSequence : MonoBehaviour
{
private void Start()
{
ITweenSequenceService sequences = ServiceLocator.Resolve<ITweenSequenceService>();
ITweenService tween = ServiceLocator.Resolve<ITweenService>();
float value = 0f;
IDisposable handle = sequences.Create()
.Append(() => tween.To(0f, 1f, 3f, v => value = v), 3f) // step 1: 3 seconds
.AppendInterval(0.5f) // then wait half a second
.AppendCallback(() => Debug.Log("Done!")) // then fire a callback
.Play(); // start playback; keep the handle to cancel
}
}
In real code, resolve and cache the services in Awake() and keep the play handle in a field so you can cancel it — the full example below shows that pattern.
API reference
ITweenSequence — builder and handle
| Member | Notes |
|---|---|
ITweenSequence Append(Func<IDisposable> tweenFactory, float durationSeconds) | Add a tween that starts after the previous step finishes. Returns this for chaining. |
ITweenSequence Join(Func<IDisposable> tweenFactory, float durationSeconds) | Add a tween that runs in parallel with the previous step. Returns this. |
ITweenSequence AppendInterval(float seconds) | Add a pure delay (no tween). Returns this. |
ITweenSequence AppendCallback(Action callback) | Add a zero-duration callback at the current point in the timeline. It fires relative to the sequence, not at an absolute time. Returns this. |
IDisposable Play() | Freeze the builder and start playback. Returns a handle; call Dispose() on it to cancel. |
bool IsPlaying { get; } | true between Play() and completion or cancel. |
float Elapsed { get; } | Playhead time so far (clock-based, clamped to Duration at completion). |
float Duration { get; } | Total sequence length, computed when the sequence is built. |
event Action OnSequenceComplete | Fires exactly once on normal completion. Never fires on cancel. |
ITweenSequenceService — the factory
| Member | Notes |
|---|---|
ITweenSequence Create(SequenceOptions options = default) | Create a new, empty sequence builder. |
int ActiveSequenceCount { get; } | Number of live sequences (started, not yet complete or canceled). |
SequenceOptions — settings
| Member | Notes |
|---|---|
int MaxStepsPerSequence = 256 | Maximum steps (Append/Join/AppendInterval/AppendCallback combined) per sequence. Clamped to 1–4096. |
int MaxStepsPerTick = 64 | Maximum steps fired in one frame; guards against runaway loops. Clamped to 1–4096. |
int MaxConcurrentSequences = 0 | Maximum live sequences across the whole service; 0 means unlimited. Clamped to 0–65536. |
Clock DefaultClock = Clock.Gameplay | Which time clock drives the playhead. The default pauses when the game pauses. |
bool LetStepExceptionsPropagate = false | When false (default), exceptions thrown by step callbacks are caught and logged. When true, they propagate — useful while developing. |
bool LogSequenceLifecycle = false | When true, logs build/play/complete/cancel traces. These logs are always compiled in, even in release builds. |
Examples
A UI panel that slides in while fading in, then announces itself:
using System;
using CommonGameSystem.Core;
using UnityEngine;
[DefaultExecutionOrder(100)]
public class PanelTransition : MonoBehaviour
{
private ITweenSequenceService _seqService;
private ITweenService _tween;
private CanvasGroup _canvasGroup;
private RectTransform _rect;
private IDisposable _playingHandle;
private void Awake()
{
_seqService = ServiceLocator.Resolve<ITweenSequenceService>();
_tween = ServiceLocator.Resolve<ITweenService>();
_canvasGroup = GetComponent<CanvasGroup>();
_rect = GetComponent<RectTransform>();
}
public void PlayEntrance()
{
_playingHandle?.Dispose(); // cancel the previous run, if any
Vector2 startPos = _rect.anchoredPosition;
_playingHandle = _seqService.Create()
// The Tween service only computes the value; you apply it in onUpdate.
.Append(() => _tween.To(startPos, Vector2.zero, 0.3f, v => _rect.anchoredPosition = v), 0.3f)
.Join(() => _tween.To(_canvasGroup.alpha, 1f, 0.3f, a => _canvasGroup.alpha = a), 0.3f)
.AppendCallback(() => OnEntranceComplete())
.Play();
}
private void OnEntranceComplete() => Debug.Log("Panel in!");
private void OnDestroy() => _playingHandle?.Dispose(); // cleanup
}
Turning it off
ServiceLocator.Replace<ITweenSequenceService>(new NullTweenSequenceService());
This mutes all sequences. Create() returns a builder that discards its steps, Play() returns an empty handle, and callbacks never fire. Useful for tests that do not need animation, or for disabling motion on low-end machines. One warning is logged when the replacement is constructed, so the swap is never silent. Programming errors — a null tween factory, an invalid clock — still throw, so real mistakes stay visible.
Common pitfalls
- A duration mismatch is your bug. The sequence trusts the
durationSecondsyou pass toAppend/Join. It must match the actual duration of the tween your factory creates. Declare 3 seconds while the tween runs 2.8, and the timing drifts. The Tween service cannot report a tween's remaining time, so the sequence cannot check this — copy the same duration literal into both calls. AppendCallbackis relative, not absolute. It fires at the sequence's current timeline position, not at a wall-clock time. For "5 seconds after game start", use the Scheduler service instead.- The clock source is captured at startup. Sequences bind to
Clock.Gameplayby default. If you replace the Time service (ServiceLocator.Replace<ITimeService>(…)) after startup, sequencing keeps the original time source it was built with. - Cancel is not completion.
Dispose()on the play handle cancels the sequence, andOnSequenceCompletedoes not fire. Use the event only for success-path cleanup; cancellation is silent. - The builder freezes after
Play(). CallingAppend,Join,AppendInterval, orAppendCallbackafterPlay()throwsInvalidOperationException. Create a new sequence if you need to extend a timeline.