---
title: "내비메시는 바닥이 아니라 설 수 있는 자리다"
description: "유니티가 내비메시를 구울 때 실제로 하는 일과, 런타임에 NavMeshAgent 가 하는 일입니다. 보켈로 덮고 반지름만큼 물러나는 과정, 폴리곤 통로와 코너, 카빙과 링크가 치르는 값."
author: "유니얼"
pubDate: 2026-09-17
updatedDate: 2026-09-18
category: "programming/algorithm/graph"
tags: ["Unity", "격자"]
canonical: https://blog.unialgames.com/posts/algo-navmesh-internals/
---
씬에서 내비메시를 구워 놓고 보면 **바닥 메시와 모양이 다릅니다.** 벽에서 한 뼘씩 떨어져 있고, 모서리가 조금씩 뭉툭합니다.
기둥 주위는 실제보다 넓게 파여 있고, 좁은 복도는 아예 없어지기도 합니다.

버그가 아닙니다. 내비메시는 **바닥을 옮겨 그린 것이 아니라, 에이전트가 설 수 있는 자리를 계산해 둔 것**입니다.
유니티 문서는 에이전트를 원기둥으로 보고, 그 원기둥이 설 수 있는 자리를 시험해서 걸을 수 있는 영역을 만든다고 적어 두고 있습니다.

## 굽는 것은 세 단계다

유니티 매뉴얼이 적어 둔 순서는 이렇습니다. **장면을 보켈로 래스터화하고, 걸을 수 있는 표면을 뽑아내고, 그것을 내비메시로 바꿉니다.**


<x-bake>
<script type="application/json">
{
  "label": "구워지는 과정",
  "w": 30,
  "h": 18,
  "radius": 2,
  "walls": [[6.4, 3.6, 5.2, 5.4], [17.2, 9.4, 7.3, 4.2], [3.0, 12.3, 5.6, 3.4], [21.6, 2.5, 4.2, 4.4]],
  "note": "회색이 장면에 놓인 도형이고 격자가 보켈입니다. 에이전트 반지름을 보켈 두 칸으로 잡았습니다"
}
</script>
</x-bake>

보켈 540개로 시작해서 폴리곤 다섯 개가 남았습니다.
도형에 안 걸린 칸이 426개였는데, **반지름만큼 물러나고 나니 77개**입니다. 여기가 핵심입니다.

단계마다 무엇이 정해지는지 보면 이렇습니다.

| 단계 | 하는 일 | 여기서 정해지는 것 |
| --- | --- | --- |
| 보켈로 덮기 | 장면 도형을 일정한 크기의 칸으로 바꿉니다 | 내비메시가 도형을 얼마나 정확히 따라가는가 |
| 설 수 있는 칸 고르기 | 경사, 높이, 단차를 보고 못 서는 칸을 버립니다 | 에이전트가 오를 수 있는 곳 |
| 반지름만큼 물러나기 | 벽에서 반지름만큼 안쪽으로 깎습니다 | 내비메시 경계가 벽이 아니라 **에이전트 가운데의 한계**가 됩니다 |
| 덩어리로 나누고 묶기 | 이어진 칸을 볼록한 조각으로 만듭니다 | 런타임에 훑을 폴리곤 |

**내비메시 위의 한 점은 「거기 발을 디딜 수 있다」 가 아니라 「거기에 에이전트 가운데를 놓아도 된다」 는 뜻입니다.**
벽에서 떨어져 보이는 것이 그래서입니다. 반지름을 0.5로 잡았다면 딱 0.5만큼 떨어져 있습니다.

## 보켈 크기가 정확도와 시간을 같이 정한다

보켈이 작을수록 도형을 정확히 따라가고, 굽는 값이 커집니다.

유니티의 기본값은 **에이전트 반지름 하나에 보켈 세 개**입니다. 에이전트 너비로 치면 여섯 개입니다.
그리고 문서는 **보켈을 절반으로 줄이면 메모리와 빌드 시간이 네 배**가 된다고 못 박아 뒀습니다. 2D 로 자르는 값이라 제곱으로 듭니다.

| 보켈 크기 | 결과 |
| --- | --- |
| 기본 (반지름당 3) | 대부분의 맵에서 쓰는 값 |
| 더 잘게 | 좁은 통로가 살아납니다. 굽는 시간과 메모리가 제곱으로 늡니다 |
| 더 굵게 | 빨리 구워지지만 모서리가 뭉툭해지고 얇은 구조가 사라집니다 |

