08/12 AI Navigation - NavMesh 제작 및 사용하기

댓글 0
댓글을 작성하려면 로그인이 필요합니다.
아직 댓글이 없습니다. 첫 번째 댓글을 작성해보세요.

댓글을 작성하려면 로그인이 필요합니다.
아직 댓글이 없습니다. 첫 번째 댓글을 작성해보세요.
Unity의 Navigation AI와 NavMesh를 사용하면 복잡한 경로 탐색 로직을 직접 구현하지 않아도 AI 캐릭터가 목적지까지 자연스럽게 이동하도록 만들 수 있습니다. NavMesh는 캐릭터가 이동 가능한 영역과 장애물을 미리 계산해 두기 때문에 효율적인 경로 탐색이 가능하며, NavMeshAgent를 이용하면 이동 속도, 회전, 장애물 회피, 경로 추종 같은 기능도 손쉽게 제어할 수 있습니다.
핵심 용어
| 핵심 용어 | 의미 | 핵심 포인트 |
|---|---|---|
| NavMesh | AI 캐릭터가 이동 가능한 영역을 표현한 내비게이션 메시 데이터 | 바닥 전체가 아니라, AI가 실제로 걸을 수 있는 공간을 지도처럼 만든 것 |
| Bake | 씬의 지오메트리를 분석하여 NavMesh 데이터를 생성하는 과정 | 장애물, 경사, 계단 등을 계산해 이동 가능/불가능 영역을 결정 |
| NavMeshAgent | NavMesh 위에서 AI의 경로 탐색과 이동을 담당하는 컴포넌트 | 목적지를 주면 경로를 따라 이동·회전·속도 조절까지 처리 |
| Agent Radius | Agent의 몸통 반지름 / 폭을 나타내는 Bake 설정 | 좁은 통로를 통과할 수 있는지 결정. 값이 클수록 이동 가능 영역이 줄어듦 |
| Agent Height | Agent의 키를 나타내는 Bake 설정 | 천장이 낮은 곳을 지나갈 수 있는지 결정 |
| Max Slope | Agent가 올라갈 수 있는 최대 경사각 | 이 각도보다 가파른 경사는 이동 불가능 영역으로 처리 |
| Step Height | Agent가 한 번에 올라갈 수 있는 최대 턱 높이 | 계단이나 낮은 장애물을 걸어서 넘어갈 수 있는 기준 |
| SetDestination | NavMeshAgent에게 이동할 목적지 좌표를 지정하는 명령 | agent.SetDestination(targetPosition) 형태로 사용 |
| A (A-star)* | 목적지까지 효율적인 경로를 찾는 경로 탐색 알고리즘 | F = G + H를 기준으로 탐색 우선순위를 결정 |
| G Cost | 시작 지점에서 현재 노드까지 실제로 이동한 비용 | 지금까지 얼마나 이동했는지를 나타냄 |
| H Cost / 휴리스틱 | 현재 노드에서 목적지까지 남은 예상 비용 | 목적지에 가까운 방향을 우선 탐색하게 만드는 핵심 요소 |
| F Cost | 해당 노드의 전체 예상 비용 | F = G + H, 보통 F가 작은 노드를 먼저 탐색 |
| openList | 앞으로 탐색할 가능성이 있는 후보 노드 집합 | 아직 확정되지 않은 노드들이 들어 있음 |
| closedList | 탐색이 끝나 확정된 노드 집합 | 이미 처리한 노드를 반복해서 탐색하지 않도록 관리 |
NavMesh와 NavMeshAgent: 지도와 운전자

NavMesh를 굽기 위한 준비물 : 땅과 장애물
설치한 벽은 움직이지 않을 것이기 때문에 Static으로 만든다.
NavMesh Agent 크기와 이동 조건 설정


