DOTween 의 Kill 은 갱신 도중이면 표시만 하고 나중에 치운다
Esc

    DOTween 의 Kill 은 갱신 도중이면 표시만 하고 나중에 치운다

    2026년 10월 18일 수정–Unity라이브러리트윈
    Tween _blink;
    
    void OnEnable()  => _blink = icon.DOFade(0f, 0.5f).SetLoops(-1, LoopType.Yoyo);
    void OnDisable() => _blink.Kill();

    코루틴은 그것을 돌리는 컴포넌트가 꺼지면 같이 멈춥니다. DOTween 의 트윈은 누구에게도 묶여 있지 않아서, 트윈을 건 오브젝트가 꺼지든 파괴되든 모릅니다. 끝내는 방법이 여럿이고, 콜백 안에서 부른 Kill 이 언제 효과를 내는지도 정해져 있습니다.

    DOTween 에서 트윈이 죽는 길은 모두 Despawn 하나로 모이고, 갱신 루프 도중에 부른 Kill 은 active 만 끈 뒤 루프 끝에 몰아서 치웁니다. 기준은 DOTween 1.3.030(GitHub 커밋 4846808)입니다.

    갱신 도중의 Kill 은 표시만 한다

    Kill, DOKill(target), 완료 뒤 자동 파기, SetLink, SafeMode 가 모두 TweenManager.Despawn 으로 갑니다. Despawn 은 OnKill 을 먼저 부르고, 활성 배열에서 빼고, 재활용이면 풀에 넣은 뒤 Reset 합니다. Kill 의 갈림길은 끝에 있습니다.

    // DOTween/TweenExtensions.cs (Kill 끝부분)
    if (TweenManager.isUpdateLoop) {
        // Just mark it for killing, so the update loop will take care of it
        t.active = false;
    } else TweenManager.Despawn(t);

    콜백은 대부분 루프 안에서 불리므로 OnComplete 나 OnUpdate 안의 Kill 은 표시만 합니다. A 가 끝나며 OnComplete 에서 C.Kill() 을 부르는 프레임입니다.

    isUpdateLoop = true;                       // 이제부터 부르는 Kill 은 표시만 한다
    int len = _maxActiveLookupId + 1;
    
    for (int i = 0; i < len; ++i)
    {
        Tween t = _activeTweens[i];
    
        // 표시된 트윈은 자기 차례에 명단에 오른다. 값은 갱신하지 않는다
        if (!t.active)
        {
            MarkForKilling(t);                 // active = false, _KillList.Add(t)
            willKill = true;
            continue;
        }
    
        // 끝나고 autoKill 이면 명단에 오른다
        if (Tween.DoGoto(t, ...))
        {
            MarkForKilling(t);
            willKill = true;
        }
    }
    
    if (willKill)
        DespawnActiveTweens(_KillList);        // 명단을 뒤에서부터 치운다
    
    _KillList.Clear();
    isUpdateLoop = false;

    활성 배열이 [A, B, C] 이고, A 가 끝나며 OnComplete 에서 C.Kill() 을 부르는 프레임입니다.

    1. A 가 끝나 OnComplete 가 불립니다. 그 안의 C.Kill() 은 루프 도중이라 C.active 만 끕니다. A 는 autoKill 이라 명단에 오릅니다. 명단은 [A] 입니다.
    2. B 는 아직 도는 중이라 그대로 갱신됩니다.
    3. C 의 차례에 꺼진 것을 보고 명단에 올립니다. 명단은 [A, C] 입니다.
    4. 루프가 끝나고 명단을 뒤에서부터 치웁니다. C 의 OnKill, A 의 OnKill 순서로 불리고, 활성 배열은 [_, B, _] 가 됩니다.

    루프를 도는 동안 배열의 칸은 움직이지 않습니다. 빼는 일은 모두 루프가 끝난 뒤에 몰려 있습니다.

    순회하는 배열에서 원소를 빼면 인덱스가 흔들리니, 표시만 해 두고 자기 차례에 명단에 올린 뒤 루프 끝에 명단의 역순으로 치웁니다. 그래서 이미 지나간 칸의 트윈은 한 프레임 늦습니다. Normal 트윈 A, B, C 에서 B 의 OnUpdate 가 A 와 C 를 죽이자, C 의 OnKill 은 그 프레임에, A 의 OnKill 은 다음 프레임에 불렸습니다. 루프 밖(버튼 처리, OnDisable)의 Kill 은 그 자리에서 바로 Despawn 합니다.

    DOKill(target) 은 활성 배열 전체를 훑는다

    transform.DOKill() 은 대상별 색인 없이 활성 배열 전체를 뒤에서부터 보며 Equals 로 target 과 id 를 비교합니다. 아무 트윈도 걸리지 않은 Transform 에 DOKill() 을 부른 한 번의 비용입니다.

    활성 트윈 수 Mono IL2CPP
    100 2.1 µs 1.4 µs
    1,000 22 µs 16 µs
    10,000 236 µs 160 µs

    활성 트윈 하나에 Mono 약 22 ns, IL2CPP 약 15 ns 로 거의 정확히 비례합니다. 트윈 1만 개가 떠 있을 때 DOKill 100번이면 Mono 에서 약 24 ms 입니다. string 이나 int id 로 부르면 그 필드만 비교합니다. 단, intId 의 기본값이 -999 라서 DOTween.Kill(-999) 는 int id 를 안 건 트윈 전부와 맞습니다(같은 세 환경에서 id 없는 트윈 3개와 string id 트윈 1개가 함께 죽었습니다).

    SetLink(gameObject) 는 이벤트를 구독하지 않고, 갱신 루프가 트윈 하나를 갱신할 때마다 가장 먼저 링크된 오브젝트를 들여다봅니다. PauseOnDisablePlayOnEnable 로 건 트윈이 세 상황을 지나는 모습입니다.

    if (!_TweenLinks.TryGetValue(t, out tLink))
        return;                                // 링크가 없는 트윈
    
    // 필드 타입이 GameObject 라 유니티의 == 가 파괴된 오브젝트를 null 로 본다
    if (tLink.target == null)
    {
        t.active = false;                      // 파괴됨. 곧바로 명단에 오른다
        return;
    }
    
    bool goActive = tLink.target.activeInHierarchy;
    bool justEnabled = !tLink.lastSeenActive && goActive;    // 지난번엔 꺼져 있었다
    bool justDisabled = tLink.lastSeenActive && !goActive;   // 지난번엔 켜져 있었다
    tLink.lastSeenActive = goActive;
    
    // case LinkBehaviour.PauseOnDisablePlayOnEnable:
    if (justDisabled)
        Pause(t);
    else if (justEnabled)
        Play(t);

    PauseOnDisablePlayOnEnable 로 건 트윈이 만나는 세 상황입니다.

    상황 판정 결과
    켜진 채 바뀐 것 없음 그대로 재생
    방금 꺼짐 justDisabled Pause
    파괴됨 (일시정지 중이어도) target == null 죽음

    KillOnDestroy 에는 따로 처리하는 코드가 없습니다. 모든 행동에 공통인 target == null 검사가 그 일을 합니다.

    링크 검사는 isPlaying 검사보다 앞이라 일시정지된 트윈도 파괴되면 죽습니다. 반응은 파괴한 순간이 아니라 그 뒤 트윈의 차례에 옵니다.

    없앤 방법 (DOTween 보다 먼저 도는 스크립트) OnKill
    Update 나 LateUpdate 에서 Destroy(go) 다음 프레임
    Update 에서 DestroyImmediate(go) 같은 프레임

    Destroy 는 프레임 끝에야 오브젝트를 지우므로 링크 트윈은 한 프레임 더 돕니다. 그리고 링크가 하나라도 있으면 모든 트윈이 딕셔너리를 한 번씩 조회합니다. IL2CPP 플레이어에서 DOMove 1만 개에 링크 하나를 더하자 PlayerLoop 이 1.72 ms 에서 2.49 ms 로, 트윈 하나에 약 74 ns 늘었습니다. Mono 에서는 트윈당 5 ns 로 흔들림에 묻혔습니다.

    재활용을 켜면 옛 참조가 남의 트윈이 된다

    재활용은 기본 꺼짐입니다. 켜면 죽은 트윈 객체가 풀에 들어가 같은 타입의 다음 트윈으로 그대로 다시 쓰이는데, 사용자 코드의 변수는 여전히 그 객체를 가리킵니다.

    _fade = image.DOFade(0f, 1f);              // 트윈 객체 X
    // 1초 뒤 완료 → autoKill → Despawn(X)
    var other = otherImage.DOFade(1f, 1f);     // 같은 타입의 새 트윈
    _fade.Kill();
    줄 재활용 켬 재활용 끔 (기본)
    _fade = ... _fade 가 X _fade 가 X
    완료 뒤 X 가 풀로 들어감. _fade 는 여전히 X X 는 버려짐. _fade 는 여전히 X
    other = ... 풀에서 X 를 꺼내 씀. other 도 X 새 객체 Y. other 는 Y
    _fade.Kill() X 는 지금 살아 있음. other 의 트윈이 죽음 X 는 죽어 있어 아무 일 없음

    재활용을 켜면 트윈 객체가 다른 트윈으로 다시 쓰여도 변수는 그대로라, IsActive() 도 true 를 돌려줍니다.

    재활용을 켜고 끝난 a 의 참조를 둔 채 b 를 만들자 같은 인스턴스였고, a.Kill() 뒤 b.IsActive() 가 false 였습니다. IsActive() 로 걸러지지도 않습니다. Despawn 은 OnKill 을 풀에 넣기 전에 부르므로, 소스 주석이 권하는 .OnKill(() => myTweenReference = null) 이 참조를 끊는 확실한 자리입니다.

    주의할 점

    1. 재활용을 켰으면 OnKill 에서 참조를 끊는다

    안 지키면 생기는 문제. 필드에 담아 둔 트윈이 끝나 풀로 돌아간 뒤 같은 타입의 다른 트윈이 그 객체를 가져갑니다. 옛 필드로 Kill() 이나 Pause() 를 부르면 전혀 다른 오브젝트의 연출이 멈추고, “같은 타입이 마침 그 사이에 생성됨” 이 조건이라 가끔만 터집니다.

    이렇게 쓴다. 트윈을 필드에 담으면 OnKill(() => _fade = null) 을 함께 겁니다. 오래 들고 있을 트윈에는 SetRecyclable(false) 를 겁니다. 재활용이 꺼진 기본 설정에서는 옛 참조가 active = false 로 남아 조작이 전부 무시됩니다.

    2. 무한 루프 트윈은 Complete 가 아니라 Kill 로 끝낸다

    안 지키면 생기는 문제. Complete 의 첫 줄이 if (t.loops == -1) return false; 입니다. SetLoops(-1) 트윈에 Complete() 를 부르면 예외도 로그도 없이 계속 돌고(IsComplete() 는 false, IsActive() 는 true), OnComplete 에 걸어 둔 다음 단계도 시작되지 않습니다. Kill(true) 도 완료 단계를 건너뜁니다.

    이렇게 쓴다. 무한 루프는 Kill() 로 끝내고, 끝값이나 시작값에 놓여야 하면 Goto 나 Rewind() 를 먼저 부르거나 값을 직접 넣습니다.

    3. DOKill(target) 을 대량으로 부르지 않는다

    안 지키면 생기는 문제. 풀링된 탄환 수백 개가 같은 프레임에 반환되며 각자 OnDisable 에서 transform.DOKill() 을 부르면, 활성 트윈 수 × 호출 수만큼 비교가 한 프레임에 몰립니다. 활성 트윈 1만 개에서 백 번이면 16~24 ms 입니다.

    이렇게 쓴다. 자주 끄고 켜는 오브젝트는 트윈 참조를 필드로 들고 _tween.Kill() 로 직접 죽입니다. 여러 트윈을 한 번에 치우려면 0 이상의 int id 로 묶어 DOTween.Kill(id) 를 한 번 부릅니다.

    4. SafeMode 는 켜 두고 경고를 읽는다

    안 지키면 생기는 문제. SafeMode 는 시작값 읽기와 값 적용의 예외를 잡아 트윈을 죽이고, 콜백 예외는 경고만 남기고 트윈을 살려 둡니다. 끄면 파괴된 대상 하나의 예외가 갱신 루프를 끊어 그 프레임의 뒤 차례 트윈들이 갱신되지 않고, 다음 갱신이 끝날 때까지 루프 밖의 Kill 도 표시만 됐습니다. 켜 둔 채 safeModeLogBehaviour 를 None 으로 두면 콜백 버그가 흔적 없이 삼켜집니다.

    이렇게 쓴다. 트윈 1만 개에서 비용이 1.7% 정도라 켜 두고, 로그 수준은 Warning 이상으로 둡니다. “Target or field is missing/null” 경고가 보이면 그 트윈에 SetLink 가 빠진 것입니다.

    정리

    DOTween 의 트윈은 수명을 코드가 챙기고, 모든 파기는 Despawn 하나로 모이며 갱신 도중의 Kill 은 루프 끝으로 미뤄집니다.

    • 표시된 트윈은 자기 차례에 명단에 올라 루프 끝에 역순으로 치워지고, 이미 지나간 칸이면 OnKill 이 한 프레임 늦습니다.
    • DOKill(target) 은 활성 트윈 수에 비례하고, SetLink 는 트윈 차례마다 묻는 폴링이라 Destroy 에 한 프레임 늦게 반응합니다.
    • 재활용을 켜면 옛 참조가 남의 트윈을 조작하므로 OnKill 에서 참조를 끊습니다.