DOTween 은 시작값을 딜레이가 끝난 첫 갱신에서 읽는다
transform.DOMoveX(10f, 1f).SetDelay(1f); // 출발점은 언제 정해질까
끝값만 있고 시작값이 없습니다. 코루틴이라면 var from = transform.position; 을 yield 위에 적었는지 아래에 적었는지가 답이지만, DOTween 에서는 라이브러리가 그 줄을 대신 부릅니다. 딜레이 동안 다른 코드가 대상을 옮기면 어디서 출발하는지가 여기에 달려 있습니다.
DOTween 은 시작값을 트윈을 만들 때가 아니라 딜레이가 끝난 첫 갱신에서 읽습니다. 상대 끝값, 변화량, 속도 기반 길이도 그때 Startup 한 번에 정합니다. 기준은 설치본 DOTween 1.3.030(GitHub 커밋 4846808)입니다.
딜레이가 끝나야 시작값을 읽는다
트윈 하나의 갱신은 딜레이가 남아 있으면 return false 로 빠지고, 딜레이가 끝난 뒤에야 Startup 을 부릅니다. x = 0 에서 만든 트윈의 대상을 딜레이 도중 x = 4 로 옮기는 경우입니다(한 프레임 0.4초, Linear).
if (!t.isPlaying)
return false;
t.creationLocked = true; // 첫 갱신에서 잠긴다. 이 뒤로 SetRelative, From 같은 설정은 무시된다
float tDelta = dt * t.timeScale;
if (!t.delayComplete)
{
tDelta = t.UpdateDelay(t.elapsedDelay + tDelta); // 딜레이가 끝나면 남은 시간을 돌려준다
if (tDelta <= 0)
return false; // 아직 딜레이 중
}
if (!t.startupDone)
t.Startup(); // 여기서 getter 로 시작값을 읽는다
// 목표 위치 계산 → DoGoto → setter
x = 0 에서 DOMoveX(10, 1).SetDelay(1) 을 만들고, 딜레이 도중 다른 스크립트가 대상을 x = 4 로 옮기는 경우입니다. 한 프레임은 0.4초, Linear 입니다.
| 프레임 | 기다린 시간 | 하는 일 | x |
|---|---|---|---|
| 1 | 0.4 | 딜레이 중. 설정은 잠김 | 0 |
| 2 | 0.8 | 딜레이 중. 이 프레임 뒤에 대상이 옮겨짐 | 4 |
| 3 | 1.2 | 딜레이가 끝나고 0.2초가 남음. 시작값을 4 로 읽음 | 4 + 6 × 0.2 = 5.2 |
트윈을 만든 순간의 x = 0 은 어디에도 쓰이지 않았습니다.
재 보니 위 표 그대로 시작값이 4 로 읽히고 x = 5.2 가 됐습니다. 시작하기 전의 트윈에 Goto, Rewind, Complete, ForceInit 을 부르면 그 호출 순간 시작값이 읽힙니다.
Startup 은 시작값, 상대 끝값, 변화량, 길이를 차례로 정한다
순서가 곧 의존 관계입니다. 시작값이 있어야 상대 끝값을 더하고, 끝값이 확정돼야 변화량이 나오고, 변화량이 있어야 속도를 길이로 바꿉니다. x = 2 에서 DOMoveX(5, 2) 를 세 가지로 불러 봅니다.
t.startupDone = true; // 맨 먼저 켠다. 실패해도 다시 하지 않는다
if (!t.hasManuallySetStartValue)
t.startValue = t.getter(); // 대상의 지금 값
if (t.isRelative)
t.endValue += t.startValue; // 넘긴 값은 끝값이 아니라 이동량이었다
t.changeValue = t.endValue - t.startValue; // 여기서 한 번만 계산한다
if (t.isSpeedBased)
t.duration = Mathf.Abs(t.changeValue) / t.duration; // duration 칸의 값을 속도로 읽고 길이로 덮어쓴다
t.fullDuration = t.loops > -1 ? t.duration * t.loops : Mathf.Infinity;
if (t.duration <= 0)
t.easeType = Ease.INTERNAL_Zero;
x = 2 에서 DOMoveX(5, 2) 를 세 가지로 부르면 이렇습니다.
| 호출 | 시작값 | 끝값 | 변화량 | 길이 |
|---|---|---|---|---|
| 기본 | 2 | 5 | 3 | 2초 |
.SetRelative() |
2 | 7 | 5 | 2초 |
.SetSpeedBased() |
2 | 5 | 3 | 1.5초 (초당 2) |
순서가 곧 의존 관계입니다. 시작값이 있어야 상대 끝값을 더하고, 끝값이 확정돼야 변화량이 나오고, 변화량이 있어야 속도를 길이로 바꿉니다.
startupDone 이 맨 먼저 켜지므로 getter 가 예외를 던져 실패하면 다시 시도하지 않고 트윈을 파기합니다. changeValue 는 여기서 한 번만 계산되고, 매 프레임은 start + change × ease 만 합니다. From() 이나 ChangeStartValue 로 시작값을 직접 준 트윈은 getter 호출을 건너뜁니다.
속도 기반 트윈의 길이는 Startup 전에는 없습니다. x = 2 에서 만든 DOMoveX(5f, 2f).SetSpeedBased() 의 Duration() 은 만든 직후 2(넘긴 속도), 첫 갱신 뒤 1.5(거리 3 / 속도 2)였습니다(같은 세 환경). 시퀀스에 넣으면 속도 기반이 꺼지고 넘긴 숫자를 초로 씁니다.
From 은 붙이는 줄에서 곧바로 값을 쓴다
From() 기본형은 Startup 을 기다리지 않고, 붙이는 순간 getter 와 setter 를 부릅니다.
// .From() 을 체인에 붙이는 순간 (setImmediately = true)
if (t.creationLocked || !t.isFromAllowed)
return t; // 한 번이라도 갱신된 트윈에는 안 먹는다
t.isFrom = true;
var prevEnd = t.endValue; // 넘긴 값
t.endValue = t.getter(); // 지금 값이 끝값이 된다
t.startValue = isRelative ? t.endValue + prevEnd : prevEnd;
t.setter(t.startValue); // 대상을 그 자리에서 옮긴다
t.hasManuallySetStartValue = true; // Startup 이 시작값을 다시 읽지 않는다
x = 2 에서 부르면 이렇습니다.
| 호출 | 시작값 | 끝값 | From() 직후 x |
|---|---|---|---|
DOMoveX(5, 1).From() |
5 | 2 | 5 |
DOMoveX(5, 1).From(true) |
2 + 5 = 7 | 2 | 7 |
시작값을 정하는 getter 와 대상에 값을 쓰는 setter 가 둘 다 From() 을 부른 그 줄에서 실행됩니다. 첫 갱신을 기다리지 않습니다.
즉시 쓰는 경로는 Startup 과 달리 SafeMode 의 try 안에 있지 않습니다. 대상을 Destroy 한 다음 프레임에 들고 있던 Transform 으로 트윈을 만들어 봤습니다(SafeMode 켬).
상대값은 Startup 에서 더하고, 순서를 바꾸면 두 번 더한다
SetRelative() 는 플래그만 켜고, end += start 는 Startup 에서 합니다. 상대 이동의 기준도 딜레이가 끝난 순간의 위치입니다. From 트윈에는 SetRelative 가 막히지만, 순서를 거꾸로 쓴 .SetRelative().From() 은 막히지 않습니다. From() 이 x = 5 로 옮기고(start 5, end 2), Startup 이 켜져 있던 플래그로 end 에 start 를 한 번 더 더해 end = 7 이 됩니다.
x = 2 에서 DOMoveX(5f, 1f) 에 |
만든 직후 x | 끝난 뒤 x |
|---|---|---|
.SetRelative().From() |
5 | 7 |
.From(true) |
7 | 2 |
시퀀스에 건 SetRelative 도 주석은 “Has no effect on Sequences” 라고 하지만, 시퀀스의 Startup 이 자식 전부의 isRelative 를 켜서 x = 2 에서 DOMoveX(5f, 1f) 하나를 넣은 시퀀스가 7 에서 끝났습니다.
주의할 점
1. 딜레이 동안 대상이 움직이면 그 자리에서 출발한다고 본다
안 지키면 생기는 문제. 딜레이가 있는 트윈의 시작값은 딜레이가 끝나는 첫 갱신에서 읽힙니다. 딜레이 동안 물리나 다른 트윈, 애니메이터가 대상을 옮기면 트윈은 옮겨진 자리에서 출발해 출발점이 튀거나 거리가 달라집니다.
이렇게 쓴다. 출발점이 정해져 있어야 하면 From(시작값, setImmediately: false) 로 시작값을 인자로 못 박습니다. Startup 이 시작값을 다시 읽지 않습니다.
2. From 은 대상이 살아 있는 것을 확인하고 붙인다
안 지키면 생기는 문제. .From() 의 즉시 경로에는 SafeMode 의 try 가 없어서, 대상이 파괴됐으면 예외가 호출한 코드로 올라와 그 메서드의 나머지가 실행되지 않습니다. 예외 종류도 에디터에서는 MissingReferenceException, 플레이어에서는 NullReferenceException 이라 에디터에서 본 종류만 catch 하면 빌드에서 빠져나갑니다.
이렇게 쓴다. From() 을 부르기 전에 if (target == null) return; 으로 거릅니다. 깜빡임이 문제되지 않으면 From(false, false) 로 미뤄 SafeMode 가 감싸는 Startup 경로를 탑니다.
3. From 에 상대값을 줄 때는 From(true) 를 쓴다
안 지키면 생기는 문제. .From().SetRelative() 는 조용히 무시되고, .SetRelative().From() 은 끝값에 시작값이 한 번 더 더해져 5 에서 2 가 아니라 5 에서 7 로 갑니다. 어느 쪽이든 경고가 없습니다.
이렇게 쓴다. “지금 위치에서 얼마만큼 떨어진 곳에서 출발” 은 From(true) 로만 씁니다.
4. 속도 기반 트윈의 길이는 첫 갱신 뒤에 읽는다
안 지키면 생기는 문제. SetSpeedBased() 트윈의 Duration() 은 Startup 전에는 초가 아니라 속도를 돌려줍니다. 그 값으로 다른 연출의 타이밍을 맞추면 어긋납니다. 이미 시작한 트윈에 길이 없이 ChangeEndValue(newEnd, true) 를 부르면 길이를 다시 계산하지 않아서, 속도 2 로 시작한 트윈이 초당 3.9 로 움직였습니다(같은 세 환경).
이렇게 쓴다. 길이가 필요하면 ForceInit() 을 먼저 불러 Startup 을 끝낸 뒤 Duration() 을 읽습니다. 속도 기반 트윈의 끝값을 바꿀 때는 ChangeEndValue(newEnd, 속도, true) 처럼 속도를 다시 넘깁니다.
정리
DOTween 은 시작값을 딜레이가 끝난 첫 갱신에서 읽고, 그때 상대 끝값, 변화량, 길이를 한 번에 정합니다.
- 딜레이 동안 대상이 옮겨지면 옮겨진 자리에서 출발합니다.
Goto같은 호출이 먼저 오면 그 순간에 읽습니다. From()은 붙이는 줄에서 getter 와 setter 를 부르고, 이 경로는 SafeMode 가 감싸지 않습니다.SetRelative와SetSpeedBased는 플래그이고 실제 계산은Startup에서 합니다. 순서를 바꾸거나 시작 뒤에 값을 바꾸면 결과가 어긋납니다.