**에이전트 반지름을 줄이면 보켈도 같이 작아집니다.** 기본값이 반지름에 묶여 있어서입니다.
작은 적을 하나 추가했더니 굽는 시간이 몇 배가 되는 일이 여기서 생깁니다. 반지름만 바꿀 생각이었다면 보켈 크기를 먼저 고정해 두세요.

## 에이전트 하나당 내비메시 하나

반지름이 구워질 때 들어가므로, **반지름이 다른 에이전트는 같은 내비메시를 쓸 수 없습니다.**
높이와 오를 수 있는 경사, 넘을 수 있는 단차도 마찬가지입니다. 이 넷이 에이전트 타입 하나를 이룹니다.

| 설정 | 무엇을 정하나 |
| --- | --- |
| 반지름 | 벽에서 얼마나 물러날지 |
| 높이 | 어느 높이의 천장 아래를 지날 수 있는지 |
| 최대 경사 | 어느 각도까지 오를 수 있는지 |
| 단차 | 계단 한 칸을 얼마까지 그냥 넘을지 |

**사람과 거대 보스가 같이 다니는 게임이면 내비메시를 두 벌 굽습니다.** 메모리도 두 배, 굽는 시간도 두 배입니다.
반대로 **반지름을 억지로 맞춰 한 벌로 끝내는 것**도 흔한 선택입니다. 큰 쪽에 맞추면 작은 유닛이 좁은 길을 못 쓰고,
작은 쪽에 맞추면 큰 유닛이 벽에 낍니다. 어느 쪽이 덜 눈에 띄는지로 정하는 문제입니다.

## 좁은 통로가 사라지는 이유

구운 결과에 있어야 할 복도가 없는 경우가 있습니다. 원인이 대개 둘 중 하나입니다.

**반지름만큼 물러나고 나니 남는 게 없는 경우.** 폭이 반지름의 두 배보다 좁으면 통로가 통째로 사라집니다.
여기에 보켈 격자가 어긋나는 몫이 더 붙습니다. 유니티 문서는 **반지름에 더해 보켈 크기의 네 배 정도 여유**를 두라고 권합니다.
그것도 안 되면 그 자리는 링크로 넘기라고 합니다.

**작은 조각이라 잘려 나간 경우.** 굽기에는 일정 넓이보다 작고 동떨어진 조각을 버리는 설정이 있습니다.
공중에 뜬 작은 발판이나 도달할 수 없는 구석을 치우는 용도입니다.
문서가 적어 둔 예외가 하나 있는데, **그 조각이 타일 경계에 걸쳐 있으면 안 잘립니다.**
타일을 나눠서 병렬로 굽기 때문에, 자를 때 옆 타일을 아직 모릅니다. 같은 크기의 조각이 어떤 데서는 남고 어떤 데서는 사라지는 이유입니다.

## 런타임에는 폴리곤을 훑는다

구워 두고 나면 런타임 길찾기는 칸이 아니라 폴리곤 위에서 돕니다.

1. 시작과 목표를 **가장 가까운 폴리곤**으로 옮깁니다. 내비메시 위가 아닌 지점을 넘기면 이 단계에서 어긋납니다
2. 이웃 폴리곤을 훑어 목표 폴리곤까지 갑니다. 그렇게 나온 **폴리곤의 줄이 통로**입니다
3. 에이전트는 통로 안에서 **다음으로 보이는 코너**를 향해 갑니다

```csharp
var path = new NavMeshPath();
if (NavMesh.CalculatePath(transform.position, target, NavMesh.AllAreas, path)) {
    // corners 는 이미 뽑아낸 꺾이는 점입니다. 폴리곤 통로가 아닙니다
    for (int i = 1; i < path.corners.Length; i++)
        Debug.DrawLine(path.corners[i - 1], path.corners[i], Color.red);
}
```

`path.status` 를 같이 봐야 합니다. 목표에 못 닿으면 **가장 가까운 데까지의 부분 경로**가 담겨 오고,
`CalculatePath` 자체는 참을 돌려줍니다. 이걸 안 보면 유닛이 벽 앞에서 멈춘 채 도착했다고 여깁니다.

내비메시 위가 아닌 좌표를 목표로 넘길 때는 먼저 붙여 두는 편이 안전합니다.

```csharp
if (NavMesh.SamplePosition(clickPoint, out var hit, 2f, NavMesh.AllAreas))
    agent.SetDestination(hit.position);
```

## 회피는 길찾기가 아니다

에이전트가 실제로 움직이는 방향은 경로가 그대로 정하지 않습니다.
유니티 문서의 표현대로 **회피 단계가 「가고 싶은 방향」 과 「부딪히지 않는 것」 사이에서 새 속도를 고릅니다.**
다른 에이전트뿐 아니라 내비메시의 가장자리도 그 계산에 들어갑니다.

