DOTween 은 트윈을 만들 때만 할당한다
void OnClick(Vector3 dest)
{
transform.DOMove(dest, 0.3f).SetEase(Ease.OutQuad);
}
“DOTween 은 트윈을 풀링하니까 할당이 없다” 는 말을 믿고 이벤트마다 트윈을 만들면, 프로파일러의 GC Alloc 열에 클릭할 때마다 수백 바이트가 찍힙니다.
DOTween 의 비용은 만들 때 할당이 몰리고, 도는 동안에는 할당이 없는 모양입니다. 재활용은 기본으로 꺼져 있고, 켜도 람다 몫은 매번 생깁니다. 기준은 DOTween 1.3.030(GitHub 커밋 4846808)이고, 할당은 ProfilerRecorder 의 “GC Allocated In Frame” 으로 쟀습니다.
DOMove 한 번은 힙 객체 넷을 만든다
DOMove 는 target 을 붙잡는 두 람다를 DOTween.To 에 넘깁니다. 컴파일러는 붙잡힌 변수를 담은 캡처 객체 하나를 두 람다가 함께 쓰게 만들고, 거기에 델리게이트 둘과 트윈 객체가 더해집니다. 설치된 DLL 의 DOMove IL 에도 newobj 가 셋(캡처 클래스, DOGetter<Vector3>, DOSetter<Vector3>)입니다. 트윈 객체는 TweenManager.GetTweener 에서 갈립니다.
// ShortcutExtensions.DOMove
// 람다 둘이 target 을 붙잡는다. 캡처 객체 1개, DOGetter 1개, DOSetter 1개가 여기서 생긴다
var t = DOTween.To(() => target.position, x => target.position = x, endValue, duration);
// DOTween.ApplyTo: 트윈 객체를 매니저에게 청한다
var tweener = TweenManager.GetTweener<T1, T2, TPlugOptions>();
// TweenManager.GetTweener
if (totPooledTweeners > 0) // 재활용이 꺼져 있으면 풀은 늘 비어 있다
{
for (int i = _maxPooledTweenerId; i > _minPooledTweenerId - 1; --i)
{
Tween tween = _pooledTweeners[i];
// 세 타입이 모두 같은 트윈을 찾으면 그것을 다시 쓴다
if (tween != null && tween.typeofT1 == typeofT1 && tween.typeofT2 == typeofT2
&& tween.typeofTPlugOptions == typeofTPlugOptions)
{
t = (TweenerCore<T1, T2, TPlugOptions>)tween;
AddActiveTween(t);
_pooledTweeners[i] = null;
totPooledTweeners--;
return t; // new 없음
}
}
}
t = new TweenerCore<T1, T2, TPlugOptions>(); // 풀에 없으면 새로 만든다
totTweeners++;
AddActiveTween(t);
return t;
// 다시 DOTween.ApplyTo: 델리게이트를 필드에 넣고 기본값을 채운다. 새로 만드는 것은 없다
bool setupSuccessful = Tweener.Setup(tweener, getter, setter, endValue, duration, plugin);
// 다시 DOMove
t.SetOptions(snapping).SetTarget(target);
| 람다 쪽 | 트윈 객체 | 새 힙 객체 | |
|---|---|---|---|
| 재활용 끔 (기본) | 캡처, getter, setter 3개 | 새로 만듦 | 4개 (656 B) |
| 재활용 켬, 풀에 같은 타입 | 캡처, getter, setter 3개 | 풀에서 꺼냄 | 3개 (280 B) |
재활용을 켜도 람다는 호출마다 새로 만듭니다. 줄어드는 것은 트윈 객체 하나입니다.
한 프레임에 트윈 100개를 만들어 한 개당으로 나눈 값입니다.
| 경로 | 에디터 배치모드 | Mono 플레이어 | IL2CPP 플레이어 |
|---|---|---|---|
DOMove, 재활용 OFF (기본) |
656 B | 656 B | 656 B |
DOMove, 재활용 ON, 풀이 찬 둘째 회 |
280 B | 280 B | 280 B |
차이 376 B 가 트윈 객체이고, 280 B 는 재활용을 켜도 남는 캡처 객체와 델리게이트 둘입니다. 세 조건이 바이트 단위로 같아서, 이 크기는 백엔드가 아니라 객체 구성에서 나옵니다. 손으로 쓴 Update 보간은 필드에 값을 넣을 뿐 시작할 때 아무것도 만들지 않고, 코루틴은 시작할 때마다 이터레이터 객체를 만듭니다.
도는 동안에는 할당하지 않는다
매 프레임 경로(갱신 루프 → 위치 계산 → 플러그인 → setter)에는 new 가 없습니다. 값은 제네릭 필드와 인자로 흐르고, 콜백은 이미 있는 델리게이트를 부릅니다. 플레이어에서 DOMove 10,000개가 도는 프레임의 할당 중앙값이 트윈 없는 프레임과 같은 686 B(Mono), 722 B(IL2CPP)였습니다. 트윈이 더한 몫은 0 B 입니다.
프레임 시간은 트윈 수에 비례합니다(서로 다른 Transform 에 건 DOMove, PlayerLoop 중앙값의 기준선 대비 증가분).
| 동시 트윈 수 | Mono 플레이어 | 트윈 하나당 | IL2CPP 플레이어 | 트윈 하나당 |
|---|---|---|---|---|
| 1,000 | +0.194 ms | 194 ns | +0.100 ms | 100 ns |
| 10,000 | +1.990 ms | 199 ns | +1.384 ms | 138 ns |
에디터 배치모드에서도 10,000개에서 트윈당 178 ns 였습니다. 할당이 문제가 되는 것은 트윈이 많을 때가 아니라 트윈을 자주 만들 때입니다.
트윈 하나를 들고 다시 쓰면 0 B 다
재활용을 켜도 람다 몫이 남으니, 할당을 없애는 길은 만드는 호출 자체를 줄이는 것입니다. 되풀이되는 연출은 트윈 하나를 살려 두고 목표만 바꿉니다.
TweenerCore<Vector3, Vector3, VectorOptions> _move; // Tweener 가 아니라 TweenerCore 로 든다
void Awake()
{
_move = transform.DOMove(transform.position, 0.3f)
.SetEase(Ease.OutQuad).SetAutoKill(false).SetLink(gameObject).Pause();
}
public void MoveTo(Vector3 dest)
{
_move.ChangeEndValue(dest, true); // 소스가 "NO-GC METHOD" 라고 적은 제네릭 오버로드
_move.Restart();
}
세 방식을 100회씩 되풀이한 1회당 할당은 656 B, 280 B, 0 B 였습니다. 필드를 Tweener 로 선언하면 ChangeEndValue(object ...) 가 골라져 Vector3 박싱으로 28 B 가 생겼습니다. 트윈을 들고 있을 수 없는 자리에서는 델리게이트 둘을 Awake 에서 한 번 만들어 DOTween.To(_get, _set, dest, 0.3f).SetRecyclable(true) 로 넘기면, 풀이 찬 상태에서 1회당 0.2 B(Mono), 0.0 B(IL2CPP)였습니다.
주의할 점
1. “DOTween 은 풀링한다” 를 믿고 할당을 재지 않고 넘어가지 않는다
안 지키면 생기는 문제. 재활용은 기본으로 꺼져 있어서(DOTween.defaultRecyclable = false) 설정을 건드리지 않은 프로젝트에서는 DOMove 한 번이 656 B 입니다. 켜도 280 B 가 남습니다. 이벤트마다 트윈을 만드는 코드가 자주 불리는 자리에서 GC 가 치울 거리를 계속 쌓습니다.
이렇게 쓴다. 프로파일러의 GC Alloc 열에서 트윈을 만드는 프레임을 직접 봅니다. 자주 불리는 연출은 트윈 하나를 SetAutoKill(false) 로 살려 두고 ChangeEndValue 와 Restart 로 다시 씁니다.
2. 다시 쓸 트윈 참조를 Tweener 로 선언하지 않는다
안 지키면 생기는 문제. Tweener 에는 AOT 때문에 object 를 받는 ChangeEndValue 만 보여서, 값 타입 목표를 넘길 때마다 박싱이 일어납니다. 할당을 없애려고 트윈을 재사용했는데 여기서 1회당 28 B 가 다시 생깁니다.
이렇게 쓴다. 필드를 숏컷의 반환 타입 그대로 TweenerCore<Vector3, Vector3, VectorOptions> 로 선언하거나 var 로 받습니다. 제네릭 ChangeEndValue(T2 ...) 가 골라집니다.
3. 매 프레임 트윈을 새로 만들지 않는다
안 지키면 생기는 문제. 마우스를 따라가게 하려고 Update 안에서 DOMove 를 부르면 프레임마다 트윈 객체와 람다를 만들고, 앞 트윈과 새 트윈이 같은 위치를 두고 겹칩니다. 60 fps 면 초당 약 39 KB 가 쌓이고 동시 트윈 수가 늘어납니다.
이렇게 쓴다. 목표가 매 프레임 바뀌는 추적은 손으로 쓴 Update 보간(Vector3.MoveTowards, SmoothDamp)으로 합니다. 트윈을 꼭 써야 하면 하나를 들고 ChangeEndValue(target, true) 로 목표만 바꿉니다.
정리
DOTween 은 트윈을 만들 때만 할당하고, 도는 동안에는 할당하지 않습니다.
DOMove한 번은 캡처 객체, 델리게이트 둘, 트윈 객체로 656 B 입니다. 재활용은 기본 꺼짐이고, 켜도 람다 몫 280 B 는 남습니다.- 도는 동안의 할당은 0 B 이고, 프레임 시간은 트윈 수에 비례해 트윈당 약 100~200 ns 입니다.
- 트윈 하나를
TweenerCore로 들고ChangeEndValue와Restart로 다시 쓰면 1회당 0 B 입니다.