1. AI가 '걸어다닐 수 있는 공간'을 어떻게 인식하는가
게임에서 AI 캐릭터가 장애물을 피해 목표 지점까지 이동하는 기능은, 구현하기 어려운 시스템 중 하나입니다. AI는 사람처럼 눈으로 지형을 파악하는 것이 아니라, 수학적으로 정의된 데이터 구조를 통해 "이 공간을 걸어갈 수 있는가"를 판단합니다.
직관적으로 생각해보면, 레벨에는 수만 개의 삼각형 폴리곤으로 이루어진 렌더 메시가 존재합니다. 그 모든 면을 AI가 직접 검사하면서 경로를 계산하는 것은 현실적으로 불가능합니다. 이 문제를 해결하기 위해 게임 업계에서 표준으로 자리잡은 방식이 Navigation Mesh(NavMesh) 입니다.
NavMesh는 레벨 지오메트리를 분석하여 "AI가 걸을 수 있는 표면"을 단순화된 볼록 다각형(Convex Polygon) 집합으로 추상화한 자료구조입니다. AI는 복잡한 렌더 메시 대신, 이 단순화된 폴리곤 그래프 위에서 경로를 탐색합니다.
언리얼 엔진 5는 이 NavMesh를 생성하고 탐색하는 핵심 기능을 외부 오픈소스 라이브러리인 Recast & Detour에 위임합니다. Recast가 NavMesh를 "만드는" 역할을 담당하고, Detour가 NavMesh 위에서 "경로를 찾는" 역할을 담당합니다. 이 두 라이브러리의 동작 원리를 이해하면, 런타임 생성 전략과 경로 탐색 알고리즘 최적화 모두를 근거 있게 결정할 수 있습니다.
2. Recast: NavMesh가 만들어지는 과정
Recast는 Mikko Mononen이 제작한 오픈소스 NavMesh 생성 라이브러리로, Unity, Unreal Engine, Godot, O3DE 등 주요 게임 엔진이 공통으로 채택하는 업계 표준입니다. 언리얼 엔진 내부의 소스 경로 Source/Runtime/Navmesh/Recast에서 수정된 버전의 소스 코드를 직접 확인할 수 있습니다.
Recast의 NavMesh 생성 파이프라인은 아래의 순서로 진행됩니다.
2-1. 복셀화(Voxelization)
첫 번째 단계는 레벨의 렌더 메시를 3D 격자(Voxel Grid)로 변환하는 것입니다. 각 복셀 셀은 공간의 작은 블록을 나타내며, 해당 위치에 지오메트리가 존재하는지 여부를 기록합니다. 이 과정이 중요한 이유는, 임의의 복잡한 삼각형 메시를 균일한 격자 구조로 단순화함으로써 이후 분석 단계를 병렬 처리하기 쉽게 만들기 때문입니다.
복셀 크기(Cell Size, Cell Height)가 작을수록 NavMesh 정밀도가 높아지지만 생성 시간과 메모리 사용량이 크게 증가합니다. 전략 시뮬레이션처럼 넓은 맵에서 세밀한 NavMesh가 필요한 경우 이 트레이드오프가 성능 병목의 주요 원인이 됩니다.
2-2. 보행 가능 영역 분류 및 침식(Walkable Area & Erosion)
복셀화가 완료되면 Recast는 각 복셀 컬럼을 분석하여 "AI가 서 있을 수 있는 면"인지를 판단합니다. 판단 기준은 두 가지입니다. 첫째, 해당 폴리곤의 기울기가 최대 경사각(Walkable Slope Angle)을 초과하지 않아야 합니다. 둘째, 해당 위치 위쪽으로 충분한 높이의 공간(Walkable Height)이 확보되어야 합니다.
분류가 끝나면 에이전트의 반경(Walkable Radius)만큼 보행 가능 영역을 내측으로 침식(Erosion)합니다. 이 과정은 벽이나 장애물 가장자리에서 에이전트가 끼이는 상황을 방지합니다. 예를 들어 반경이 35cm인 캐릭터의 경우, 벽으로부터 35cm 이내의 영역은 보행 불가 영역으로 표시됩니다.
2-3. 컴팩트 높이 필드 및 거리 필드 생성
보행 가능 영역 정보는 컴팩트 높이 필드(Compact Heightfield) 자료구조로 압축됩니다. 이후 각 보행 가능 스팬(Span)에서 가장 가까운 장애물까지의 거리를 계산하는 거리 필드(Distance Field) 가 생성됩니다. 거리 필드는 이후 다각형 영역 분리 단계에서 중심 축을 추출하는 데 사용됩니다.
2-4. 다각형 영역 생성(Region Generation)
거리 필드를 기반으로 보행 가능 표면을 서로 분리된 다각형 영역들로 나눕니다. Recast는 이 과정에서 Watershed 알고리즘을 적용합니다. 물이 낮은 곳으로 흘러 웅덩이를 형성하는 원리와 유사하게, 거리값이 낮은(장애물에 가까운) 지점들이 영역의 경계가 됩니다.
2-5. 윤곽 추출 및 다각형 생성(Contour & Polygon Mesh)
각 영역의 경계를 윤곽선(Contour)으로 추출하고, 이를 단순화하여 볼록 다각형(Convex Polygon) 집합으로 변환합니다. 이 볼록 다각형들의 집합이 최종 NavMesh 폴리곤 메시가 됩니다. Detour는 이 폴리곤 메시 위에서 경로를 탐색합니다.
언리얼 엔진에서 P 키(또는 에디터에서 Show → Navigation)를 누르면 확인되는 녹색 영역이 바로 이 최종 폴리곤 메시입니다.
2-6. 타일 기반 구조(Tile-Based NavMesh)
UE5의 NavMesh는 전체 맵을 하나의 거대한 메시로 생성하지 않고, 고정 크기의 타일(Tile) 단위로 분할하여 관리합니다. 각 타일은 독립적으로 생성·갱신·스트리밍될 수 있으므로, 오픈 월드처럼 광대한 환경에서 필요한 영역의 NavMesh만 메모리에 유지하는 것이 가능합니다.
타일 크기는 ARecastNavMesh의 TileSizeUU 속성으로 설정하며, 이 값은 NavMesh 정밀도와 업데이트 비용 사이의 핵심 균형점입니다.
3. 언리얼 엔진의 NavMesh 클래스 구조
Recast 파이프라인은 언리얼 엔진의 여러 클래스를 거쳐 실행됩니다. 각 클래스의 역할을 이해하면 런타임에서 NavMesh를 제어하는 코드를 작성할 때 어떤 API를 사용해야 하는지 명확해집니다.
주요 클래스의 관계는 다음과 같습니다.
UNavigationSystemV1: 내비게이션 시스템 전체를 관리하는 최상위 싱글턴 매니저입니다. 에이전트 등록, 경로 요청, NavMesh 갱신 등 모든 고수준 API의 진입점입니다.ARecastNavMesh: Recast 라이브러리와 언리얼 엔진 사이의 브릿지 역할을 하는 액터입니다. 에이전트 타입별로 하나씩 월드에 자동 스폰됩니다. 생성된 NavMesh 데이터에 접근하는 유틸리티 함수를 제공합니다.FRecastNavMeshGenerator: 타일 기반 생성 작업을 실질적으로 수행하는 클래스입니다. 언리얼의 Task 시스템을 통해FRecastTileGeneratorTask인스턴스들을 병렬로 실행합니다.FPImplRecastNavMesh: Recast의 핵심 타입(dtNavMesh,dtMeshTile)에 대한 접근을 래핑하는 PIMPL 구현체입니다.ARecastNavMesh가 소유합니다.
에이전트를 프로젝트 설정에 등록하면, 레벨에 ANavMeshBoundsVolume이 배치될 때 UNavigationSystemV1이 각 에이전트 타입에 대응하는 ARecastNavMesh를 자동으로 스폰합니다. 이후 ARecastNavMesh는 자신의 FRecastNavMeshGenerator에 타일 생성을 위임하고, 생성이 완료된 데이터는 dtNavMesh 객체에 채워집니다. 런타임 경로 탐색은 이 dtNavMesh를 Detour가 직접 쿼리합니다.
4. Detour: NavMesh 위에서 경로를 찾는 방법
NavMesh가 생성되면, AI가 실제로 경로를 요청할 때는 Recast가 아닌 Detour 가 담당합니다. Detour는 Recast와 동일한 오픈소스 라이브러리에 포함된 모듈로, 런타임 NavMesh 쿼리 시스템입니다.
4-1. A* on Polygon Graph
Detour의 경로 탐색 알고리즘은 폴리곤 그래프 위의 A*(A-Star) 입니다. 일반적인 그리드 기반 A*와 핵심 논리는 동일하지만, 노드(Node)가 격자 셀이 아닌 NavMesh 폴리곤이라는 점이 다릅니다.
A* 알고리즘은 각 노드에 대해 두 가지 비용을 유지합니다. g(n)은 시작 노드에서 현재 노드까지의 실제 이동 비용이고, h(n)은 현재 노드에서 목표 노드까지의 추정 비용(휴리스틱)입니다. f(n) = g(n) + h(n) 이 가장 작은 노드를 우선적으로 탐색함으로써 최단 경로에 가까운 결과를 효율적으로 찾아냅니다.
Detour에서는 다음의 요소가 추가됩니다. 첫째, Area Cost 입니다. 각 NavMesh 영역에는 이동 비용 계수를 부여할 수 있습니다. 물, 진흙, 도로 등 지형 유형에 따라 다른 비용을 설정하면, 동일한 거리라도 비용이 낮은 경로를 선호하는 AI를 구현할 수 있습니다. 둘째, Nav Link 입니다. 점프, 낙하, 사다리 등 물리적으로 연결되지 않은 폴리곤 사이를 이어주는 특수 링크입니다. Detour는 이 링크를 통해 폴리곤 그래프의 연결 관계를 확장합니다.
4-2. 폴리곤 경로에서 실제 경로 포인트로
A가 완료되면 "출발지 → 폴리곤 A → 폴리곤 B → ... → 목적지" 형태의 *폴리곤 경로(Polygon Path)** 가 생성됩니다. 그러나 이것은 실제 월드 좌표 경로가 아닙니다. AI가 따라갈 수 있는 좌표 배열로 변환하려면 String Pulling(또는 Funnel Algorithm) 이 필요합니다.
String Pulling은 폴리곤 경로에서 가장 짧고 직선에 가까운 실제 이동 경로를 추출하는 알고리즘입니다. 각 인접 폴리곤이 공유하는 엣지(Edge)를 "문(Portal)"으로 정의하고, 시작점에서 목적지까지 이 포탈들을 꿰는 가장 짧은 실을 당기는 것으로 이해할 수 있습니다. 결과로 얻어지는 경로는 불필요한 중간 웨이포인트가 제거되어 자연스러운 이동선이 됩니다.
4-3. 경로 매끄럽게 하기(Path Smoothing)
String Pulling 이후에도 경로는 폴리곤 모서리를 정확히 통과하는 "꺾인 경로"입니다. 캐릭터 AI에서는 이것이 크게 눈에 띄지 않지만, 차량 AI나 고품질 이동 연출이 필요한 경우에는 추가적인 경로 스무딩이 필요합니다.
UE5의 ARecastNavMesh에는 PathfindingAlgorithm 및 bDoPathPostProcessing 설정이 있으며, 커스텀 AI 컨트롤러에서 FindPathForMoveRequest를 오버라이드하여 스무딩 로직을 직접 삽입할 수도 있습니다. 아래는 경로를 찾은 뒤 가시성 검사(Line of Sight)로 중간 포인트를 제거하여 경로를 단순화하는 기본적인 스무딩 예시입니다.
경로 스무딩을 적용하는 커스텀 AI 컨트롤러의 기본 구조입니다.
// CustomAIController.h
#pragma once
#include "CoreMinimal.h"
#include "AIController.h"
#include "CustomAIController.generated.h"
UCLASS()
class MYGAME_API ACustomAIController : public AAIController
{
GENERATED_BODY()
public:
// AIController의 경로 계산 요청을 가로채 스무딩 로직을 추가합니다.
virtual void FindPathForMoveRequest(
const FAIMoveRequest& MoveRequest,
FPathFindingQuery& Query,
FNavPathSharedPtr& OutPath) const override;
private:
// 연속된 경로 포인트 사이의 시야를 확인하여 불필요한 중간 포인트를 제거합니다.
void SmoothPath(FNavPathSharedPtr& Path) const;
};
// CustomAIController.cpp
#include "CustomAIController.h"
#include "NavigationSystem.h"
#include "Kismet/KismetSystemLibrary.h"
void ACustomAIController::FindPathForMoveRequest(
const FAIMoveRequest& MoveRequest,
FPathFindingQuery& Query,
FNavPathSharedPtr& OutPath) const
{
// 기본 경로 탐색을 먼저 실행합니다.
Super::FindPathForMoveRequest(MoveRequest, Query, OutPath);
if (OutPath.IsValid() && OutPath->IsValid())
{
SmoothPath(OutPath);
}
}
void ACustomAIController::SmoothPath(FNavPathSharedPtr& Path) const
{
TArray<FNavPathPoint>& PathPoints = Path->GetPathPoints();
if (PathPoints.Num() <= 2)
{
return; // 시작점과 끝점만 있으면 스무딩 불필요
}
const UWorld* World = GetWorld();
int32 CurrentIndex = 0;
while (CurrentIndex < PathPoints.Num() - 2)
{
const FVector& From = PathPoints[CurrentIndex].Location;
const FVector& To = PathPoints[CurrentIndex + 2].Location;
FHitResult HitResult;
// 두 점 사이에 장애물이 없으면 중간 포인트를 제거합니다.
const bool bBlocked = UKismetSystemLibrary::SphereTraceSingle(
World,
From,
To,
30.f, // 에이전트 반경에 맞춰 조정
UEngineTypes::ConvertToTraceType(ECC_WorldStatic),
false,
TArray<AActor*>(),
EDrawDebugTrace::None,
HitResult,
true
);
if (!bBlocked)
{
// 중간 포인트 제거 후 동일한 인덱스에서 다시 검사
PathPoints.RemoveAt(CurrentIndex + 1);
}
else
{
++CurrentIndex;
}
}
}
위 코드에서는 FindPathForMoveRequest를 오버라이드하여 기본 경로 탐색 결과가 OutPath로 채워진 직후 SmoothPath를 호출합니다. SmoothPath 내부에서는 현재 포인트와 두 번째 앞 포인트 사이에 SphereTraceSingle을 수행하여 장애물이 없으면 중간 포인트를 제거하는 방식으로, 경로 포인트의 수를 최소화합니다.
5. 런타임 NavMesh 생성 전략
UE5는 NavMesh 생성 시점을 제어하는 세 가지 전략을 제공합니다. 프로젝트 설정의 Project Settings → Navigation Mesh → Runtime Generation에서 선택할 수 있습니다.
Static (기본값): 레벨 로드 시 NavMesh를 한 번 빌드하고, 이후에는 업데이트하지 않습니다. 성능이 가장 우수하며, 정적인 환경에 적합합니다.
Dynamic: 런타임에 장애물이 추가·제거·이동될 때 영향을 받은 타일을 자동으로 재빌드합니다. UNavigationSystemV1이 변경된 지오메트리를 감지하면, 해당 타일을 Dirty 상태로 표시하고 비동기로 재생성합니다.
Dynamic Modifiers Only: 지오메트리 자체는 재빌드하지 않되, NavModifierVolume 등의 영역 변경만 동적으로 반영합니다. Dynamic 모드보다 비용이 낮으면서도 일부 동적 요소를 반영할 수 있어 실용적인 타협점입니다.
Dynamic 모드에서 NavMesh 갱신을 코드로 요청하는 예시입니다.
// 특정 위치와 범위의 NavMesh를 명시적으로 Dirty 처리하여 재빌드를 요청합니다.
void AMyLevelManager::RequestNavMeshRebuildInArea(
const FVector& Center,
const FVector& Extent)
{
UNavigationSystemV1* NavSys = UNavigationSystemV1::GetCurrent(GetWorld());
if (!NavSys)
{
return;
}
// 갱신이 필요한 영역을 FBox로 정의합니다.
const FBox DirtyArea(Center - Extent, Center + Extent);
// 해당 영역을 Navigation System에 Dirty로 등록합니다.
// ENavigationDirtyFlag::All은 지오메트리 변경을 포함한 전체 갱신을 의미합니다.
NavSys->AddDirtyArea(DirtyArea, ENavigationDirtyFlag::All);
}
위 코드처럼 AddDirtyArea를 호출하면, 내비게이션 시스템이 해당 영역과 겹치는 NavMesh 타일들을 비동기로 재생성합니다. 단, 재생성이 완료되기 전에 경로 요청이 들어오면 이전 NavMesh 데이터로 처리되므로, 시나리오에 따라 갱신 완료 콜백이 필요합니다.
6. Navigation Invoker: 필요한 곳에만 NavMesh를 생성하기
오픈 월드나 대규모 맵에서 전체 영역의 NavMesh를 미리 생성하면 메모리와 빌드 시간 모두 막대합니다. 이를 해결하는 방법이 Navigation Invoker 입니다.
Navigation Invoker는 컴포넌트(UNavigationInvokerComponent)로 구현되며, 이 컴포넌트를 가진 액터 주변 일정 반경(TileGenerationRadius)에만 NavMesh 타일이 생성됩니다. 반경 밖의 타일은 자동으로 해제됩니다. 이 기능을 사용하려면 Runtime Generation이 Dynamic으로 설정되어 있어야 합니다.
Navigation Invoker 컴포넌트를 AI 캐릭터에 추가하는 예시입니다.
// MyAICharacter.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "Navigation/NavInvokerComponent.h"
#include "MyAICharacter.generated.h"
UCLASS()
class MYGAME_API AMyAICharacter : public ACharacter
{
GENERATED_BODY()
public:
AMyAICharacter();
private:
// 이 컴포넌트가 존재하는 한, 주변 반경에 NavMesh가 유지됩니다.
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))
UNavigationInvokerComponent* NavInvoker;
};
// MyAICharacter.cpp
#include "MyAICharacter.h"
AMyAICharacter::AMyAICharacter()
{
NavInvoker = CreateDefaultSubobject<UNavigationInvokerComponent>(TEXT("NavInvoker"));
// 캐릭터 주변 3000 UU 반경에 NavMesh 타일을 생성합니다.
NavInvoker->SetGenerationRadii(3000.f, 2000.f);
// 첫 번째 인자: 타일을 새로 생성할 반경
// 두 번째 인자: 타일을 제거할 반경 (히스테리시스 방지를 위해 생성 반경보다 작게 설정)
}
위 코드처럼 생성 반경과 제거 반경을 다르게 설정하는 것이 중요합니다. 두 값이 같으면 캐릭터가 경계선 근처를 이동할 때 타일이 반복 생성·해제되는 히스테리시스(Hysteresis) 현상이 발생합니다.
7. 경로 탐색 API 사용하기
경로를 직접 계산해야 하는 경우, UNavigationSystemV1이 제공하는 동기/비동기 API를 사용합니다.
7-1. 동기 경로 탐색 (FindPathSync)
결과를 즉시 반환합니다. 경로 탐색 비용이 작은 경우나 즉각적인 결과가 필요한 단순한 로직에 사용합니다.
FPathFindingQuery를 구성하여 동기 경로 탐색을 수행하는 예시입니다.
void AMyAIController::RequestPathToTarget(const FVector& TargetLocation)
{
UNavigationSystemV1* NavSys = UNavigationSystemV1::GetCurrent(GetWorld());
APawn* ControlledPawn = GetPawn();
if (!NavSys || !ControlledPawn)
{
return;
}
// 어떤 에이전트가, 어디서, 어디로 경로를 찾는지 쿼리 객체에 담습니다.
FPathFindingQuery PathQuery(
this,
*NavSys->GetDefaultNavDataInstance(),
ControlledPawn->GetActorLocation(),
TargetLocation
);
// NavMesh 위에 없는 위치를 자동으로 가장 가까운 NavMesh 위치로 투영합니다.
PathQuery.NavAgentProperties = ControlledPawn->GetNavAgentPropertiesRef();
// 동기 방식으로 경로를 즉시 계산합니다.
const FPathFindingResult Result = NavSys->FindPathSync(PathQuery);
if (Result.IsSuccessful())
{
// 경로 포인트들을 순서대로 순회합니다.
const TArray<FNavPathPoint>& PathPoints = Result.Path->GetPathPoints();
for (const FNavPathPoint& Point : PathPoints)
{
// 각 웨이포인트 좌표 활용
UE_LOG(LogTemp, Log, TEXT("Path Point: %s"), *Point.Location.ToString());
}
}
}
위 코드에서 FPathFindingQuery의 두 번째 인자로 넘기는 NavData는 어떤 NavMesh를 사용할지 지정합니다. GetDefaultNavDataInstance()는 기본 에이전트 타입의 NavMesh를 반환하며, 여러 에이전트 타입을 사용하는 경우 해당 에이전트의 NavAgentProperties에 맞는 NavData를 명시적으로 지정해야 합니다.
7-2. 비동기 경로 탐색 (FindPathAsync)
경로 탐색을 백그라운드 스레드에서 수행하고, 완료 시 델리게이트를 통해 결과를 통보합니다. 복잡한 경로 탐색이나 복수의 AI가 동시에 경로를 요청하는 경우에 적합합니다.
비동기 경로 탐색 요청과 콜백 처리 예시입니다.
// MyAIController.h (관련 선언 발췌)
UCLASS()
class MYGAME_API AMyAIController : public AAIController
{
GENERATED_BODY()
public:
void RequestAsyncPath(const FVector& TargetLocation);
private:
// 비동기 경로 탐색 완료 시 호출될 콜백 함수입니다.
void OnPathFound(uint32 RequestID, ENavigationQueryResult::Type ResultType, FNavPathSharedPtr FoundPath);
uint32 PendingPathRequestID = INVALID_NAVQUERYID;
};
// MyAIController.cpp (관련 구현 발췌)
void AMyAIController::RequestAsyncPath(const FVector& TargetLocation)
{
UNavigationSystemV1* NavSys = UNavigationSystemV1::GetCurrent(GetWorld());
APawn* ControlledPawn = GetPawn();
if (!NavSys || !ControlledPawn)
{
return;
}
FPathFindingQuery PathQuery(
this,
*NavSys->GetDefaultNavDataInstance(),
ControlledPawn->GetActorLocation(),
TargetLocation
);
// 결과가 준비되면 OnPathFound가 호출되도록 델리게이트를 바인딩합니다.
const FNavPathQueryDelegate PathFoundDelegate =
FNavPathQueryDelegate::CreateUObject(this, &AMyAIController::OnPathFound);
// 비동기 요청을 등록하고 요청 ID를 저장합니다.
PendingPathRequestID = NavSys->FindPathAsync(
ControlledPawn->GetNavAgentPropertiesRef(),
PathQuery,
PathFoundDelegate
);
}
void AMyAIController::OnPathFound(
uint32 RequestID,
ENavigationQueryResult::Type ResultType,
FNavPathSharedPtr FoundPath)
{
if (RequestID != PendingPathRequestID)
{
return; // 이전 요청의 응답은 무시합니다.
}
PendingPathRequestID = INVALID_NAVQUERYID;
if (ResultType == ENavigationQueryResult::Success && FoundPath.IsValid())
{
// 유효한 경로를 수신했을 때의 처리
UE_LOG(LogTemp, Log, TEXT("Path found with %d points"), FoundPath->GetPathPoints().Num());
}
}
위 코드처럼 PendingPathRequestID를 저장하고 콜백에서 일치 여부를 확인하는 것이 중요합니다. 이동 도중 목표가 변경되어 새로운 경로 요청이 발생했을 때, 이전 요청의 콜백이 늦게 도착하는 Race Condition을 방지합니다.
8. 일반 C++과의 비교
Recast/Detour를 일반 C++ 프로젝트에 직접 통합하는 것도 가능합니다. 이 라이브러리는 C++98 컴파일러만 있으면 빌드되며, 외부 의존성이 전혀 없는 순수 C++ 라이브러리입니다. 경로 탐색의 핵심 로직인 A* 알고리즘 역시 일반 C++의 그것과 동일한 원리입니다.
차이점은 언리얼 엔진이 이 라이브러리를 래핑하는 방식에서 발생합니다. 일반 C++에서 Detour를 직접 사용할 경우, dtNavMesh 초기화, 타일 데이터 생성, dtNavMeshQuery 관리 등 모든 생명 주기를 직접 관리해야 합니다. 반면 언리얼 엔진의 UNavigationSystemV1은 이 모든 과정을 자동화하고, 엔진의 Task 시스템을 활용한 비동기 처리, 에이전트 기반 멀티 NavMesh 관리, 레벨 스트리밍과의 통합 등 추가 기능을 제공합니다.
또한 일반 C++에서는 좌표 변환에 주의가 필요합니다. Recast는 Y-up 좌표계를 사용하는 반면 언리얼 엔진은 Z-up 좌표계를 사용합니다. UE5 내부적으로는 Recast2UnrealPoint, Unreal2RecastPoint 함수를 통해 좌표 변환이 자동으로 처리되지만, 외부 서버에서 UE의 내비게이션 데이터를 재사용하는 경우에는 이 변환을 직접 적용해야 합니다.
9. 유니티 엔진과의 비교
Unity도 내부적으로 동일한 Recast & Detour 라이브러리를 사용하므로, NavMesh를 생성하는 수학적 파이프라인은 근본적으로 동일합니다. 그러나 언리얼 엔진과 API 설계 철학이 다릅니다.
Unity의 NavMesh는 기본적으로 단일 씬에 단일 NavMesh를 생성합니다. 여러 에이전트 크기를 지원하려면 NavMesh Surface 컴포넌트를 에이전트 타입별로 별도 배치해야 합니다. 언리얼 엔진은 프로젝트 설정에 에이전트 타입을 등록하면, UNavigationSystemV1이 해당 타입 각각에 대한 ARecastNavMesh를 자동으로 관리합니다.
런타임 업데이트 측면에서 Unity는 NavMesh.RemoveNavMeshData / NavMesh.AddNavMeshData 와 NavMeshSurface.UpdateNavMesh()를 조합하여 갱신합니다. 언리얼 엔진은 Runtime Generation 설정만으로 동적 갱신 여부를 전환하고, 지오메트리 변경 시 엔진이 영향받는 타일만 자동으로 갱신합니다.
Navigation Invoker에 해당하는 기능은 Unity에서 NavMesh.AddNavMeshData와 커스텀 스크립트를 조합하여 유사하게 구현할 수 있지만, 언리얼 엔진의 UNavigationInvokerComponent처럼 컴포넌트 하나로 완성되는 내장 솔루션은 아닙니다.
10. 주의사항
10-1. 타일 크기와 업데이트 비용
Dynamic 모드에서 장애물이 빈번하게 이동하는 경우, 타일 크기(TileSizeUU)가 너무 크면 작은 변경에도 넓은 타일이 통째로 재생성됩니다. 반대로 너무 작으면 타일 수가 폭증하여 관리 비용이 증가합니다. 동적 장애물의 규모에 맞게 타일 크기를 조정하는 것이 중요합니다.
10-2. NavMesh 재생성은 비동기이므로 지연이 발생합니다
AddDirtyArea 호출 직후 경로 탐색을 요청하면, 갱신 전의 NavMesh로 경로가 계산될 수 있습니다. 갱신 완료를 보장해야 하는 시나리오에서는 UNavigationSystemV1의 OnNavigationGenerationFinishedDelegate에 바인딩하여 갱신 완료 시점을 확인하는 것이 올바른 접근 방식입니다.
10-3. Navigation Invoker와 Static 모드는 함께 사용할 수 없습니다
Navigation Invoker는 반드시 Runtime Generation이 Dynamic으로 설정된 경우에만 동작합니다. Static 모드에서는 런타임에 새로운 타일이 생성되지 않으므로 Invoker가 아무런 효과를 내지 못합니다.
10-4. 경로 탐색 결과는 NavMesh 위에 없는 위치를 요청하면 실패합니다
시작점이나 목적지가 NavMesh 범위 밖에 있는 경우 FindPathSync는 ENavigationQueryResult::Error를 반환합니다. ProjectPointToNavigation으로 좌표를 NavMesh 위로 투영한 뒤 경로 탐색을 수행하거나, FPathFindingQuery의 bAllowPartialPaths 옵션을 활성화하여 부분 경로를 허용하는 방식을 활용할 수 있습니다.
10-5. 복셀 크기와 NavMesh 정확도
Cell Size(복셀 크기)는 NavMesh의 정밀도를 결정하는 핵심 파라미터입니다. 값을 줄이면 좁은 통로나 계단 모서리 등 세밀한 지형을 정확하게 표현하지만, 생성 시간과 메모리 사용량이 제곱에 비례하여 증가합니다. 프로젝트의 지형 복잡도와 성능 예산에 맞게 튜닝하는 과정이 필수입니다.
10-6. 에이전트 크기가 다르면 NavMesh를 공유할 수 없습니다
Walkable Radius가 다른 에이전트들은 서로 다른 NavMesh가 필요합니다. 예를 들어 거대 보스와 일반 병사가 같은 맵에 존재한다면, 두 개의 ARecastNavMesh 인스턴스가 각각 생성됩니다. 에이전트 타입이 많을수록 NavMesh 생성 시간과 메모리가 선형 증가하므로, 크기가 유사한 에이전트들은 하나의 타입으로 통합하는 것이 좋습니다.
이 글에서는 Recast가 복셀화 파이프라인을 통해 NavMesh를 생성하는 과정과, Detour가 폴리곤 그래프 위의 A*로 경로를 탐색하는 원리를 단계별로 살펴보았습니다. NavMesh의 동작 원리를 깊이 이해하면, 단순히 AIMoveTo 노드를 사용하는 것을 넘어 런타임 업데이트 전략 선택, 타일 크기 튜닝, 다중 에이전트 설계, 커스텀 경로 스무딩 구현까지 근거 있는 결정을 내릴 수 있습니다. 특히 소울라이크나 오픈 월드처럼 복잡한 지형과 다양한 AI 캐릭터가 공존하는 프로젝트에서, Navigation Invoker와 Dynamic 모드의 조합은 성능과 기능 사이의 실용적인 균형점이 됩니다.
'언리얼 엔진 5' 카테고리의 다른 글
| [UE5] NavLink - 단절된 NavMesh 구간을 연결하는 기술 (1) | 2026.03.23 |
|---|---|
| [UE5] AIController · MoveTo & NavigationQueryFilter 커스터마이징 (0) | 2026.03.22 |
| [UE5] 네비게이션 시스템 개요 및 NavMesh 기초 (0) | 2026.03.19 |
| [UE5] UPrimitiveComponent 의 바운딩 박스 (1) | 2026.03.18 |
| [UE5] 언리얼 인터페이스(UInterface) (0) | 2026.03.15 |