---
title: "DOTween 의 경로 트윈은 시작할 때 곡선을 완성하고 길이 표로 등속을 맞춘다"
description: "DOTween 의 DOPath 는 첫 갱신에서 현재 위치를 앞에 끼워 곡선을 완성하고, 곡선 위를 같은 빠르기로 가도록 길이 표를 만듭니다. resolution 은 그 표의 촘촘함이고, Pro 의 DOTweenPath 는 이 표를 에디터에서 미리 만들어 둡니다."
author: "유니얼"
pubDate: 2026-09-25
updatedDate: 2026-10-18
category: "game-dev/unity/libraries"
tags: ["Unity", "라이브러리", "트윈"]
canonical: https://blog.unialgames.com/posts/unity-dotween-path/
---
```csharp
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` 에서 **대상의 현재 위치를 읽어** 완성됩니다. 첫 점이 현재 위치와 다르면 현재 위치를 맨 앞에 끼웁니다.

```csharp
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 인 네 칸짜리 표로 봅니다.

```csharp
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 를 찾으면 이렇습니다.

1. 전체 길이 10 의 40%, 곧 거리 4 를 찾습니다.
2. 누적 길이가 처음으로 4 를 넘는 칸은 둘째 칸(6)입니다. 그 앞 칸은 (u 0.25, 길이 2), 이 칸은 (u 0.5, 길이 6) 입니다.
3. 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` 를 보고 곧바로 돌아가 런타임에 길이 표를 만들지 않습니다.

<x-compare>
<script type="application/json">
{
  "label": "코드의 DOPath 와 DOTween Pro 의 DOTweenPath",
  "tabs": [
    {
      "label": "DOPath (코드)",
      "code": "transform.DOPath(points, 2f, PathType.CatmullRom)\n    .SetLookAt(0.01f)\n    .SetEase(Ease.Linear);",
      "stats": [
        [
          "경로 완성",
          "첫 갱신 프레임, 현재 위치를 앞에 끼움"
        ],
        [
          "길이 표",
          "런타임에 점 수 × resolution 칸 계산"
        ],
        [
          "SetTarget",
          "Transform"
        ],
        [
          "다시 쓰기",
          "코드로 다시 만든다"
        ]
      ]
    },
    {
      "label": "DOTweenPath (Pro)",
      "text": "씬 뷰에서 점을 찍고 인스펙터에서 시간, ease, LookAt 을 고른다. Awake 에서 트윈 하나를 만든다.",
      "stats": [
        [
          "경로 완성",
          "에디터에서 미리, 직렬화"
        ],
        [
          "길이 표",
          "런타임 계산 없음"
        ],
        [
          "SetTarget",
          "GameObject"
        ],
        [
          "다시 쓰기",
          "Awake 한 번뿐, AutoKill 을 끄고 DORestart"
        ]
      ]
    }
  ]
}
</script>
</x-compare>

`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` 도 함께 지워집니다.