DOTween 의 Kill 은 갱신 도중이면 표시만 하고 나중에 치운다
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() 을 부르는 프레임입니다.
- A 가 끝나
OnComplete가 불립니다. 그 안의C.Kill()은 루프 도중이라C.active만 끕니다. A 는 autoKill 이라 명단에 오릅니다. 명단은[A]입니다. - B 는 아직 도는 중이라 그대로 갱신됩니다.
- C 의 차례에 꺼진 것을 보고 명단에 올립니다. 명단은
[A, C]입니다. - 루프가 끝나고 명단을 뒤에서부터 치웁니다. 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 는 트윈 차례마다 묻는다
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에서 참조를 끊습니다.