DOTween 은 매 프레임 위치와 루프 수를 새로 정해 그 자리로 간다
Esc

    DOTween 은 매 프레임 위치와 루프 수를 새로 정해 그 자리로 간다

    2026년 10월 18일 수정–Unity라이브러리트윈
    elapsed += Time.deltaTime;
    if (elapsed >= duration) { elapsed -= duration; loop++; forward = !forward; }   // 한 프레임에 루프 둘을 넘으면?
    float p = elapsed / duration;

    손으로 쓴 Update 보간은 시간을 쌓고, 루프나 Yoyo, 딜레이가 붙을 때마다 분기를 더합니다. 로딩 직후처럼 한 프레임이 길어 루프를 여러 번 넘으면 넘친 시간을 버리거나 루프 하나만 넘긴 것으로 치기 쉽습니다.

    DOTween 은 시간을 쌓지 않고, 매 프레임 “몇 번째 루프의 몇 초 지점” 을 새로 계산해 DoGoto 로 트윈을 그 자리에 놓습니다. 정상 재생도 Goto 도 Complete 도 같은 DoGoto 를 거칩니다. 기준은 설치본 DOTween 1.3.030(GitHub 커밋 4846808)입니다.

    넘긴 루프는 while 로 하나씩 센다

    트윈의 시간은 두 칸입니다. 루프 안의 시간 position(0 ~ duration)과 끝낸 루프 수 completedLoops 입니다. 매 프레임 매니저는 position 에 dt 를 더하고, duration 을 넘는 동안 한 루프씩 빼며 셉니다.

    float toPosition = t.position;                   // 지금 루프 안의 시간
    bool wasEndPosition = toPosition >= t.duration;
    int toCompletedLoops = t.completedLoops;         // 지금까지 끝낸 루프 수
    
    toPosition += tDeltaTime;                        // 이번 프레임에 흐른 시간을 더한다
    
    // duration 을 넘는 동안 한 루프씩 빼며 센다
    while (toPosition >= t.duration && (t.loops == -1 || toCompletedLoops < t.loops))
    {
        toPosition -= t.duration;
        toCompletedLoops++;
    }
    
    if (wasEndPosition)
        toCompletedLoops--;
    
    // 유한 루프를 다 썼으면 끝에 고정한다
    if (t.loops != -1 && toCompletedLoops >= t.loops)
        toPosition = t.duration;
    
    return Tween.DoGoto(t, toPosition, toCompletedLoops, UpdateMode.Update);

    길이 1초, loops = 3 인 트윈이 0.5초 지점에 있을 때 프레임이 크게 늦어 2.7초가 한꺼번에 들어오면 이렇게 셉니다.

    단계 toPosition toCompletedLoops
    시간을 더함 3.2 0
    첫 루프를 뺌 2.2 1
    둘째 루프를 뺌 1.2 2
    셋째 루프를 뺌 0.2 3
    루프를 다 써서 끝에 고정 1.0 3

    DoGoto(1.0, 3) 이 불립니다. 프레임이 크게 늦어도 넘긴 루프 수가 정확히 남습니다.

    넘긴 루프 수가 그대로 DoGoto 에 가고, DoGoto 는 그 차이만큼 OnStepComplete 를 부릅니다. DOTween.To(...).SetLoops(3) 을 v = 0.75 까지 보낸 뒤 DOTween.ManualUpdate(2.7f, 2.7f) 를 한 번 부르자 그 호출 안에서 OnStepComplete 가 세 번, OnComplete 가 한 번 불렸습니다.

    딜레이의 남는 시간은 버리지 않는다

    목표를 계산하기 전에 딜레이가 먼저 시간을 가져갑니다.

    // Tweener.cs (DoUpdateDelay 줄임)
    if (elapsed > tweenDelay) {
        t.elapsedDelay = tweenDelay;
        t.delayComplete = true;
        return elapsed - tweenDelay;   // 남는 시간을 이번 프레임 진행에 넘긴다
    }
    t.elapsedDelay = elapsed;
    return 0;

    딜레이 0.5 에 0.45 를 기다린 상태에서 0.1 이 들어오면, 딜레이를 끝내고 남는 0.05 를 같은 프레임의 진행에 씁니다. 딜레이와 본 재생을 합친 길이가 프레임 경계와 무관하게 맞습니다. 트위너의 딜레이는 처음 한 번만 기다립니다. 루프마다 다시 기다리지 않고, 다시 기다리게 하는 것은 Restart() 입니다. 시퀀스에 건 SetDelay 는 맨 앞에 빈 구간을 끼우는 방식이라 루프마다 반복됩니다.

    Yoyo 는 시간만 뒤집는다

    Yoyo 는 홀수 번째 루프 동안 duration - position 을 플러그인에 넘깁니다. 시작값과 끝값은 그대로이고 시간만 거꾸로 흐릅니다.

    t.position = toPosition;
    t.completedLoops = toCompletedLoops;
    
    // 루프를 끝낸 직후 0 에 있으면 끝 위치로 본다
    if (t.position <= 0 && (t.completedLoops > 0 || t.isComplete))
        t.position = t.duration;
    
    // Yoyo 의 홀수 번째 루프면 시간을 뒤집는다
    bool useInverse = t.loopType == LoopType.Yoyo
        && (t.position < t.duration ? t.completedLoops % 2 != 0 : t.completedLoops % 2 == 0);
    
    float updatePosition = useInverse ? t.duration - t.position : t.position;
    
    plugin.EvaluateAndApply(..., updatePosition, ...);   // x = 0 + 10 * updatePosition

    DOMoveX(10, 1).SetLoops(2, LoopType.Yoyo) 를 x = 0 에서 Linear 로 돌리면 이렇습니다.

    누적 시간 position completedLoops 뒤집나 넘기는 시간 x
    0.5 0.5 0 아니오 0.5 5
    1.2 0.2 1 예 0.8 8
    2.0 1.0 2 예 0 0

    마지막 줄은 위치가 끝(1.0)이라 오른쪽 식 2 % 2 == 0 을 씁니다. 왼쪽 식을 썼다면 뒤집지 않고 x = 10 에서 끝났을 것입니다.

    ManualUpdate 로 0.5, 0.7, 0.8 초를 밀며 적은 x 는 5, 8, 0 이었습니다. 시간만 뒤집으므로 이징도 거울상이 됩니다. 같은 트윈을 OutQuad 로 바꿔 0.1초씩 밀자 가는 루프의 변화량은 1.9, 1.7 … 0.1 로 줄고, 돌아오는 루프는 -0.1, -0.3 … -1.9 로 커졌습니다. 돌아오는 길은 가는 길을 거꾸로 튼 모양이라 천천히 출발해 빠르게 도착합니다.

    콜백은 값을 쓴 뒤 정해진 순서로 불린다

    DoGoto 는 값을 적용한 다음 콜백을 부릅니다. loops = 3 트윈의 첫 갱신과, 한 프레임에 끝까지 간 경우입니다.

    // 처음 갱신되는 순간에만
    if (!t.playedOnce)
    {
        t.playedOnce = true;
        onStart?.Invoke();
        onPlay?.Invoke();
    }
    
    t.completedLoops = toCompletedLoops;
    t.isComplete = t.completedLoops == t.loops;
    int newCompletedSteps = t.completedLoops - prevCompletedLoops;   // 이번 프레임에 넘긴 루프 수
    
    t.ApplyTween(...);                         // setter 로 값을 쓴다
    onUpdate?.Invoke();                        // 값을 쓴 다음에 부른다
    
    if (t.position <= 0 && t.completedLoops <= 0 && !wasRewinded)
        onRewind?.Invoke();
    
    // 넘긴 루프 수만큼 부른다. 도중에 죽으면 멈춘다
    for (int i = 0; i < newCompletedSteps; i++)
    {
        onStepComplete();
        if (!t.active) break;
    }
    
    if (t.isComplete && !wasComplete)
        onComplete?.Invoke();
    
    // autoKill 로 끝나는 트윈에는 OnPause 를 부르지 않는다
    if (!t.isPlaying && wasPlaying && (!t.isComplete || !t.autoKill))
        onPause?.Invoke();
    
    return t.autoKill && t.isComplete;         // true 면 루프 끝에서 OnKill

    loops = 3 인 트윈에서 불리는 콜백입니다.

    프레임 불리는 콜백 (순서대로)
    첫 갱신 (0 → 0.3초) OnStart, OnPlay, OnUpdate
    한 프레임에 끝까지 (루프 0 → 3) OnUpdate, OnStepComplete × 3, OnComplete, 루프 끝에서 OnKill

    순서는 OnStart → OnPlay → 값 적용 → OnUpdate → OnRewind → OnStepComplete × n → OnComplete → OnPause 이고 OnKill 은 루프 끝입니다. 같은 프레임에 A, B 가 함께 끝나면 기록은 A.OnComplete, B.OnComplete, B.OnKill, A.OnKill 이었습니다. OnComplete 는 배열 순서, OnKill 은 그 반대입니다.

    사용자가 위치를 옮기는 API 도 같은 DoGoto 를 부르되 UpdateMode 로 콜백을 뺍니다. Goto, Rewind, Complete 는 UpdateMode.Goto 라서 OnStart, OnPlay, OnStepComplete 가 빠지고, 건너뛴 루프의 OnStepComplete 가 필요하면 GotoWithCallbacks 를 씁니다.

    주의할 점

    1. 한 프레임에 한 루프만 넘어간다고 가정하지 않는다

    안 지키면 생기는 문제. 프레임이 크게 늦으면 짧은 루프 트윈은 한 프레임에 루프 여러 개를 넘기고, OnStepComplete 가 그 수만큼 연달아 불립니다. 그 안에서 사운드를 틀거나 이펙트를 만드는 코드가 같은 프레임에 여러 번 실행됩니다.

    이렇게 쓴다. OnStepComplete 에서 하는 일은 같은 프레임에 여러 번 불려도 괜찮게 만들거나 Time.frameCount 로 프레임당 한 번만 하게 거릅니다. 루프 수로 상태를 셀 때는 콜백 횟수보다 CompletedLoops() 를 읽습니다.

    2. 딜레이가 루프마다 반복된다고 기대하지 않는다

    안 지키면 생기는 문제. 트위너에 SetDelay(0.5f).SetLoops(3) 을 걸면 처음 한 번만 기다리고 세 루프는 쉬지 않고 이어집니다. 루프마다 쉬는 깜빡임을 기대한 연출이 한 번만 쉽니다.

    이렇게 쓴다. 시퀀스에 AppendInterval 과 트윈을 넣고 시퀀스에 SetLoops 를 겁니다.

    3. Yoyo 로 돌아오는 길의 이징을 따로 기대하지 않는다

    안 지키면 생기는 문제. OutQuad 로 부드럽게 도착하는 트윈을 Yoyo 로 되돌리면, 돌아올 때는 처음에 느리고 도착할 때 빨라져서 출발점에서 툭 끊깁니다.

    이렇게 쓴다. InOutQuad 처럼 양끝이 대칭인 이징을 쓰거나, 가는 트윈과 오는 트윈을 시퀀스로 따로 만들어 각자 이징을 줍니다.

    4. 콜백 안에서 Kill 해도 같은 호출의 남은 콜백은 불린다

    안 지키면 생기는 문제. OnStart, OnPlay, OnStepComplete 뒤에는 트윈이 죽었는지 보는 검사가 있지만 OnUpdate, OnRewind, OnComplete 뒤에는 없습니다. 마지막 프레임의 OnUpdate 에서 Kill() 을 불러도 이어서 OnComplete 가 한 번 불렸습니다. “죽였으니 완료 처리는 없다” 고 가정한 코드가 완료 처리를 합니다.

    이렇게 쓴다. 끝났을 때 할 일은 OnComplete 한 곳에 모으고 OnUpdate 에서는 값을 읽기만 합니다. OnUpdate 에서 멈춰야 하면 내 쪽 플래그를 두고 OnComplete 에서 봅니다.

    정리

    DOTween 은 매 프레임 목표 위치와 루프 수를 새로 계산해 DoGoto 로 그 자리에 놓습니다.

    • 넘긴 루프는 while 로 하나씩 세어 OnStepComplete 를 빠짐없이 부릅니다.
    • 딜레이가 끝나는 프레임의 남는 시간은 진행에 넘기고, 트위너의 딜레이는 처음 한 번뿐입니다.
    • Yoyo 는 시간만 뒤집어 이징도 거울상이 되고, 콜백은 값을 쓴 뒤 정해진 순서로 불립니다.