| 항목 | 의미 | 현재 설정 기준 |
|---|---|---|
| Name | Agent 타입 이름 | Humanoid |
| Radius | 캐릭터의 반지름, 즉 몸의 폭을 판단하는 기준 | 0.5 → 폭은 대략 1m로 계산 |
| Height | 캐릭터의 키 | 2 → 높이 2m 공간이 필요 |
| Step Height | 걸어서 올라갈 수 있는 최대 턱 높이 | 0.75 이하 턱은 이동 가능 |
| Max Slope | 올라갈 수 있는 최대 경사각 | 45°까지 이동 가능 |
| Drop Height | 자동 링크 생성 시 뛰어내릴 수 있는 최대 높이 | 0 → 자동 낙하 링크 사용 안 함 |
| Jump Distance | 자동 링크 생성 시 뛰어서 건널 수 있는 최대 거리 | 0 → 자동 점프 링크 사용 안 함 |
Agent Radius가 너무 크면 좁은 문을 통과하지 못하고 지도에서 그 통로 자체가 사라집니다. 너무 작으면 실제 모델은 벽에 닿아 보이는데 경로상으로는 지나갈 수 있다고 판단해 벽에 비비적거립니다. Agent Height가 낮으면 지나갈 수 없는 천장 아래를 갈 수 있다고 오해할 수 있고, Max Slope와 Step Height는 경사와 계단을 다룰 때 특히 중요합니다.
NavMesh Surface 설정 이해하기
| 항목 | 의미 |
|---|---|
| Agent Type | 이 NavMesh를 사용할 AI의 Agent 타입을 지정합니다. Radius, Height, Step Height 등의 설정이 적용됩니다. |
| Default Area | 생성되는 NavMesh 영역의 기본 Area 타입을 지정합니다. 일반적으로 Walkable을 사용합니다. |
| Generate Links | NavMesh 생성 시 점프나 단차 이동 등에 사용할 NavMesh Link를 자동 생성할지 설정합니다. |
| Use Geometry | NavMesh를 계산할 때 어떤 지오메트리 정보를 사용할지 결정합니다. Render Meshes는 화면에 보이는 메시를 기준으로 계산합니다. |
| Object Collection | NavMesh를 Bake할 때 어떤 오브젝트들을 수집해서 계산할지 설정합니다. |
| Advanced | NavMesh 생성과 관련된 세부 옵션을 설정합니다. |
| NavMesh Data | Bake를 통해 생성된 NavMesh 데이터를 저장하고 연결하는 항목입니다. |
| Clear | 현재 생성되어 있는 NavMesh 데이터를 삭제합니다. |
| Bake | 설정한 조건을 바탕으로 AI가 이동할 수 있는 NavMesh를 생성합니다. |

파란색으로 표시된 부분이 NavMesh가 구워진 부분, 에이전트가 이동할 수 있는 범위
경사면에서 에이전트 점프 시키는법
Navigation 탭에서 Drop Hegiht 설정해주고, Navmesh Suface에 generate links를 활성화해준다.
소제목: NavMeshAgent 이해하기
NavMeshAgent는 NavMesh 위에서 AI 캐릭터가 목적지까지 이동하도록 제어하는 컴포넌트입니다. 이동 속도, 회전 속도, 가속도, 정지 거리 등을 설정해 캐릭터의 움직임을 조절할 수 있습니다.

