트윈 시퀀싱
트윈을 순차·병렬로 조합해 일시정지를 인식하는 타임라인을 하나의 플루언트 체인으로 만듭니다.
트윈, 딜레이, 콜백을 하나의 일시정지 인식 타임라인으로 체이닝하세요.
| 인터페이스 | ITweenSequenceService |
| 끄기 스위치 | NullTweenSequenceService |
| 어셈블리 | CommonGameSystem.Core |
| 시작 | 부팅 시 자동 등록 (23개 서비스 중 하나) — 별도 설정 불필요 |
하는 일
트윈 시퀀싱 서비스는 단일 플루언트 체인으로 여러 단계의 애니메이션 타임라인을 만듭니다. 트윈을 차례로 덧붙이고, 병렬로 겹치고, 딜레이를 끼워 넣고, 중간중간 콜백을 발생시킬 수 있습니다. 각 스텝의 지속 시간은 미리 선언하며, 트윈 서비스에 전달하는 지속 시간과 일치시킵니다. 시퀀스는 시작할 때 정확한 타이밍을 계산합니다 — 개별 트윈을 폴링하는 일은 없습니다.
재생은 타임 클록을 따릅니다: 게임을 일시정지하면 Clock.Gameplay(기본값)에 바인딩된 시퀀스도 함께 멈춥니다. 패키지에 포함된 눈으로 볼 수 있는 데모 씬 Demo/MotionLab.unity에서 시퀀싱이 움직이는 모습을 확인할 수 있습니다.
빠른 시작
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
}
}
실제 코드에서는 Awake()에서 서비스를 resolve해 캐시하고, 취소할 수 있도록 재생 핸들을 필드에 보관하세요 — 아래의 전체 예제가 그 패턴을 보여 줍니다.
API 레퍼런스
ITweenSequence — 빌더이자 핸들
| 멤버 | 설명 |
|---|---|
ITweenSequence Append(Func<IDisposable> tweenFactory, float durationSeconds) | 이전 스텝이 끝난 뒤 시작하는 트윈을 추가합니다. 체이닝을 위해 this를 반환합니다. |
ITweenSequence Join(Func<IDisposable> tweenFactory, float durationSeconds) | 이전 스텝과 병렬로 실행되는 트윈을 추가합니다. this를 반환합니다. |
ITweenSequence AppendInterval(float seconds) | 순수 딜레이(트윈 없음)를 추가합니다. this를 반환합니다. |
ITweenSequence AppendCallback(Action callback) | 타임라인의 현재 지점에 지속 시간 0의 콜백을 추가합니다. 절대 시각이 아니라 시퀀스 기준으로 발생합니다. this를 반환합니다. |
IDisposable Play() | 빌더를 동결하고 재생을 시작합니다. 핸들을 반환하며, 취소하려면 핸들의 Dispose()를 호출하세요. |
bool IsPlaying { get; } | Play()와 완료 또는 취소 사이에 true. |
float Elapsed { get; } | 지금까지의 재생 헤드 시간(클록 기반, 완료 시 Duration으로 클램프). |
float Duration { get; } | 시퀀스 전체 길이. 시퀀스가 만들어질 때 계산됩니다. |
event Action OnSequenceComplete | 정상 완료 시 정확히 한 번 발생합니다. 취소 시에는 절대 발생하지 않습니다. |
ITweenSequenceService — 팩토리
| 멤버 | 설명 |
|---|---|
ITweenSequence Create(SequenceOptions options = default) | 비어 있는 새 시퀀스 빌더를 만듭니다. |
int ActiveSequenceCount { get; } | 살아 있는 시퀀스의 수(시작되었고 아직 완료·취소되지 않음). |
SequenceOptions — 설정
| 멤버 | 설명 |
|---|---|
int MaxStepsPerSequence = 256 | 시퀀스당 최대 스텝 수(Append/Join/AppendInterval/AppendCallback 합산). 1–4096으로 클램프. |
int MaxStepsPerTick = 64 | 한 프레임에 발생하는 최대 스텝 수. 폭주 루프를 막습니다. 1–4096으로 클램프. |
int MaxConcurrentSequences = 0 | 서비스 전체의 최대 라이브 시퀀스 수. 0은 무제한. 0–65536으로 클램프. |
Clock DefaultClock = Clock.Gameplay | 재생 헤드를 구동하는 타임 클록. 기본값은 게임이 일시정지되면 함께 멈춥니다. |
bool LetStepExceptionsPropagate = false | false(기본)이면 스텝 콜백이 던진 예외를 잡아 로그합니다. true이면 전파됩니다 — 개발 중에 유용합니다. |
bool LogSequenceLifecycle = false | true이면 build/play/complete/cancel 추적을 로그합니다. 이 로그는 릴리즈 빌드에서도 항상 컴파일되어 들어갑니다. |
예제
슬라이드 인과 페이드 인을 동시에 한 뒤 완료를 알리는 UI 패널:
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
}
끄는 방법
ServiceLocator.Replace<ITweenSequenceService>(new NullTweenSequenceService());
이렇게 하면 모든 시퀀스가 무음 처리됩니다. Create()는 스텝을 버리는 빌더를 반환하고, Play()는 빈 핸들을 반환하며, 콜백은 절대 발생하지 않습니다. 애니메이션이 필요 없는 테스트나 저사양 머신에서 모션을 끌 때 유용합니다. 교체 구현이 생성될 때 경고가 한 번 로그되므로 교체가 조용히 지나가는 일은 없습니다. 프로그래밍 오류 — null 트윈 팩토리, 잘못된 클록 — 는 여전히 throw하므로 실제 실수는 계속 보입니다.
흔한 함정
- 지속 시간 불일치는 여러분의 버그입니다. 시퀀스는
Append/Join에 전달한durationSeconds를 신뢰합니다. 이는 팩토리가 만드는 트윈의 실제 지속 시간과 일치해야 합니다. 3초로 선언했는데 트윈이 2.8초를 돌면 타이밍이 어긋납니다. 트윈 서비스는 트윈의 남은 시간을 보고할 수 없으므로 시퀀스도 이를 검사할 수 없습니다 — 같은 지속 시간 리터럴을 두 호출에 복사해 넣으세요. AppendCallback은 상대적이지 절대적이지 않습니다. 시퀀스의 현재 타임라인 위치에서 발생하며, 벽시계 시각이 아닙니다. "게임 시작 5초 후"에는 스케줄러 서비스를 대신 쓰세요.- 클록 소스는 시작 시점에 캡처됩니다. 시퀀스는 기본적으로
Clock.Gameplay에 바인딩됩니다. 시작 이후에 Time 서비스를 교체하면(ServiceLocator.Replace<ITimeService>(…)), 시퀀싱은 만들어질 때의 원래 시간 소스를 계속 사용합니다. - 취소는 완료가 아닙니다. 재생 핸들의
Dispose()는 시퀀스를 취소하며,OnSequenceComplete는 발생하지 않습니다. 이 이벤트는 성공 경로 정리에만 쓰세요. 취소는 조용합니다. Play()후 빌더는 동결됩니다.Play()이후에Append,Join,AppendInterval,AppendCallback을 호출하면InvalidOperationException이 발생합니다. 타임라인을 늘려야 하면 새 시퀀스를 만드세요.