DOTween 은 모든 트윈을 배열 하나에 두고 빈칸은 한 번에 메운다
Esc

    DOTween 은 모든 트윈을 배열 하나에 두고 빈칸은 한 번에 메운다

    2026년 10월 18일 수정–Unity라이브러리트윈GC
    for (int i = 0; i < 300; i++)
        coins[i].DOMove(wallet.position, 0.5f);   // 보상 연출 한 프레임

    돌려받은 트윈을 버려도 트윈은 끝까지 돕니다. 누군가 참조를 쥐고 매 프레임 불러 준다는 뜻이고, 트윈이 한 프레임에 수백 개 생기고 죽으면 그 자료구조가 무엇을 치르는지가 문제가 됩니다.

    DOTween 의 TweenManager 는 정적 클래스 하나가 모든 트윈을 활성 배열 하나에 만든 순서대로 두고, 뺄 때는 칸에 null 만 쓴 뒤 다음 갱신 때 한 번에 당겨 메웁니다. 풀은 타입을 선형으로 찾고, 용량은 넘칠 때 한꺼번에 늘어납니다. 기준은 DOTween 1.3.030(GitHub 커밋 4846808)입니다.

    활성 트윈은 배열 하나에 만든 순서대로 있다

    Update, LateUpdate, FixedUpdate, 수동 갱신 트윈이 모두 _activeTweens 한 배열에 섞여 있습니다. 넣을 때는 늘 끝에 붙이고, 트윈은 자기 칸 번호를 activeId 로 기억합니다. 뺄 때는 그 칸에 null 을 쓰고 “가장 앞 빈칸” 만 기억해 둡니다. 당기는 일은 다음 추가나 다음 갱신의 시작에서 ReorganizeActiveTweens 가 한 번에 합니다.

    다섯 칸 [A, B, C, D, E] 에서 B 와 D 가 죽은 경우와, 맨 끝 E 만 죽은 경우입니다.

    if (totActiveTweens <= 0)
    {
        _maxActiveLookupId = -1;               // 살아 있는 트윈이 없다
        return;
    }
    
    // 빈칸이 맨 끝 하나뿐이면 끝 번호만 줄인다
    if (_reorganizeFromId == _maxActiveLookupId)
    {
        _maxActiveLookupId--;
        return;
    }
    
    int shift = 1;                             // 앞으로 당길 거리
    int len = _maxActiveLookupId + 1;
    _maxActiveLookupId = _reorganizeFromId - 1;   // 첫 빈칸 앞까지는 손대지 않는다
    
    for (int i = _reorganizeFromId + 1; i < len; ++i)
    {
        Tween t = _activeTweens[i];
    
        if (t == null)
        {
            shift++;                           // 빈칸을 하나 더 만났다
            continue;
        }
    
        // 살아 있는 트윈을 shift 칸 앞으로 옮긴다
        t.activeId = _maxActiveLookupId = i - shift;
        _activeTweens[i - shift] = t;
        _activeTweens[i] = null;
    }
    
    _requiresActiveReorganization = false;
    _reorganizeFromId = -1;

    [A, B, C, D, E] 에서 B 와 D 가 죽어 [A, _, C, _, E] 가 됐을 때입니다. 첫 빈칸은 1번입니다.

    칸 만난 것 shift 배열
    시작 1 [A, _, C, _, E]
    2 C. 1번 칸으로 1 [A, C, _, _, E]
    3 빈칸 2 [A, C, _, _, E]
    4 E. 2번 칸으로 2 [A, C, E, _, _]

    한 번 훑어서 빈칸 둘을 메웠고, 순서는 A, C, E 그대로입니다. 맨 끝 E 만 죽었다면 둘째 if 에서 끝 번호만 줄이고 훑지 않습니다.

    이 압축은 순서를 지킵니다. 맨 뒤 원소를 빈칸에 옮기면 훑지 않고 끝나지만 E 가 C 앞으로 옵니다. DOTween 은 첫 빈칸부터 끝까지를 밀어서, 활성 배열의 순서가 곧 만든 순서이고 매 갱신도 그 순서로 돕니다. 같은 값에 트윈 둘이 걸리면 나중에 만든 쪽이 나중에 setter 를 불러 이기는 이유입니다. 한 프레임에 백 개가 죽어도 미는 일은 한 번입니다.

    풀은 타입 셋을 위에서부터 훑는다

    재활용은 기본으로 꺼져 있어서(DOTween.defaultRecyclable = false) 보통은 풀이 비어 있습니다. 켜면 죽은 트위너가 _pooledTweeners 배열에 들어가는데, 트위너는 제네릭 인자가 제각각이라 “아무거나 하나” 를 꺼낼 수 없습니다. 생성자가 적어 둔 Type 세 개를 맨 위 칸부터 비교합니다.

    if (totPooledTweeners > 0)
    {
        // 풀을 맨 위 칸부터 내려가며 타입 셋이 모두 같은 것을 찾는다
        for (int i = maxPooledId; i >= minPooledId; --i)
        {
            Tween tween = _pooledTweeners[i];
    
            if (tween != null && 타입 셋이 모두 같다)
            {
                AddActiveTween(tween);
                _pooledTweeners[i] = null;
    
                // 끝 칸일 때만 범위를 좁힌다. 가운데 칸이면 범위 안에 빈칸이 남는다
                if (i == maxPooledId) maxPooledId--;
                else if (i == minPooledId) minPooledId++;
    
                totPooledTweeners--;
                return tween;                  // new 없음
            }
        }
    
        // 맞는 것이 없고 한도에 닿았으면 풀의 하나를 버려 자리를 만든다
        if (totTweeners >= maxTweeners)
        {
            _pooledTweeners[maxPooledId--] = null;
            totPooledTweeners--;
            totTweeners--;
        }
    }
    else if (totTweeners >= maxTweeners - 1)
    {
        IncreaseCapacities(TweenersOnly);      // 한도를 늘리고 경고 로그를 남긴다
    }
    
    var t = new TweenerCore<T1, T2, TPlugOptions>();
    totTweeners++;
    AddActiveTween(t);
    return t;
    상황 지나는 길 결과
    float 요청, 풀 [V3, float, V3] 위에서 둘째 칸에서 찾음 풀에서 꺼냄. 가운데 칸이라 빈칸이 남음
    Color 요청, 같은 풀 풀 셋을 다 헛돈 뒤 new
    풀이 비었고 200번째 트위너 한도 200 에 닿음 한도를 500 으로 늘리고 new. Max Tweens reached: 200/50 → 500/50 경고

    재활용이 꺼져 있으면 풀은 늘 비어 있어 셋째 줄의 길만 탑니다.

    맞는 타입이 없으면 풀을 끝까지 돈 뒤에 new 합니다. float 트윈 1,000개를 한 프레임에 만드는 시간을 풀에 든 것에 따라 비교했습니다.

    풀에 든 것 Mono IL2CPP
    float 트윈 0.12 ms 0.30 ms
    Vector3 트윈 1,000개 5.3 ms 7.6 ms

    Vector3 가 깔린 풀에서는 할당도 풀이 빈 경우와 같은 1개당 344 / 352 B 였습니다. 헛돈 끝에 결국 만드니 할당은 하나도 아끼지 못하고 시간만 25~45배를 씁니다. 시퀀스 풀은 타입이 하나라 Stack 에서 Pop 한 번이면 됩니다.

    용량은 넘칠 때 한꺼번에 2.5배가 된다

    기본 용량은 트위너 200, 시퀀스 50 입니다. 지금 있는 트위너(시퀀스 안의 자식 포함)가 maxTweeners - 1 개일 때 하나를 더 만들면 용량을 늘립니다.

    // DOTween/Core/TweenManager.cs (IncreaseCapacities 줄임)
    int increaseTweenersBy = Mathf.Max((int)(maxTweeners * 1.5f), _DefaultMaxTweeners);
    maxTweeners += increaseTweenersBy;
    Array.Resize(ref _pooledTweeners, maxTweeners);
    Array.Resize(ref _activeTweens, maxActive);
    if (killAdd > 0) _KillList.Capacity += killAdd;

    지금의 1.5배를 더하므로 200 → 500 → 1,250 → 3,125 입니다. 그때마다 풀 배열, 활성 배열, 파기 목록을 새로 잡아 복사하고, 설정 에셋의 기본 로그 수준(Default)에서는 Max Tweens reached 경고도 찍습니다. 늘어난 프레임의 순 할당입니다.

    경계 로그 수준 늘어난 프레임 할당 만드는 호출 시간
    200 → 500 ErrorsOnly 14.1 / 14.1 KB 0.03 / 0.04 ms
    200 → 500 Default 21.9 / 24.2 KB 0.84 / 0.28 ms
    500 → 1,250 ErrorsOnly 32.1 / 32.1 KB 0.02 / 0.03 ms
    1,250 → 3,125 ErrorsOnly 77.1 / 77.1 KB 0.07 / 0.08 ms

    ErrorsOnly 줄은 새로 잡은 참조 칸 수 × 8 B 와 맞고(200 → 500 이면 1,600 칸, 12.8 KB), Default 와의 차이는 경고 로그 몫입니다. 프레임 간격은 늘지 않은 프레임과 구분되지 않았습니다. 한 번의 확장이 치르는 것은 한 프레임에 몰리는 할당입니다. 한 번 늘어난 용량은 줄지 않고, 갱신은 용량이 아니라 마지막으로 찬 칸까지만 훑으므로 큰 용량이 매 프레임 비용이 되지는 않습니다(SetTweensCapacity(20000, 200) 에 트윈 0개인 프레임이 기준선과 같았습니다).

    주의할 점

    1. 용량은 게임 시작 때 넉넉하게 한 번 잡는다

    안 지키면 생기는 문제. 기본 용량 200 은 금방 찹니다. 트윈이 몰리는 연출 프레임에 경계를 넘으면 배열 셋의 재할당(200 경계에서 14~24 KB, 1,250 경계에서 77 KB)과 경고 로그가 그 프레임에 한꺼번에 얹힙니다.

    이렇게 쓴다. Utility Panel 의 Editor Report(showUnityEditorReport)를 켜고 플레이를 끝내면 동시에 활성이었던 최대 수가 찍힙니다. 이 수에는 시퀀스 자식이 빠져 있으니 자식 수를 더해 넉넉하게 DOTween.Init().SetCapacity(t, s) 로 시작할 때 잡습니다.

    2. 풀에 여러 타입을 쌓아 두지 않는다

    안 지키면 생기는 문제. 트위너 풀은 맨 위 칸부터 Type 셋을 비교하는 선형 탐색입니다. Vector3 트윈이 천 개 깔린 풀에서 float 트윈을 요청하면 천 칸을 돈 뒤 결국 새로 만들어, 할당은 그대로이고 시간만 수십 배가 됩니다.

    이렇게 쓴다. 재활용은 전역으로 켜기보다 같은 종류를 대량으로 반복하는 트윈에만 .SetRecyclable(true) 로 붙입니다.

    3. 트윈이 도는 도중에 SetTweensCapacity 를 부르지 않는다

    안 지키면 생기는 문제. SetTweensCapacity 는 받은 값에 맞춰 배열을 Array.Resize 하므로 용량을 줄이기도 합니다. 소스 주석도 “use this method only when there are no tweens running” 이라고 적습니다. 게임 도중 부르면 도는 트윈이 있는 배열을 다시 잡습니다.

    이렇게 쓴다. 용량은 첫 트윈을 만들기 전, DOTween.Init 과 함께 정합니다.

    정리

    TweenManager 는 모든 트윈을 배열 하나에 만든 순서대로 두고, 빈칸은 다음 갱신에 한 번에 메웁니다.

    • 넣을 때는 끝에 붙이고, 뺄 때는 칸에 null 만 씁니다. 압축은 순서를 지켜서 갱신 순서가 곧 만든 순서입니다.
    • 풀은 타입 셋을 위에서부터 훑는 선형 탐색이라, 타입이 섞이면 할당은 못 아끼고 시간만 씁니다.
    • 용량은 200 에서 넘칠 때마다 2.5배가 되며 그 프레임에 할당이 몰립니다. 시작할 때 한 번 잡으면 매 프레임 비용 없이 피합니다.