| 항목 | 의미 | 효과 |
|---|---|---|
Speed | 최고 이동 속도 | 값이 클수록 더 빠르게 이동합니다. |
Angular Speed | 회전 속도 | 값이 클수록 방향을 빠르게 바꾸고, 작으면 코너에서 천천히 크게 회전합니다. |
Acceleration | 가속도 | 값이 클수록 빠르게 최고 속도에 도달하고, 작으면 서서히 속도가 붙습니다. |
Stopping Distance | 정지 거리 | 목적지로부터 몇 m 앞에서 멈출지를 결정합니다. |
Radius | Agent의 반지름 | 다른 Agent나 장애물과 충돌을 피할 때 캐릭터의 폭 기준으로 사용됩니다. |
Height | Agent의 높이 | 캐릭터의 높이를 나타내며 장애물 회피 계산 등에 사용됩니다. |
Auto Braking | 자동 감속 여부 | 활성화하면 목적지에 가까워질수록 자동으로 속도를 줄입니다. |
Obstacle Avoidance | 장애물 회피 품질 | 다른 Agent나 장애물을 얼마나 정교하게 피할지 결정합니다. |
Priority | 회피 우선순위 | 여러 Agent가 만났을 때 서로 피하는 우선순위를 결정합니다. 값이 낮을수록 우선순위가 높습니다. |
경로 탐색의 원리: A*는 어떻게 길을 찾는가
먼저 두 알고리즘을 나란히 놓고 봅니다.
| 항목 | 다익스트라 | A* |
|---|---|---|
| 목적 | 시작점에서 모든 노드까지의 최단 거리 | 특정 목적지까지의 최단 거리 |
| 휴리스틱 사용 | 없음 (실제 거리만 계산) | 있음 (남은 거리를 추정) |
| 탐색 범위 | 넓음 (모든 방향으로 균등하게) | 좁음 (목표 쪽으로 집중) |
| 효율성 | 느릴 수 있음 | 휴리스틱이 정확하면 빠름 |
| 최적성 | 항상 최적 | 휴리스틱이 적절하면 최적 |
A*의 전부는 이 한 줄입니다.
F(x) = G(x) + H(x)
F : 예상 최종 거리 - 이 칸을 거쳐 가면 총 얼마나 걸릴 것 같은가
G : 지금까지 걸린 거리 - 시작점에서 이 칸까지 실제로 든 비용
H : 남은 예상 거리 - 이 칸에서 목적지까지 대략 얼마나 남았는가 (휴리스틱)
Node는 Map의 한 칸을 나타냄. 데이터로 위치/이동 가능 여부/G, H, F/부모(이전 노드)의 참조값을 가짐Node.walkable이 아니라 map 배열로 확인함F는 별도로 저장하지 않고 G + H로 계산함G를 0, H를 목적지까지의 예상 거리로 설정하고 openList에 넣으며 시작openList == 발견했지만 아직 주변 탐색을 완료하지 않은 후보 노드들openList가 빌 때까지 탐색을 반복openList에서 가장 좋은 노드를 찾음F가 가장 작은 노드F가 같다면 H가 가장 작은 노드current로 설정current를 openList에서 제거하고 closedList에 추가closedList == 주변 탐색 대상으로 선택되어 처리가 끝났거나 처리 중인 노드의 좌표들current가 목적지라면 parent를 따라 경로를 역추적하고 탐색을 종료current의 주변 8방향을 하나씩 탐색하여 다음 조건을 확인closedList에 없는가?newG를 계산newG = current.G + 이동 비용1014Node가 이미 만들어졌는지 확인Node를 가져옴Node를 만들고 Dictionary에 저장Node가 여러 개 생성되는 것을 방지openList에 있는지 확인openList에 없거나, newG가 기존 이웃 노드의 G보다 작다면 이웃 노드의 정보를 업데이트G = newGH = 목적지까지의 예상 거리parent = currentopenList에 없었던 경우, 앞으로 탐색할 수 있도록 openList에 추가openList에서 가장 좋은 노드를 선택하면서 탐색을 반복openList가 비었다면 목적지까지 갈 수 있는 경로가 없으므로 null을 반환특히 기존 설명에서 보완할 부분은 세 가지야.
current는 목적지인지 검사하기 전에 먼저 openList에서 빠지고 closedList에 들어감openList가 비어 경로가 없다고 판단하는 경우, 두 가지임// 경로를 탐색하는 함수
public List<Vector2Int> FindPath(Vector2Int start, Vector2Int goal)
{
// [준비]
// openList: 앞으로 탐색할 후보 노드들
// closedList: 이미 확정된 노드들(다시 안 봄)
List<Node> openList = new List<Node>();
List<Vector2Int> closedList = new List<Vector2Int>();
// 같은 좌표의 Node를 매번 만들지 않도록 좌표를 통해 노드를 찾을 수 있도록 해준다.
Dictionary<Vector2Int, Node> nodes = new Dictionary<Vector2Int, Node>();
// [시작]
// 시작 노드의 G = 0, H = 휴리스틱 계산
Node startNode = new Node(start, true)
{
G = 0,
H = GetDistance(start, goal)
};
// 시작 노드를 openList에 추가
openList.Add(startNode);
nodes[start] = startNode;
// [반복] openList가 비어있지 않은 동안
while (openList.Count > 0)
{
// ① openList에서 F가 가장 낮은 노드를 고른다
// (F가 같으면 H가 낮은 쪽)
Node current = openList[0];
// F와 H를 비교해서 가장 낮은 것을 찾아준다.
for (int i = 1; i < openList.Count; i++)
{
bool isBetterF = openList[i].F < current.F;
bool isBetterH = openList[i].F == current.F && openList[i].H < current.H;
if (isBetterF || isBetterH)
{
current = openList[i];
}
}
// ② 그 노드를 openList에서 빼고 closedList에 넣는다
openList.Remove(current);
closedList.Add(current.position);
// ③ 그 노드가 목적지면 → 경로를 역추적하고 종료
if (current.position == goal)
{
return RetracePath(startNode, current);
}
// ④ 그 노드의 이웃 칸들을 하나씩 확인한다
foreach (Vector2Int dir in Directions)
{
Vector2Int neighborPos = current.position + dir;
// 격자 밖이거나
if (!IsInsideGrid(neighborPos))
{
continue;
}
// 벽이면 건너뛴다
if (!map[neighborPos.x, neighborPos.y])
{
continue;
}
// 이미 closedList에 있으면 건너뛴다 == 이미 확정된 칸이면 건너뛴다.
if (closedList.Contains(neighborPos))
{
continue;
}
// 대각선이면 인접한 수직·수평이 모두 열려있는지 확인
if (IsDiagonal(dir) && !CanMoveDiagonally(current.position, dir))
{
continue;
}
// 새 G = 현재 노드의 G +이동 비용(직선 10 / 대각선 14)
int moveCost = IsDiagonal(dir) ? DIAGONAL_COST : STRAIGHT_COST;
int newG = current.G + moveCost;
// 현재 위치에 만들어진 노드가 존재하지 않는 경우
// openList에 중복으로 들어가게 되는 현상을 막기위해
if (!nodes.TryGetValue(neighborPos, out Node neighborNode))
{
// 새로 노드를 만들어 준다.
neighborNode = new Node(neighborPos, true);
nodes[neighborPos] = neighborNode;
}
bool isOpenListExist = openList.Contains(neighborNode);
// 새 G가 기존에 기록된 G보다 작거나, openList에 없다면
if (newG < neighborNode.G || !isOpenListExist)
{
// G, H를 갱신하고
neighborNode.G = newG;
neighborNode.H = GetDistance(neighborPos, goal);
// 부모(어디서 왔는지)를 현재 노드로 기록하고
neighborNode.parent = current;
// openList에 없으면 openList 추가까지 진행.
if (!isOpenListExist)
{
openList.Add(neighborNode);
}
}
}
}
// [종료]
// openList가 비었는데 목적지에 도달하지 못했다면 → 경로 없음
return null;
}

우리들의 게임 발매 이야기

안녕하세요. 플밍 4기 입니다. 게임 개발을 배우기 전 네트워크 엔지니어 도메인에서 익히고 배웠던 네트워크 이론에 대한 기초 입니다. 학습에 도움이 되길 바라며 공유 드립니다.
XR을 활용한 게임 개발 3기(유니티) 수강생입니다. 곧 수료 하지만 앞으로 이곳에 가끔 저의 개발 경험이 나 지식 기록할까 합니다. 더 나아가 이 사이트가 제 개인위키의 역할을 할 수 있으면 좋겠습니다. 한국 게임 시장을 흔들겠습니다


안녕하세요. 플밍 4기 입니다. 게임 개발을 배우기 전 네트워크 엔지니어 도메인에서 익히고 배웠던 네트워크 이론에 대한 기초 입니다. 학습에 도움이 되길 바라며 공유 드립니다.
XR을 활용한 게임 개발 3기(유니티) 수강생입니다. 곧 수료 하지만 앞으로 이곳에 가끔 저의 개발 경험이 나 지식 기록할까 합니다. 더 나아가 이 사이트가 제 개인위키의 역할을 할 수 있으면 좋겠습니다. 한국 게임 시장을 흔들겠습니다
