LitMotion 의 시퀀스는 자식의 시각을 밀어 주는 모션 하나다
Esc

    LitMotion 의 시퀀스는 자식의 시각을 밀어 주는 모션 하나다

    2026년 10월 18일 수정–Unity라이브러리트윈
    var handle = LSequence.Create()
        .Append(LMotion.Create(0f, 1f, 1f).BindToPositionX(transform))         // 0초부터
        .Join(LMotion.Create(Color.white, Color.red, 1f).BindToColor(sprite))  // 같이 시작
        .AppendInterval(0.5f)                                                   // 0.5초 쉬고
        .Append(LMotion.Create(1f, 2f, 1f).BindToPositionX(transform))         // 1.5초부터
        .Run();

    코루틴이라면 yield return 으로 차례를 잇고, 동시에 도는 것은 코루틴을 하나 더 띄웁니다. 되감거나 중간으로 건너뛰려면 따로 짜야 합니다.

    LitMotion 의 LSequence 에는 따로 도는 엔진이 없습니다. Run() 은 0 에서 전체 길이까지 가는 double 모션 하나를 만들고, 그 값이 바뀔 때마다 자식 모션들에게 “너는 지금 몇 초다” 를 SetTime 으로 알려 줍니다. 기준 판본은 2.0.2 입니다.

    Append 와 Join 은 tail 두 개로 자리를 정한다

    빌더는 다음 Append 가 놓일 tail, 그 직전의 tail 인 lastTail, 전체 길이 duration 셋으로 자리를 계산합니다. Append 는 자기 길이만큼 AppendInterval 을 부르고, Join 은 lastTail 에 Insert 합니다.

    public void Append(MotionHandle handle)
    {
        AddItem(new MotionSequenceItem(tail, handle));   // 지금 tail 에 놓는다
        AppendInterval(TotalDuration);                   // 그 길이만큼 tail 을 민다
    }
    
    public void AppendInterval(double interval)
    {
        lastTail = tail;                                 // 밀기 전 위치를 기억한다
        tail += interval;
        duration = Math.Max(duration, tail);
    }
    
    public void Insert(double position, MotionHandle handle)
    {
        AddItem(new MotionSequenceItem(position, handle));
        duration = Math.Max(duration, position + TotalDuration);   // tail 은 건드리지 않는다
    }
    
    public void Join(MotionHandle handle)
    {
        Insert(lastTail, handle);                        // 마지막으로 민 자리의 시작에 붙인다
    }
    호출 놓이는 위치 tail lastTail duration
    시작 0 0 0
    Append(A), 1초 A@0 1 0 1
    Join(B), 2초 B@0 1 0 2
    Append(C), 1초 C@1 2 1 2
    AppendInterval(0.5) 2.5 2 2.5
    Join(D), 1초 D@2 2.5 2 3

    C 는 B 가 끝나는 2 가 아니라 tail 인 1 에 놓입니다. Join 은 tail 을 늘리지 않기 때문입니다. D 는 인터벌 뒤 2.5 가 아니라 인터벌 시작 2 에 붙습니다. AppendInterval 도 lastTail 을 옮기기 때문입니다.

    • Join 한 모션이 길어도 다음 Append 는 밀리지 않습니다. Join 은 전체 길이만 늘리고 tail 은 그대로 둡니다.
    • AppendInterval 바로 뒤의 Join 은 쉬기 전 자리에 붙습니다. AppendInterval 도 lastTail 을 옮기기 때문입니다.

    Insert(초, 모션) 은 둘 다 건드리지 않고 절대 위치에 놓습니다.

    넣는 순간 자식은 시퀀스 것이 된다

    Append, Join, Insert 는 먼저 자식을 검사하고 두 플래그를 찍습니다.

    검사, 표시 결과
    상태가 Scheduled 가 아니면 예외 모션은 첫 갱신에서 Scheduled 를 벗어나므로 만든 프레임 안에서 넣어야 합니다
    길이가 무한이면 예외 WithLoops(-1) 은 시간표에 놓을 수 없습니다
    IsInSequence = true 자기 Job 과 콜백 루프에서 빠지고, 사용자가 Cancel, Complete, Time 을 부르면 Cannot access the motion in sequence.
    IsPreserved = true 스스로 끝나도 저장소에서 빠지지 않습니다. 시퀀스가 되감을 수 있어야 해서입니다

    시퀀스는 double 모션 하나다

    // Runtime/MotionSequenceBuilder.cs (줄임)
    var builder = LMotion.Create(0.0, duration, (float)duration)
        .WithOnComplete(source.OnCompleteDelegate)
        .WithOnCancel(source.OnCancelDelegate);
    configuration?.Invoke(builder);
    var handle = builder.Bind(source, (x, source) => source.Time = x);

    기본 이징이 선형이라 값이 곧 경과 시각입니다. 돌려받는 핸들도 이 모션의 핸들이라 Cancel, Complete, Preserve, Time, PlaybackSpeed 가 평범한 모션처럼 동작합니다. 한 프레임의 흐름입니다.

    그래서 Run(b => b.WithEase(Ease.InOutQuad)) 의 이징은 자식이 아니라 시계에 걸리고, WithLoops(2, LoopType.Yoyo) 면 시퀀스 전체가 되감깁니다. 그리고 자식의 값 계산은 Burst Job 이 아니라 메인 스레드의 SetTime 안에서 하나씩 일어납니다. 10초 float 모션 1,000개를 따로 돌린 것과 한 시퀀스에 Join 한 것의 LitMotion 갱신 단계 증가분입니다.

    모션 1,000개 Mono IL2CPP
    따로 돌림 +0.140 ms +0.156 ms
    한 시퀀스에 Join +0.362 ms +0.242 ms

    200프레임을 잰 값입니다. 조립도 공짜가 아닙니다. 자식 셋짜리 시퀀스를 한 프레임에 100개 만들었더니 시퀀스당 1,264 B(IL2CPP 1,272 B) 였습니다. 소스 객체, 델리게이트 둘, ArrayPool 이 새로 만든 32칸 항목 배열 몫이고, 풀로 돌아가는 것은 시퀀스가 끝나거나 취소될 때라 동시에 살아 있는 시퀀스 수만큼 쌓입니다.

    시각은 뒤에서 한 번, 앞에서 한 번 쓴다

    Time 이 들어오면 위치 순으로 정렬한 자식 목록을 두 번 훑습니다. 아직 시작 전인 자식(Position >= time)을 뒤에서 앞으로, 이미 시작한 자식을 앞에서 뒤로 씁니다. 같은 x 를 A(0→1)와 B(1→2)가 이어서 움직이는 경우입니다.

    시작 전 자식은 가장 가까운 것이, 시작한 자식은 가장 늦게 시작한 것이 마지막에 씁니다. 그래서 되감기와 점프에서도 한 속성이 지금 맡은 자식의 값으로 모입니다.

    이 장치는 시작 전 자식이 시작값을 쓴다는 데 기댑니다. 그런데 WithDelay(d) 의 세 번째 인자 skipValuesDuringDelay 는 기본이 true 라, 딜레이를 건 자식은 시작 전에 값을 쓰지 않습니다. A 다음에 0.5초 딜레이를 건 B(5→6)를 붙인 보존 시퀀스를 끝까지 재생하고 h.Time = 0.5 로 되감았더니, A 는 0.5 로 돌아왔는데 B 가 맡은 값은 끝값 6 에 남았습니다. 세 번째 인자를 false 로 주면 5 로 돌아갔습니다.

    같은 위치에 놓인 자식끼리의 순서는 위치 하나만 보는 Array.Sort 가 정하고, 같은 키의 순서를 지킨다는 약속이 없습니다. 이 환경들에서는 자식 40개까지 마지막에 Join 한 자식의 값이 남았지만 API 가 보장하는 결과는 아닙니다.

    끝나면 자식을 Cancel 로 치운다

    자식은 보존 상태라 스스로 빠지지 않습니다. 시퀀스의 완료 콜백과 취소 콜백이 자식 전원을 Cancel 합니다. 그런데 보존된 완료 모션은 취소가 허용되고, 그때 OnCancel 이 불립니다. 0.2초 자식 하나를 넣은 시퀀스를 1초 기다린 뒤 자식 콜백 기록입니다.

    시퀀스 자식이 받은 콜백
    그냥 Run() 완료, 취소
    Run().Preserve() 완료
    보존한 시퀀스를 그 뒤 Cancel() 완료, 취소

    정상으로 끝난 시퀀스에서도 자식은 완료와 취소를 둘 다 받습니다.

    주의할 점

    1. 자식 모션은 만든 그 자리에서 넣는다

    안 지키면 생기는 문제. 한 프레임이라도 지난 모션을 넣으면 ArgumentException("Cannot add a running motion to a sequence.") 입니다. 모션을 필드에 만들어 두었다가 버튼을 누를 때 조립하는 코드가 여기 걸립니다.

    이렇게 쓴다. 조립하는 메서드 안에서 자식을 만들고 바로 넣습니다. 미리 준비하려면 모션이 아니라 모션을 만드는 메서드를 들고 있습니다. 반복은 무한 루프 자식 대신 Run(b => b.WithLoops(-1)) 로 겁니다.

    2. AppendInterval 바로 뒤에는 Append 로 시작한다

    안 지키면 생기는 문제. AppendInterval(0.5f).Join(x) 는 쉬기 전 자리에서 시작해 쉬는 시간이 사라집니다.

    이렇게 쓴다. 쉬고 나서 여럿을 같이 돌릴 때는 AppendInterval(0.5f).Append(a).Join(b) 로 씁니다. 위치가 복잡해지면 Insert(초, 모션) 로 절대 위치를 적습니다.

    3. 자식의 OnCancel 에 되돌리기를 두지 않는다

    안 지키면 생기는 문제. 시퀀스가 정상으로 끝나도 자식의 OnCancel 이 불립니다. 취소 콜백에서 위치나 색을 원래대로 돌리면 끝까지 재생한 연출이 마지막 순간 되돌아갑니다.

    이렇게 쓴다. 취소 처리는 Run(b => b.WithOnCancel(Restore)) 처럼 시퀀스 쪽에 둡니다. 자식에는 바인딩과 OnComplete 만 둡니다. 자식 하나를 따로 멈춰야 한다면 그 모션은 시퀀스에 넣지 않습니다.

    4. 되감을 시퀀스의 자식에는 기본 딜레이를 쓰지 않는다

    안 지키면 생기는 문제. 딜레이를 건 자식은 시작 전에 값을 쓰지 않아, 되감으면 그 자식이 맡은 속성이 끝값에 남습니다.

    이렇게 쓴다. 자리를 띄울 때는 AppendInterval 이나 Insert 를 씁니다. 자식에 딜레이가 꼭 필요하면 WithDelay(d, DelayType.FirstLoop, false) 로 세 번째 인자를 false 로 줍니다. 같은 위치의 자식 둘에게 한 속성을 맡기지도 않습니다. 같은 위치끼리의 순서는 API 가 약속하지 않습니다.

    정리

    LitMotion 의 시퀀스는 자식의 시각을 밀어 주는 double 모션 하나입니다.

    • 빌더는 tail 과 lastTail 로 자리를 정하고, 자식은 만든 프레임 안에서 넣어야 하며 넣는 순간 사용자 접근이 막힙니다.
    • 자식의 값은 메인 스레드의 SetTime 안에서 계산되고, Run 에 준 이징과 루프는 시계에 걸립니다.
    • 시각을 뒤에서 한 번, 앞에서 한 번 써서 되감기가 맞습니다. 딜레이를 건 자식은 예외이고, 정상 완료에서도 자식의 OnCancel 이 불립니다.