DOTween 의 트윈 하나는 값 세 개와 람다 두 개를 든 객체다
Tweener t = transform.DOMove(new Vector3(0, 3, 0), 1f);
// 다음 프레임에
t.SetLoops(-1); // 반복이 안 된다
트윈 참조를 필드에 두고 나중에 설정을 바꾸거나 다시 쓰려면, 그 객체가 무엇을 들고 있고 무엇이 언제 굳는지 알아야 합니다. 위 코드는 예외도 경고도 없이 아무 일도 하지 않습니다.
DOTween 의 트윈 하나는 값 세 개와 getter·setter 두 람다, 옵션 구조체를 든 TweenerCore 객체 하나입니다. 만들 때 전역 기본값을 복사해 두고, 첫 갱신부터 생성 설정을 잠급니다. 기준은 DOTween 1.3.030(GitHub 커밋 4846808)입니다.
트윈은 제네릭 객체 하나다
// DOTween/Core/TweenerCore.cs
public class TweenerCore<T1,T2,TPlugOptions> : Tweener where TPlugOptions : struct, IPlugOptions
{
public T2 startValue, endValue, changeValue;
public TPlugOptions plugOptions;
public DOGetter<T1> getter;
public DOSetter<T1> setter;
internal ABSTweenPlugin<T1, T2, TPlugOptions> tweenPlugin;
T1 은 대상 값의 타입, T2 는 트윈이 값을 들고 있는 형식입니다. DOMove 는 <Vector3, Vector3, VectorOptions> 로 둘이 같고, DORotate 는 Quaternion 을 주고받으면서 오일러 각 Vector3 로 들고 있어 360 도를 넘는 끝값을 줄 수 있습니다. 시간과 상태(duration, position, loops, isPlaying), 콜백은 부모 Tween 이 듭니다. 매니저는 모든 트윈을 Tween[] 하나에 넣고 가상 메서드로만 다루므로 제네릭 인자를 몰라도 됩니다.
DOMove 는 끝값과 기본값만 적는다
DOMove 한 줄이 이 객체를 채우는 길입니다. 세 메서드를 이어 붙이고 줄였습니다.
// ShortcutExtensions.DOMove
// 람다 둘이 target 을 붙잡는다. 캡처 객체 하나와 델리게이트 둘이 생긴다
var t = DOTween.To(() => target.position, x => target.position = x, endValue, duration);
// DOTween.ApplyTo<Vector3, Vector3, VectorOptions>
InitCheck(); // 처음 부른 것이면 [DOTween] 구동기가 생긴다
var tweener = TweenManager.GetTweener<Vector3, Vector3, VectorOptions>(); // 새 TweenerCore 를 활성 배열 끝에
Tweener.Setup(tweener, getter, setter, endValue, duration);
// Tweener.Setup
if (t.tweenPlugin == null)
t.tweenPlugin = PluginsManager.GetDefaultPlugin<...>(); // Vector3Plugin
t.getter = getter;
t.setter = setter;
t.endValue = endValue; // 끝값과 길이만 적는다
t.duration = duration; // 시작값은 비어 있다. 지금 위치를 읽지 않는다
// 전역 기본값을 트윈마다 복사한다
t.autoKill = DOTween.defaultAutoKill;
t.isRecyclable = DOTween.defaultRecyclable;
t.easeType = DOTween.defaultEaseType;
t.loopType = DOTween.defaultLoopType;
t.isPlaying = (defaultAutoPlay 가 All 이나 AutoPlayTweeners); // 기본은 곧바로 재생
// 다시 DOMove
t.SetOptions(snapping).SetTarget(target); // target 은 DOKill(transform) 이 이 트윈을 찾는 열쇠
return t;
이 한 줄이 끝난 뒤 트윈 객체에 든 것입니다.
| 필드 | 값 |
|---|---|
getter, setter |
두 람다 |
tweenPlugin |
Vector3Plugin |
endValue, duration |
(0, 3, 0), 1 |
startValue |
비어 있음 |
easeType, loopType, autoKill, isRecyclable |
지금의 전역 기본값을 복사한 것 |
isPlaying |
true |
target |
이 Transform |
전역 기본값은 만들 때 복사됩니다. 이징, 루프 방식, 자동 파기, 재활용 여부가 트윈마다 필드로 들어가서, 나중에 DOTween.defaultEaseType 을 바꿔도 이미 만든 트윈은 그대로입니다. 그리고 DOMove 는 위치를 읽지 않습니다. 시작값은 비어 있다가 첫 갱신에서 getter 로 읽힙니다.
첫 갱신부터 생성 설정이 잠긴다
트윈이 처음 갱신되는 순간 creationLocked 가 true 가 되고, 생성 설정 메서드는 첫 줄에서 그냥 돌아갑니다.
// DOTween/TweenSettingsExtensions.cs
public static T SetLoops<T>(this T t, int loops) where T : Tween
{
if (t == null || !t.active || t.creationLocked) return t;
| 첫 갱신 뒤에는 무시된다 | 언제든 먹힌다 |
|---|---|
SetLoops, SetDelay, SetAutoKill, SetRelative, SetSpeedBased, SetInverted, From |
SetEase, SetId, SetTarget, SetLink, SetRecyclable, SetUpdate, SetOptions, OnComplete 같은 콜백 |
딜레이가 남아 아직 움직이지 않는 트윈도 첫 갱신은 지나갔으므로 잠깁니다. 시간 구조를 바꾸는 설정은 만든 프레임 안에서만 먹습니다.
재활용용 Reset 은 두 필드를 남긴다
재활용을 켜면 죽은 트윈이 풀로 돌아가고, 같은 <T1, T2, TPlugOptions> 를 요청한 호출이 그 객체를 다시 씁니다. 흔적을 지우는 것은 Reset() 입니다. SetInverted() 와 OnPause 를 걸어 둔 재활용 트윈이 끝나는 과정입니다.
// TweenManager.Despawn(t)
if (t.onKill != null)
Tween.OnTweenCallback(t.onKill, t); // 죽기 직전 onKill 이 먼저 불린다
RemoveActiveTween(t); // 활성 배열의 제 칸을 비운다
if (t.isRecyclable)
_pooledTweeners[빈 칸] = t; // 재활용 트윈은 풀로
t.active = false;
t.Reset();
// Tween.Reset()
timeScale = 1;
isBackwards = false;
id = null; stringId = null; intId = -999;
onStart = onPlay = onRewind = onUpdate = onComplete = onStepComplete = onKill = null; // onPause 가 없다
target = null;
duration = 0; loops = 1; delay = 0;
isRelative = false;
creationLocked = startupDone = playedOnce = false;
position = completedLoops = 0;
// TweenerCore.Reset()
plugOptions.Reset();
getter = null; // 람다를 놓아 풀에 있는 동안 대상을 붙잡지 않는다
setter = null;
SetInverted() 와 OnPause 를 건 재활용 트윈이 끝나 풀에 들어가면 이렇게 남습니다.
| 필드 | Reset 뒤 |
|---|---|
target, getter, setter, onComplete 등 콜백 일곱 |
지워짐 |
duration, loops, position |
처음으로 |
onPause |
남음 |
isInverted |
남음 |
easeType, 값 세 개 |
남지만 다음 Setup 과 첫 갱신이 다시 씀 |
이징과 값 세 개는 다음 Setup 과 첫 갱신이 다시 쓰지만, isInverted 와 onPause 는 다시 쓰는 곳이 없습니다. 재활용을 켜고 OnPause 를 건 트윈 a 를 끝낸 뒤 같은 타입 b 를 만들자 ReferenceEquals(a, b) 가 true 였고, b.Pause() 한 번에 a 의 콜백이 불렸습니다. SetInverted() 한 트윈도 같은 방식으로 새 트윈의 isInverted 가 true 로 남았습니다. 위치를 duration - position 으로 뒤집는 표식이라 새 트윈이 거꾸로 재생됩니다.
주의할 점
1. 생성 설정은 만든 자리에서 끝낸다
안 지키면 생기는 문제. SetLoops, SetDelay, SetRelative, From 같은 설정은 트윈이 한 번이라도 갱신된 뒤에는 조용히 무시됩니다. 트윈 참조를 필드에 두었다가 다음 프레임에 덧붙이면 아무 일도 일어나지 않는데, 같은 자리에서 SetEase 나 OnComplete 는 먹혀서 원인을 찾기 어렵습니다.
이렇게 쓴다. 트윈을 만드는 식 한 줄에서 체인으로 모두 붙입니다. 나중에 바꿔야 하는 것이 값이면 ChangeEndValue 를 쓰고, 시간 구조가 바뀌어야 하면 트윈을 새로 만듭니다.
2. 재활용을 켰으면 SetInverted 와 OnPause 를 쓴 트윈은 재활용에서 뺀다
안 지키면 생기는 문제. Reset 이 isInverted 와 onPause 를 지우지 않아서, 풀에서 그 객체를 받은 다음 트윈이 반전된 채로 재생되거나 멈출 때 옛 콜백을 부릅니다. 증상이 전혀 다른 곳의 트윈에서 가끔 나타납니다.
이렇게 쓴다. 두 기능을 쓰는 트윈에는 .SetRecyclable(false) 를 붙여 풀로 돌아가지 않게 합니다.
3. 전역 기본값은 트윈을 만들기 전에 정한다
안 지키면 생기는 문제. Setup 이 defaultEaseType, defaultLoopType, defaultAutoKill, defaultRecyclable 을 트윈마다 복사해 둡니다. 게임 도중 기본값을 바꾸면 이미 만든 트윈은 옛 값으로 돌고, 새 트윈만 새 값을 따릅니다. 같은 화면의 트윈끼리 동작이 갈립니다.
이렇게 쓴다. 기본값은 DOTween.Init 이나 설정 에셋으로 게임 시작 때 한 번 정합니다. 트윈 하나만 다르게 하려면 그 트윈에 SetEase, SetAutoKill 을 붙입니다.
정리
DOTween 의 트윈 하나는 값 세 개와 getter·setter, 옵션 구조체를 든 TweenerCore 객체입니다.
DOMove는 끝값, 길이, 전역 기본값의 복사본만 적고 위치는 읽지 않습니다.- 첫 갱신부터
creationLocked가 루프, 지연, 상대값 같은 생성 설정을 조용히 막습니다. - 재활용 때
Reset은 콜백, 대상, 람다를 지우지만isInverted와onPause는 남깁니다.