그래서 경로는 멀쩡한데 유닛이 이상하게 움직이는 일이 생깁니다. 이때 볼 것은 경로가 아니라 회피 설정입니다.

- **회피 품질을 낮추면** 계산이 싸지는 대신 서로 겹쳐 지나갑니다. 유닛이 수백이면 대개 이쪽을 씁니다
- **우선순위**로 누가 비켜 줄지 정합니다. 같은 값이면 서로 비키다가 둘 다 멈추는 모양이 나옵니다
- **멈춰 있는 유닛은 장애물로 바꿔 두는 편**이 낫습니다. 회피 대상으로 두면 계속 계산에 들어갑니다

## 런타임에 지형이 바뀌면

문이 닫히거나 건물이 세워지면 내비메시를 고쳐야 합니다. 두 가지 길이 있고 값이 다릅니다.

**카빙.** 장애물 컴포넌트가 내비메시에 닿는 부분을 찾아 **구멍을 냅니다.**
문서가 대놓고 비싼 연산이라고 적어 둔 부분입니다. 움직이는 물체마다 켜 두면 프레임이 그대로 무너집니다.
움직이는 동안은 끄고 멈춘 뒤에만 켜는 것이 기본입니다.

**다시 굽기.** 표면 컴포넌트를 런타임에 다시 구울 수 있습니다. 구역을 나눠 놓고 바뀐 구역만 다시 굽는 방식이 흔합니다.
한 번에 다 구우면 그 프레임이 통째로 날아갑니다.

**장애물이 잠깐 지나가는 것뿐이라면 둘 다 안 하는 것이 맞습니다.** 회피에 맡기면 됩니다.
내비메시를 고치는 것은 **길 자체가 막히거나 열릴 때**입니다.

## 이어지지 않은 곳은 링크로

점프, 사다리, 난간 넘기처럼 표면이 이어지지 않은 이동은 구워지지 않습니다.
그런 자리는 **링크 컴포넌트로 「여기로 건너갈 수 있다」 를 직접 알려 줍니다.** 길찾기는 그 링크를 간선 하나로 취급합니다.

새 프로젝트에서는 `NavMeshLink` 를 씁니다. 예전의 `OffMeshLink` 는 더 이상 에디터에서 추가할 수 없습니다.
쓰던 프로젝트는 그대로 돌아가지만, 새로 놓을 수는 없습니다.

링크를 지날 때의 움직임은 **직접 만들어야 합니다.** 기본 동작은 그냥 순간이동에 가깝습니다.
`agent.isOnOffMeshLink` 를 보고 그 구간만 코루틴으로 포물선을 그리는 식이 흔한 처리입니다.

## 높이는 따로 있다

내비메시는 근사입니다. 계단을 구우면 **경사로처럼 뭉개져서** 나오고, 에이전트가 계단 위를 미끄러지듯 지나갑니다.

그래서 **높이 메시**를 따로 굽는 설정이 있습니다. 길찾기는 여전히 뭉갠 폴리곤으로 하고,
에이전트를 그 자리에 놓을 때만 정확한 높이를 봅니다. 문서가 적어 둔 대로 메모리와 굽는 시간을 더 씁니다.

계단이 있는 맵에서 유닛이 공중에 뜨거나 파묻힌다면 여기부터 보면 됩니다.

## 정리

내비메시는 바닥을 옮겨 그린 것이 아니라 **에이전트가 설 수 있는 자리를 미리 계산해 둔 결과**입니다.

- 굽기는 **보켈로 덮고, 설 수 있는 칸을 고르고, 반지름만큼 물러나고, 볼록한 조각으로 묶는** 과정입니다
- 내비메시 경계는 벽이 아니라 **에이전트 가운데의 한계**입니다. 벽에서 떨어져 보이는 것이 정상입니다
- 보켈은 기본이 **반지름당 세 개**이고, 절반으로 줄이면 메모리와 시간이 네 배입니다
- **반지름·높이·경사·단차가 구워지므로 에이전트 타입마다 내비메시가 한 벌씩** 필요합니다
- 런타임은 폴리곤 통로를 찾고 그 안에서 코너를 뽑아 따라갑니다. **부분 경로를 성공으로 오해하지 마세요**
- 유닛이 이상하게 움직이면 경로가 아니라 **회피 설정**을 보세요
- 카빙은 비쌉니다. 길이 진짜로 막히고 열릴 때만 씁니다