DOTween 의 경로 트윈은 시작할 때 곡선을 완성하고 길이 표로 등속을 맞춘다
Vector3[] points = { new Vector3(2, 0, 0), new Vector3(2, 0, 2), new Vector3(0, 0, 2) };
transform.DOPath(points, 2f, PathType.CatmullRom);
곡선 위를 Update 에서 손으로 따라가면, 매개변수를 고르게 늘려도 곡선이 휜 정도에 따라 빨라졌다 느려졌다 합니다. 이 한 줄은 곡선이 언제 계산되는지, 같은 빠르기는 어떻게 보장되는지, 기본값 10 인 resolution 이 무엇의 개수인지를 감춥니다.
DOTween 의 경로 트윈은 첫 갱신에서 현재 위치를 넣어 곡선을 완성하고, 곡선 위를 같은 빠르기로 가도록 길이 표를 한 번 만들어 둡니다. 기준은 DOTween 1.3.030(GitHub 커밋 4846808)과 DOTween Pro 1.0.430 이고, DOTweenPath 는 설치된 DLL 을 디컴파일한 코드 기준입니다.
곡선은 트윈이 시작할 때 완성된다
DOPath 는 DOTween.To 에 PathPlugin 과 Path 객체를 끼운 숏컷입니다. 기본 곡선 종류는 PathType.Linear 이고, new Path(...) 는 점 배열을 복사해 둘 뿐 곡선을 계산하지 않습니다. 곡선은 첫 갱신(딜레이가 끝난 뒤)의 PathPlugin.SetChangeValue 에서 대상의 현재 위치를 읽어 완성됩니다. 첫 점이 현재 위치와 다르면 현재 위치를 맨 앞에 끼웁니다.
Vector3 currVal = t.getter(); // 대상의 지금 위치. 이 순간이 처음이자 마지막이다
// 첫 점이 지금 자리와 다르면 맨 앞에 지금 자리를 끼운다
bool addStart = wps.Length <= minInputWaypoints || wps[0] != currVal;
// 닫힌 경로면 끝에 출발점을 붙인다
bool addEnd = isClosedPath && wps[wps.Length - 1] != currVal;
var newWps = new Vector3[wps.Length + (addStart ? 1 : 0) + (addEnd ? 1 : 0)]; // 새 배열
if (addStart)
newWps[0] = currVal;
for (int i = 0; i < wps.Length; ++i)
newWps[i + (addStart ? 1 : 0)] = wps[i]; // 넘긴 점들은 한 칸씩 뒤로
if (addEnd)
newWps[newWps.Length - 1] = newWps[0];
path.wps = newWps;
path.FinalizePath(isClosedPath, lockPositionAxis, currVal); // 제어점과 길이 표를 여기서 한 번 계산
| 대상이 서 있는 곳 | DOPath(A, B) 의 최종 점 |
|---|---|
| P (첫 점이 아닌 곳) | [P, A, B]. 지금 자리에서 출발 |
| A (첫 점과 같은 곳) | [A, B]. 넘긴 두 점 그대로 |
그래서 맨 위 코드는 세 점만 지나는 것이 아니라 “지금 자리 → 첫 점 → 둘째 점 → 셋째 점” 으로 갑니다. 이 프레임에 새 배열과 표가 생기는 것이 할당으로 보입니다. CatmullRom 경로(입력 점 5개, resolution 10) 100개를 만들어 한 개당으로 나눈 값입니다.
| 프레임 | DOPath (Mono / IL2CPP) |
DOMove |
|---|---|---|
| 만든 프레임 | 1,117 B / 1,138 B | 659 B |
| 다음 프레임 (첫 갱신) | 784 B / 784 B | 0 B |
784 B 는 새 웨이포인트 배열, 제어점, 구간 길이, 길이 표 둘 다섯 배열을 소스로 셈한 크기와 딱 맞습니다. From() 은 경로 트윈에서 아무 일도 하지 않습니다(SetFrom 본문이 비어 있습니다). 거꾸로 가려면 점 배열을 뒤집어 넘깁니다.
resolution 은 곡선 모양이 아니라 등속 보정의 정밀도다
캣멀롬이든 베지어든 매개변수 u 를 고르게 늘려도 곡선 위의 거리는 고르게 늘지 않습니다. DOTween 은 u 를 점 수 × resolution 칸으로 나눠 누적 길이를 표로 만들어 두고, 매 프레임 “전체 길이의 몇 % 지점은 u 가 얼마인가” 를 표에서 거꾸로 찾습니다. 누적 길이가 2, 6, 8, 10 인 네 칸짜리 표로 봅니다.
if (type == PathType.Linear)
return perc; // 직선은 진행률이 곧 거리 비율이다
float tLen = length * perc; // 찾을 거리
// 누적 길이가 처음으로 tLen 을 넘는 칸을 찾는다
for (int i = 0; i < lengthsTable.Length; ++i)
{
if (lengthsTable[i] > tLen)
{
t1 = timesTable[i];
l1 = lengthsTable[i];
if (i > 0) l0 = lengthsTable[i - 1];
break;
}
t0 = timesTable[i];
}
// 두 칸 사이를 직선으로 나눠 u 를 얻는다
perc = t0 + (tLen - l0) / (l1 - l0) * (t1 - t0);
times = [0.25, 0.5, 0.75, 1], 누적 길이 lengths = [2, 6, 8, 10] 인 곡선에서 진행률 0.4 를 찾으면 이렇습니다.
- 전체 길이 10 의 40%, 곧 거리 4 를 찾습니다.
- 누적 길이가 처음으로 4 를 넘는 칸은 둘째 칸(6)입니다. 그 앞 칸은 (u 0.25, 길이 2), 이 칸은 (u 0.5, 길이 6) 입니다.
- 4 는 2 와 6 의 딱 가운데라, u 도 0.25 와 0.5 의 가운데인 0.375 입니다.
진행률은 0.4 인데 곡선의 매개변수 u 는 0.375 를 씁니다. 이렇게 바꿔야 곡선 위를 고른 빠르기로 갑니다.
곡선 위의 점은 매 프레임 식으로 정확히 계산하므로 resolution 을 1 로 내려도 모양은 그대로이고, 빠르기가 고른 정도만 달라집니다. Linear 는 표 없이 누적 거리 비율만 쓰고 resolution 을 쓰지 않습니다. 매 프레임 비용입니다(서로 다른 Transform 1,000개, 길이 600초).
| 트윈 하나의 매 프레임 비용 | Mono | IL2CPP |
|---|---|---|
DOMove |
190 ns | 102 ns |
CatmullRom, 점 10개, resolution 10 |
507 ns | 191 ns |
같은 경로, resolution 5 |
513 ns | 192 ns |
| Linear, 점 10개 | 307 ns | 102 ns |
진행률이 0.1% 에도 못 미친 구간이라 표 탐색은 첫 칸에서 끝났고, 곡선 경로가 더 든 몫은 곡선 식 계산입니다. 표 탐색은 처음부터 훑는 선형 탐색이라 진행률이 끝에 가까울수록 비교가 늘어납니다.
DOTween Pro 는 코어를 고치지 않고 위에 얹는다
Pro 는 코어 DLL 을 바꾸지 않고 공개 API 와, 코어가 InternalsVisibleTo("DOTweenPro") 로 열어 둔 internal 멤버를 씁니다. DOTweenPath 는 경로를 에디터에서 미리 완성해 제어점과 길이 표까지 직렬화하고, Awake 에서 그 Path 를 코어의 DOPath(Path, ...) 에 넘깁니다. 첫 갱신은 isFinalized 를 보고 곧바로 돌아가 런타임에 길이 표를 만들지 않습니다.
DOTweenAnimation 은 코어 숏컷으로 트윈을 만든 뒤 대상을 GameObject 로 덮어씁니다. 그래서 DOPause() 같은 제어 메서드가 그 GameObject 를 대상으로 가진 트윈 전부에 작용합니다. 재 보니 DOTween.Kill(transform) 은 0, DOTween.Kill(gameObject) 는 1 을 돌려줬습니다. SetLookAt 의 회전도 코어가 직접 쓰지 않고 Physics 모듈이 이벤트로 받아 적용하므로, 모듈 초기화가 실패하면(“Couldn’t load Modules system”) 경로 이동은 되는데 회전만 빠집니다.
트윈이 죽으면 Path 도 지워진다
트윈이 파기되면 PathPlugin.Reset 이 목표값 Path 를 Destroy() 해서 점과 길이 표를 null 로 지웁니다. Vector3[] 를 넘긴 DOPath 는 Path 를 스스로 만들었으니 상관없지만, DOPath(Path, ...) 로 밖에서 만든 Path 를 넘긴 경우와 DOTweenPath 는 이 수명을 따라갑니다. DOTweenPath 를 0.2초로 재생하고 1초 뒤에 경로를 읽었습니다(같은 세 환경).
| AutoKill | GetDrawPoints() 와 path.wps |
이어서 DORestart(true) |
|---|---|---|
| 켬 | 둘 다 null |
- |
| 끔 | 둘 다 Vector3[4] |
다시 재생되며 출발점 쪽으로 돌아감 |
주의할 점
1. Linear 경로에 1 을 넘는 이징을 걸지 않는다
안 지키면 생기는 문제. 곡선 경로는 진행률을 표로 바꾸는 함수 끝에서 0~1 로 자르지만, Linear 는 첫 줄에서 돌아가 자르지 않습니다. OutBack 처럼 1 을 넘는 값이 들어가면 첫 점을 돌려줘, 오버슈트 구간 동안 대상이 시작점으로 순간이동합니다. (0, 0, 0) 에서 DOPath(new[]{ (5,0,0), (5,0,5) }, 1f, PathType.Linear).SetEase(Ease.OutBack) 는 진행 40% 부터 95% 까지 (0, 0, 0) 에 있었고, 같은 경로의 CatmullRom 은 끝점에 멈춰 있었습니다.
이렇게 쓴다. Linear 경로에는 1 을 넘지 않는 이징만 씁니다. 튕기는 느낌이 필요하면 CatmullRom 으로 바꾸거나, 도착한 뒤에 짧은 DOPunchPosition 을 잇습니다.
2. resolution 을 곡선 품질 조절로 쓰지 않는다
안 지키면 생기는 문제. 경로가 각져 보인다고 resolution 을 올려도 모양은 달라지지 않고, 시작 프레임의 표 계산과 매 프레임 비교 횟수만 늘어납니다. 각져 보이는 원인은 대개 기본값 Linear 입니다.
이렇게 쓴다. 곡선이 필요하면 PathType.CatmullRom 을 명시합니다. resolution 은 빠르기가 구간마다 눈에 띄게 달라질 때만 올리고, 점 사이가 완만하면 소스 주석대로 5 로 내려도 됩니다.
3. GotoWaypoint 는 Linear 이징에서만 그 점에 정확히 선다
안 지키면 생기는 문제. GotoWaypoint(i) 는 그 점까지의 길이 비율을 시간 비율로 써서 Goto 합니다. 이징이 Linear 가 아니면 둘이 달라 점을 지나치거나 못 미칩니다. 2초짜리 CatmullRom 경로에서 Ease.Linear 는 목표 점에서 0.0015, Ease.OutQuad 는 2.61 떨어진 자리에 섰습니다. 인덱스도 첫 갱신이 현재 위치를 끼운 배열 기준이라, 한 칸이 끼워졌다면 넘긴 배열의 i - 1 번째 점입니다.
이렇게 쓴다. 웨이포인트로 건너뛸 경로는 SetEase(Ease.Linear) 로 만들고, 인덱스는 끼워 넣은 현재 위치를 0 번으로 세어 넘깁니다.
4. 밖에서 만든 Path 를 두 번 쓰지 않는다
안 지키면 생기는 문제. DOPath(Path, ...) 에 넘긴 Path 는 트윈이 파기될 때 점과 길이 표를 잃습니다. 같은 Path 를 다음 트윈에 넘기면 비어 있는 경로를 받습니다. DOTweenPath 도 AutoKill 이 켜져 있으면 한 번 재생한 뒤 경로가 null 이 됩니다.
이렇게 쓴다. 코드에서는 Vector3[] 를 받는 오버로드를 씁니다. 배열은 복사되므로 몇 번 써도 안전합니다. DOTweenPath 를 다시 쓰려면 AutoKill 을 끄고 DORestart 로 되돌립니다.
정리
DOTween 의 경로 트윈은 첫 갱신에서 곡선을 완성하고 길이 표로 등속을 맞춥니다.
- 첫 점이 현재 위치와 다르면 현재 위치를 앞에 끼우고, 그 프레임에 표를 만드느라 트윈 하나에 784 B 를 더 할당합니다.
resolution은 점당 길이 샘플 수로, 곡선 모양이 아니라 빠르기가 고른 정도를 정합니다.Linear는 표가 없고 진행률을 자르지 않습니다.- Pro 의
DOTweenPath는 표를 에디터에서 미리 만들어 넘기고, 트윈이 파기되면 넘긴Path도 함께 지워집니다.