DOTween 은 타입마다 하나인 플러그인에 값 계산을 맡긴다
Esc

    DOTween 은 타입마다 하나인 플러그인에 값 계산을 맡긴다

    2026년 10월 18일 수정–Unity라이브러리트윈
    float hp = 0f;
    DOTween.To(() => hp, x => hp = x, 10f, 1f);

    이 한 줄에는 0 에서 10 사이 값을 무엇으로 계산할지가 없습니다. float 이면 더하고 곱하면 되지만 Quaternion 이나 string 이면 방법이 다릅니다. 같은 Transform 에 트윈 둘을 걸었을 때 누가 이기는지, SetEase 에 넘긴 숫자가 무슨 뜻인지도 이 계산에 달려 있습니다.

    DOTween 의 트윈은 시간과 루프만 알고, 값 계산은 트윈을 만들 때 타입 조합으로 고른 플러그인 하나가 매 프레임 시작 + 변화량 × 이징 으로 합니다. 기준은 DOTween 1.3.030(GitHub 커밋 4846808)입니다.

    플러그인은 만들 때 타입 조합으로 한 번 고른다

    Tweener.Setup 이 PluginsManager.GetDefaultPlugin<T1, T2, TPlugOptions>() 을 부릅니다. 딕셔너리가 아니라 typeof 를 차례로 비교하는 if 체인입니다.

    Type t1 = typeof(T1);
    Type t2 = typeof(T2);
    
    // 딕셔너리가 아니라 typeof 를 차례로 비교한다
    if (t1 == typeof(Vector3) && t1 == t2)
        plugin = _vector3Plugin;
    else if (t1 == typeof(Vector3) && t2 == typeof(Vector3[]))
        plugin = _vector3ArrayPlugin;
    else if (t1 == typeof(Quaternion))
    {
        if (t2 == typeof(Quaternion))
            LogError("Quaternion tweens require a Vector3 endValue");   // 거부. plugin 은 null
        else
            plugin = _quaternionPlugin;        // 끝값을 오일러각(Vector3)으로 받는 회전
    }
    else if (t1 == typeof(Vector2))
        plugin = _vector2Plugin;
    else if (t1 == typeof(float))
        plugin = _floatPlugin;                 // 처음이면 여기서 하나 만들어 정적 필드에 둔다
    // ... Color, int, Vector4, Rect, RectOffset, uint, string, Color2, long, ulong, double
    
    return plugin as ABSTweenPlugin<T1, T2, TPlugOptions>;
    트윈 타입 고른 플러그인
    float 값 트윈 <float, float, FloatOptions> 다섯 번째 비교에서 FloatPlugin
    DORotate <Quaternion, Vector3, QuaternionOptions> QuaternionPlugin
    Quaternion 을 Quaternion 으로 <Quaternion, Quaternion, NoOptions> 없음. 오류를 남기고 트윈을 버림

    조회는 트윈을 만들 때 한 번뿐입니다. 매 프레임에는 트윈의 tweenPlugin 필드에 담아 둔 참조를 그대로 씁니다.

    각 분기는 정적 필드가 비어 있을 때만 new 해서, float 트윈이 만 개여도 FloatPlugin 은 하나입니다. 그래서 플러그인은 상태를 갖지 않고, 트윈마다 다른 값은 트윈 객체에 있다가 매 프레임 인자로 들어옵니다. 조회는 만들 때 한 번이고, 매 프레임에는 tweenPlugin 필드의 참조를 씁니다.

    매 프레임은 시작값에서 계산해 덮어쓴다

    // DOTween/Plugins/FloatPlugin.cs (EvaluateAndApply 줄임)
    if (t.loopType == LoopType.Incremental) startValue += changeValue * (t.isComplete ? t.completedLoops - 1 : t.completedLoops);
    setter(startValue + changeValue * EaseManager.Evaluate(t.easeType, t.customEase, elapsed, duration, t.easeOvershootOrAmplitude, t.easePeriod));

    getter 를 부르지 않습니다. 시작할 때 저장한 시작값과 변화량에서 계산해 setter 로 덮어씁니다. 증가 루프(LoopType.Incremental)도 트윈이 따로 기억하지 않고 플러그인이 끝낸 루프 수만큼 시작값을 밀어서 만듭니다. SafeMode 가 켜져 있으면 이 호출 전체가 try 안에 있어서, 대상이 파괴돼 setter 가 예외를 던지면 트윈이 죽습니다.

    축 제약 트윈만 지금 값을 읽는다

    DOMoveX 는 DOMove 와 같은 Vector3 트윈에 AxisConstraint.X 만 붙은 것입니다. 축 제약이 있으면 Vector3Plugin 이 getter 로 지금 값을 읽어 한 축만 바꿔 씁니다. 원점의 Transform 에 같은 프레임에 두 트윈을 걸고 첫 갱신에서 0.5초를 민 경우입니다(Linear, 길이 1초).

    재 보니 DOMoveX + DOMoveY 는 (5, 5, 0), DOMove 둘은 (2.5, 5, 0) 이었습니다. 제약 없는 뒤 트윈이 세 축을 모두 덮어쓰되, 그 시작값은 앞 트윈이 첫 갱신에서 옮겨 둔 자리입니다. 축 제약의 대가는 getter 한 번입니다.

    Transform 1만 개, 트윈 하나의 매 프레임 비용 DOMoveX (축 제약) DOMove 차이
    Mono 플레이어 240 ns 201 ns 39 ns
    IL2CPP 플레이어 169 ns 132 ns 37 ns

    (200프레임 PlayerLoop 중앙값의 증가분 ÷ 1만.)

    이징은 초 단위로 받고, 두 인자의 뜻은 이징마다 다르다

    EaseManager.Evaluate 는 0~1 진행률이 아니라 (time, duration) 초 단위 쌍을 받는 switch 하나이고, 결과를 자르지 않습니다(OutBack 은 1 을 넘습니다). SetEase(ease, a, b) 의 두 숫자는 트윈의 필드 두 개에 들어가고 뜻은 이징이 정합니다.

    이징 overshootOrAmplitude period
    Back 지나치는 정도. 기본 1.70158 쓰지 않음
    Elastic 진폭. 1 보다 작으면 1 로 올림 흔들림 한 번의 길이, 초 단위. 0 이면 duration × 0.3
    Flash 깜빡임 횟수. 정수로 자름 약해지거나 세지는 정도

    Elastic 의 sin 한 바퀴가 period 초라서, 트윈 전체에서 흔들리는 횟수는 duration ÷ period 입니다.

    SetEase(Ease.OutElastic, 1f, 0.3f) 로 v 가 1 을 지나는 횟수를 세자 1초 트윈은 7번, 3초 트윈은 20번이었고, period 를 0 으로 비우면 둘 다 7번이었습니다.

    Flash 는 SetEase 가 필드를 정수로 자르고 되돌리지 않습니다. 인자 없는 SetEase(Ease.Flash) 는 기본값 1.70158 을 1 로 만들고, 같은 트윈에 이어서 인자 없는 SetEase(Ease.OutBack) 를 부르면 지나치는 정도가 1 로 남아 최대값이 1.100 이 아니라 1.037 이었습니다(같은 세 환경).

    주의할 점

    1. Elastic 의 period 를 비율로 주지 않는다

    안 지키면 생기는 문제. period 는 초 단위입니다. 1초 트윈에서 맞춘 SetEase(Ease.OutElastic, 1f, 0.3f) 를 3초 트윈에 그대로 쓰면 열 번 가까이 떨립니다. 진폭은 1 아래로 내려가지 않아 덜 튀게 하려고 0.5 를 줘도 효과가 없습니다.

    이렇게 쓴다. 길이와 무관하게 같은 모양을 원하면 period 를 0 으로 비웁니다. 직접 정해야 하면 duration × 비율 로 계산해 넘깁니다.

    2. 같은 속성에 축 제약 없는 트윈 둘을 걸지 않는다

    안 지키면 생기는 문제. DOMove 는 자기 시작값에서 세 축을 모두 씁니다. 같은 Transform 에 X 로 가는 DOMove 와 Y 로 가는 DOMove 를 걸면 뒤 차례가 세 축을 덮어써서 앞 트윈의 움직임이 사라지고, 첫 갱신이 길면 X 도 엉뚱한 자리에서 출발해 되돌아옵니다. 오류도 경고도 없습니다.

    이렇게 쓴다. 축을 나눠 움직이면 DOMoveX, DOMoveY 를 씁니다. 같은 축을 새로 움직이면 먼저 걸린 트윈을 DOKill 하고 겁니다. 움직임을 더해야 하면 DOBlendableMoveBy 계열을 씁니다.

    3. AnimationCurve 이징을 트윈마다 새로 걸지 않는다

    안 지키면 생기는 문제. SetEase(curve) 는 부를 때마다 EaseCurve 객체와 델리게이트를 새로 만듭니다. float 트윈 하나가 OutQuad 로 350 B 남짓일 때 커브면 약 150 B 가 더 붙고, 매 프레임 평가도 키 3개짜리 커브에서 트윈 비용이 1.6~1.8배가 됐습니다.

    이렇게 쓴다. 델리게이트를 한 번만 만들어 두고 SetEase(EaseFunction) 로 넘깁니다. 이 오버로드는 넘긴 델리게이트를 필드에 넣기만 합니다.

    static EaseFunction _popEase;
    void Awake() => _popEase ??= new EaseCurve(popCurve).Evaluate;
    void Pop() => transform.DOScale(1.2f, 0.3f).SetEase(_popEase);

    4. Flash 는 횟수를 함께 넘긴다

    안 지키면 생기는 문제. 인자 없는 SetEase(Ease.Flash) 는 한 번만 오가서 깜빡이지 않고 스르륵 변하고 끝납니다. 잘린 값은 필드에 남아 같은 트윈에 다시 건 OutBack 이 10% 대신 4% 만 지나칩니다.

    이렇게 쓴다. SetEase(Ease.Flash, 6) 처럼 횟수를 적습니다. 한 트윈에 이징을 바꿔 걸 때는 SetEase(ease, overshoot, period) 로 두 값을 명시합니다.

    정리

    DOTween 은 트윈을 만들 때 타입 조합으로 고른 플러그인 하나에 매 프레임 값 계산을 맡깁니다.

    • 플러그인은 타입마다 정적 인스턴스 하나라 상태가 없고, 매 프레임 저장한 시작값에서 시작 + 변화량 × 이징 을 계산해 덮어씁니다.
    • 축 제약 트윈만 getter 로 지금 값을 읽어 한 축만 바꾸므로 서로 다른 축끼리 공존합니다. 제약 없는 트윈 둘은 뒤 차례가 이깁니다.
    • 이징은 초 단위로 계산하고, overshootOrAmplitude 와 period 의 뜻은 이징마다 다릅니다. Elastic 의 period 는 초입니다.