언리얼 엔진 5

[UE5] UNavLocalGridManager - 로컬 내비게이션 그리드로 AI 경로 탐색 제어하기

언린이 2026. 3. 28. 16:49
반응형

1. 이 기능이 필요한 이유

언리얼 엔진의 기본 AI 경로 탐색 시스템은 Recast NavMesh 기반으로 동작합니다. NavMesh는 레벨 전체를 미리 굽는(Bake) 정적 구조이기 때문에, 플레이 중에 장애물이 생기거나 이동하는 경우 실시간으로 반응하는 데 한계가 있습니다.

예를 들어 다음과 같은 상황을 생각해볼 수 있습니다.

  • 폭발로 인해 특정 구역이 일시적으로 위험해져 AI가 접근하지 말아야 할 때
  • NPC가 지나가면서 좁은 통로를 순간적으로 막고 있을 때
  • 플레이어가 설치한 바리케이드가 AI의 이동 경로를 막아야 할 때

이러한 경우 NavMesh를 실시간으로 다시 굽는 것은 비용이 매우 크고, RVO(Reciprocal Velocity Obstacles) 회피 시스템은 이미 움직이는 에이전트 간의 충돌을 피하는 데 특화되어 있어 정적 장애물 표현에는 적합하지 않습니다.

UNavLocalGridManager는 이 공백을 메우기 위해 설계된 시스템입니다. NavMesh와 독립적으로 동작하는 가볍고 동적인 2D 그리드를 런타임에 추가하고 제거함으로써, AI 에이전트가 특정 영역을 장애물로 인식하고 경로 탐색 시 우회하도록 만들 수 있습니다.

2. UNavLocalGridManager의 구조와 핵심 개념

2-1. 클래스 개요

UNavLocalGridManagerUObject를 상속하며, AIModule에 포함된 월드 서브시스템 수준의 싱글턴 매니저입니다. 소스 파일 위치는 다음과 같습니다.

Engine/Source/Runtime/AIModule/Classes/Navigation/NavLocalGridManager.h

이 클래스는 여러 출처에서 등록된 그리드 데이터를 취합하여 비중복(Non-Overlapping) 통합 그리드를 구성하고, 이를 경로 탐색에 활용합니다. 함께 사용해야 할 연관 클래스는 다음과 같습니다.

클래스 / 구조체 역할
UNavLocalGridManager 그리드들을 등록·관리하고 경로 탐색 쿼리를 제공하는 매니저
FNavLocalGridData 개별 장애물이 소유하는 그리드 데이터 구조체
UGridPathFollowingComponent Local Grid를 인식하여 경로를 따라가는 PathFollowing 컴포넌트

2-2. FNavLocalGridData 구조

FNavLocalGridData는 개별 장애물 하나가 차지하는 로컬 그리드 영역을 표현합니다. 내부적으로 2D 셀 배열로 구성되며, 각 셀은 이동 가능(Free) 또는 장애물(Obstacle) 상태 중 하나를 가집니다. 셀은 8방향 이웃과 연결되며, 셀과 셀 사이에 별도의 벽(Wall) 개념은 없습니다.

FNavLocalGridData를 생성할 때는 그리드의 중심 위치와 2D 반경(Extent2D)을 지정합니다.

// 중심 위치와 2D 반경을 받아 FNavLocalGridData를 생성하는 생성자
FNavLocalGridData(const FVector& Center, float Extent2D);

위의 생성자를 사용하면 지정한 중심 좌표와 반경을 기반으로 그리드의 커버 범위가 자동으로 계산됩니다.

2-3. 그리드의 밀도(Density)

그리드 밀도는 SetLocalNavigationGridDensity를 통해 전역적으로 조정할 수 있으며, 밀도가 높을수록 셀 하나의 크기가 작아져 더 정밀한 장애물 표현이 가능해집니다. 단, 셀이 많아질수록 메모리 사용량과 경로 탐색 비용도 함께 증가합니다.

3. 그리드 등록과 제거 — 핵심 API

UNavLocalGridManager의 모든 공개 API는 BlueprintCallable 정적 함수로 제공됩니다. C++과 블루프린트 모두에서 동일하게 호출할 수 있습니다.

3-1. 그리드 등록 함수

언리얼 엔진은 장애물의 형태에 따라 세 가지 형태의 그리드 등록 함수를 제공합니다.

함수 장애물 형태
AddLocalNavigationGridForPoint 단일 점(Point) 기반 원형 영역
AddLocalNavigationGridForCapsule 캡슐 형태의 영역
AddLocalNavigationGridForBox 박스(Box) 형태의 영역

세 함수 모두 공통적으로 Radius2DHeight, bRebuildGrids 파라미터를 받으며, 등록 성공 시 그리드 핸들(int32) 을 반환합니다. 이 핸들은 나중에 그리드를 제거할 때 필요하므로 반드시 보관해야 합니다.

아래는 캡슐 장애물을 등록하는 가장 일반적인 예시입니다.

// MyObstacleActor.h
UCLASS()
class MYGAME_API AMyObstacleActor : public AActor
{
    GENERATED_BODY()

public:
    AMyObstacleActor();

    virtual void BeginPlay() override;
    virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;

private:
    // 등록된 그리드의 핸들 — 제거 시 사용
    int32 LocalGridHandle = INDEX_NONE;
};
// MyObstacleActor.cpp
#include "Navigation/NavLocalGridManager.h"

void AMyObstacleActor::BeginPlay()
{
    Super::BeginPlay();

    // 캡슐 형태의 로컬 그리드를 등록 (반지름 50, 반높이 90)
    LocalGridHandle = UNavLocalGridManager::AddLocalNavigationGridForCapsule(
        this,               // WorldContextObject
        GetActorLocation(), // 장애물 중심 위치
        50.f,               // CapsuleRadius
        90.f,               // CapsuleHalfHeight
        5,                  // Radius2D: 그리드 셀 반경
        100.f,              // Height: 그리드 유효 높이
        true                // bRebuildGrids: 즉시 통합 그리드 재빌드 여부
    );
}

void AMyObstacleActor::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
    // 액터가 소멸될 때 등록된 그리드를 반드시 제거
    if (LocalGridHandle != INDEX_NONE)
    {
        UNavLocalGridManager::RemoveLocalNavigationGrid(this, LocalGridHandle, true);
        LocalGridHandle = INDEX_NONE;
    }

    Super::EndPlay(EndPlayReason);
}

위 코드처럼 BeginPlay에서 그리드를 등록하고 EndPlay에서 제거하는 패턴이 가장 기본적이며 안전한 사용 방식입니다. LocalGridHandleINDEX_NONE인지 확인한 후 제거 함수를 호출하는 습관이 중요합니다.

3-2. 박스 형태의 그리드 등록

박스 형태의 장애물에는 AddLocalNavigationGridForBox를 사용합니다. 회전(Rotation)을 함께 지정할 수 있어 방향이 있는 장애물에 유용합니다.

// 박스 형태의 장애물 등록 예시 (회전 포함)
LocalGridHandle = UNavLocalGridManager::AddLocalNavigationGridForBox(
    this,
    GetActorLocation(),
    FVector(100.f, 60.f, 50.f), // Extent: 박스 반크기
    GetActorRotation(),          // Rotation: 박스 방향
    7,                           // Radius2D
    120.f,                       // Height
    true
);

Extent 파라미터는 박스 전체 크기의 절반값(Half-Extent)을 의미합니다. 위 코드처럼 X/Y/Z 각 축의 절반 크기를 FVector로 전달하면, 그 범위 안의 셀들이 장애물로 표시됩니다.

3-3. 그리드 제거

// 핸들을 사용해 등록된 그리드를 제거
// bRebuildGrids: true이면 제거 후 즉시 통합 그리드를 재빌드
UNavLocalGridManager::RemoveLocalNavigationGrid(this, LocalGridHandle, true);

bRebuildGridsfalse로 설정하면 그리드 제거 자체는 즉시 이루어지지만, 내부 통합 그리드는 다음 업데이트까지 갱신되지 않습니다. 여러 그리드를 한꺼번에 추가하거나 제거할 때는 마지막 호출에만 true를 전달하여 불필요한 중간 재빌드를 피하는 것이 효율적입니다.

4. UGridPathFollowingComponent 연동

UNavLocalGridManager에 그리드를 등록하는 것만으로는 AI가 이를 자동으로 인식하지 않습니다. AI 컨트롤러가 사용하는 UPathFollowingComponentUGridPathFollowingComponent로 교체해야 로컬 그리드 기반의 경로 탐색이 활성화됩니다.

아래는 AI 컨트롤러에서 UGridPathFollowingComponent를 등록하는 방법입니다.

// MyAIController.h
#include "Navigation/GridPathFollowingComponent.h"

UCLASS()
class MYGAME_API AMyAIController : public AAIController
{
    GENERATED_BODY()

public:
    AMyAIController(const FObjectInitializer& ObjectInitializer);
};
// MyAIController.cpp
AMyAIController::AMyAIController(const FObjectInitializer& ObjectInitializer)
    : Super(ObjectInitializer.SetDefaultSubobjectClass<UGridPathFollowingComponent>(TEXT("PathFollowingComponent")))
{
    // ObjectInitializer를 사용해 기본 UPathFollowingComponent를
    // UGridPathFollowingComponent로 교체합니다.
}

위 코드처럼 FObjectInitializer::SetDefaultSubobjectClass를 활용하면 부모 클래스에서 생성하는 기본 컴포넌트를 파생 클래스로 교체할 수 있습니다. 이 방식은 언리얼 엔진에서 컴포넌트를 서브클래싱할 때 표준적으로 사용하는 패턴입니다.

5. 그리드 밀도 조절

그리드 전체의 셀 크기는 SetLocalNavigationGridDensity로 전역 설정합니다. 이 값은 씬 전체에 등록된 모든 로컬 그리드에 일괄 적용됩니다.

// 그리드 밀도를 설정 (단위: cm, 값이 작을수록 셀이 촘촘해집니다)
UNavLocalGridManager::SetLocalNavigationGridDensity(this, 25.f);

밀도 값은 셀 하나의 세계 공간(World Space) 크기를 의미합니다. 값을 25로 설정하면 25cm×25cm 크기의 셀 단위로 장애물이 표현됩니다. 위 코드처럼 밀도를 낮추면 더 정밀한 장애물 표현이 가능하지만, 동시에 셀 수가 기하급수적으로 늘어나 성능에 영향을 줄 수 있으므로 프로젝트의 AI 에이전트 밀도와 장애물 크기에 맞게 조정해야 합니다.

6. 현업 활용 예시

6-1. 동적 위험 구역 — 범위 공격 및 폭발

전략 게임이나 액션 RPG에서 플레이어 또는 다른 AI가 범위 스킬을 시전했을 때, 해당 영역을 AI가 일정 시간 동안 피하도록 만들 수 있습니다.

// UDangerZoneComponent.h
UCLASS(ClassGroup = (AI), meta = (BlueprintSpawnableComponent))
class MYGAME_API UDangerZoneComponent : public UActorComponent
{
    GENERATED_BODY()

public:
    // 위험 구역을 활성화하고 Duration 후 자동 해제
    UFUNCTION(BlueprintCallable, Category = "AI|Navigation")
    void ActivateDangerZone(float Radius, float Duration);

private:
    void DeactivateDangerZone();

    int32 DangerGridHandle = INDEX_NONE;
    FTimerHandle DangerTimerHandle;
};
// UDangerZoneComponent.cpp
#include "Navigation/NavLocalGridManager.h"

void UDangerZoneComponent::ActivateDangerZone(float Radius, float Duration)
{
    // 이미 활성화된 그리드가 있다면 먼저 제거
    if (DangerGridHandle != INDEX_NONE)
    {
        UNavLocalGridManager::RemoveLocalNavigationGrid(this, DangerGridHandle, false);
    }

    // 폭발 중심 위치에 원형 위험 구역 등록
    DangerGridHandle = UNavLocalGridManager::AddLocalNavigationGridForPoint(
        this,
        GetOwner()->GetActorLocation(),
        FMath::CeilToInt(Radius / 25.f), // Radius2D: 그리드 셀 단위로 변환
        200.f,
        true
    );

    // Duration 이후 자동 해제
    GetWorld()->GetTimerManager().SetTimer(
        DangerTimerHandle,
        this,
        &UDangerZoneComponent::DeactivateDangerZone,
        Duration,
        false
    );
}

void UDangerZoneComponent::DeactivateDangerZone()
{
    if (DangerGridHandle != INDEX_NONE)
    {
        UNavLocalGridManager::RemoveLocalNavigationGrid(this, DangerGridHandle, true);
        DangerGridHandle = INDEX_NONE;
    }
}

위 코드처럼 타이머와 결합하면 "N초 동안 이 구역에 접근하지 마라"는 의도를 매우 적은 비용으로 구현할 수 있습니다. 이 방식은 NavMesh 재빌드 없이도 동작하기 때문에 실시간 연출이 많은 게임에서 실용적입니다.

6-2. 이동하는 장애물 — 이동 플랫폼 및 패트롤 NPC

장애물이 이동하는 경우에는 매 틱(Tick)마다 그리드를 제거하고 새로 등록하거나, 일정 거리 이상 이동했을 때만 갱신하는 방식으로 구현합니다. 갱신 주기를 조절하면 성능과 정확도 사이의 균형을 맞출 수 있습니다.

// 이동 장애물의 그리드를 주기적으로 갱신하는 예시
void AMovingObstacleActor::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);

    const FVector CurrentLocation = GetActorLocation();
    const float MovedDistance = FVector::Dist(CurrentLocation, LastRegisteredLocation);

    // 25cm 이상 이동한 경우에만 그리드 갱신 (매 틱 갱신 방지)
    if (MovedDistance > 25.f)
    {
        if (LocalGridHandle != INDEX_NONE)
        {
            UNavLocalGridManager::RemoveLocalNavigationGrid(this, LocalGridHandle, false);
        }

        LocalGridHandle = UNavLocalGridManager::AddLocalNavigationGridForCapsule(
            this,
            CurrentLocation,
            CapsuleRadius,
            CapsuleHalfHeight,
            5,
            100.f,
            true // 마지막에만 true로 재빌드
        );

        LastRegisteredLocation = CurrentLocation;
    }
}

위 코드처럼 MovedDistance 임계값을 두면 작은 진동이나 미세한 이동에 의해 매 프레임마다 그리드가 재빌드되는 상황을 방지할 수 있습니다. 임계값은 셀 크기의 배수 단위로 설정하는 것이 자연스럽습니다.

6-3. 플레이어 설치형 장애물 — 바리케이드, 함정

플레이어가 레벨 내에 물건을 배치하는 게임 플레이에서, 설치된 오브젝트를 AI가 실제 장애물로 인식하게 만드는 패턴입니다.

// 바리케이드가 설치될 때 호출되는 함수
void ABarricadeActor::OnPlaced()
{
    const FVector BoxExtent = GetComponentsBoundingBox().GetExtent();

    // 바리케이드의 실제 바운드를 기반으로 박스 그리드 등록
    LocalGridHandle = UNavLocalGridManager::AddLocalNavigationGridForBox(
        this,
        GetActorLocation(),
        BoxExtent,
        GetActorRotation(),
        FMath::CeilToInt(FMath::Max(BoxExtent.X, BoxExtent.Y) / 25.f),
        BoxExtent.Z * 2.f,
        true
    );
}

// 바리케이드가 파괴될 때 호출되는 함수
void ABarricadeActor::OnDestroyed()
{
    if (LocalGridHandle != INDEX_NONE)
    {
        UNavLocalGridManager::RemoveLocalNavigationGrid(this, LocalGridHandle, true);
        LocalGridHandle = INDEX_NONE;
    }
}

위 코드처럼 바운딩 박스에서 Extent를 추출하여 그대로 그리드 크기에 반영하면, 메시의 실제 크기에 맞는 장애물 범위를 자동으로 계산할 수 있어 유지보수가 용이합니다.

7. FindPath — 로컬 그리드 경로 탐색

UNavLocalGridManager는 등록된 그리드 안에서의 경로 탐색 쿼리도 제공합니다. FindPath 함수는 시작 위치와 목표 위치가 동일한 통합 그리드 위에 있을 경우 경로를 반환합니다.

// 로컬 그리드 위에서 직접 경로를 탐색하는 예시
TArray<FVector> PathPoints;
const bool bFound = UNavLocalGridManager::FindPath(
    this,
    StartLocation,
    EndLocation,
    PathPoints // 출력: 경로 포인트 배열
);

if (bFound)
{
    // PathPoints를 이용해 에이전트를 이동시킵니다.
    for (const FVector& Point : PathPoints)
    {
        DrawDebugSphere(GetWorld(), Point, 10.f, 8, FColor::Green, false, 2.f);
    }
}

위 코드처럼 FindPathtrue를 반환하면 PathPoints 배열에 경로 포인트들이 채워집니다. 시작과 도착 위치가 서로 다른 그리드에 있거나, 그리드 밖의 좌표를 전달하면 false를 반환합니다. 이 함수는 UGridPathFollowingComponent와 독립적으로 사용할 수 있어, 경로 미리보기나 디버그 시각화에도 활용할 수 있습니다.

8. 일반 C++과의 비교

일반 C++에서 동적 장애물 맵을 구현한다면, 직접 2D 배열을 관리하고 비트 연산으로 장애물 셀을 마킹한 뒤 A* 알고리즘을 붙이는 방식이 일반적입니다.

UNavLocalGridManager와의 핵심 차이는 다음과 같습니다.

  • 등록/해제 생명주기 관리: 여러 출처에서 그리드를 독립적으로 등록·해제해도, 매니저가 비중복 통합 그리드를 자동으로 재계산합니다. 일반 C++이라면 여러 그리드의 병합 충돌 처리 로직을 직접 작성해야 합니다.
  • 형상 헬퍼 내장: Point, Capsule, Box 등 일반적인 콜리전 형상을 그리드 셀로 변환하는 함수가 내장되어 있습니다. 일반 C++에서는 이러한 래스터라이징 로직을 직접 구현해야 합니다.
  • UObject 생명주기 연동: UNavLocalGridManager는 UObject 시스템 위에서 동작하므로 GC, 월드 컨텍스트, 멀티플레이어 환경의 서버/클라이언트 분리를 자연스럽게 따릅니다.

9. 유니티 엔진과의 비교

유니티의 Navigation 시스템은 NavMeshObstacle 컴포넌트를 통해 런타임 장애물을 표현합니다. NavMeshObstacle은 NavMesh 위에 Carving 방식으로 구멍을 뚫어 AI가 해당 영역으로 이동하지 않도록 합니다.

두 시스템의 차이는 다음과 같습니다.

항목 유니티 NavMeshObstacle 언리얼 UNavLocalGridManager
동작 방식 NavMesh Carving (NavMesh에 직접 구멍) 독립적인 2D 그리드 레이어 추가
경로 탐색 영향 NavMesh 재빌드 필요 (Carve Mode) NavMesh와 무관하게 즉시 반영
형상 지원 Box, Capsule Point, Capsule, Box (+ 임의 셀 마킹)
PathFollowing 연동 NavMeshAgent가 자동 처리 UGridPathFollowingComponent 교체 필요
세밀도 제어 NavMesh Voxel 크기에 종속 SetLocalNavigationGridDensity로 독립 조절

언리얼의 방식은 NavMesh 재빌드 비용 없이 장애물을 즉시 반영할 수 있다는 점이 강점입니다. 유니티의 NavMeshObstacle은 Carving을 활성화하면 약간의 재빌드 딜레이가 발생하는 반면, 언리얼은 독립적인 그리드 레이어를 사용하므로 이 문제에서 자유롭습니다.

10. 주의사항

10-1. 그리드 핸들 누수에 주의해야 합니다.

AddLocalNavigationGridFor* 함수가 반환하는 핸들을 잃어버리면, 해당 그리드를 제거할 방법이 없어집니다. 액터 소멸 시 반드시 EndPlay나 소멸자에서 RemoveLocalNavigationGrid를 호출해야 합니다. 핸들은 반드시 멤버 변수로 보관하고, 초기값은 INDEX_NONE으로 설정하는 것이 안전합니다.

10-2. bRebuildGrids true 남용에 주의해야 합니다.

한 프레임 내에서 여러 그리드를 동시에 추가하거나 제거할 경우, 매 호출마다 bRebuildGrids = true를 전달하면 통합 그리드가 불필요하게 여러 번 재빌드됩니다. 중간 호출에서는 false를 전달하고 마지막 호출에서만 true를 사용하는 방식으로 최적화해야 합니다.

10-3. UGridPathFollowingComponent 교체를 잊지 말아야 합니다.

UNavLocalGridManager에 그리드를 등록했더라도, AI 컨트롤러가 기본 UPathFollowingComponent를 사용하고 있다면 해당 AI는 로컬 그리드를 경로 탐색에 활용하지 못합니다. 로컬 그리드 기반 경로 탐색을 원하는 AI 컨트롤러는 반드시 UGridPathFollowingComponent로 교체해야 합니다.

10-4. Radius2D 파라미터는 셀 단위입니다.

AddLocalNavigationGridFor* 함수의 Radius2D는 월드 공간의 cm 단위가 아니라 셀 개수 단위입니다. 그리드 밀도가 25cm일 때 반경 100cm의 장애물을 표현하려면 Radius2D = 4를 전달해야 합니다. 이 점을 혼동하면 장애물이 지나치게 크거나 작게 등록될 수 있습니다.

10-5. 멀티플레이어 환경에서는 서버에서만 호출해야 합니다.

UNavLocalGridManager는 서버에서 관리되는 내비게이션 데이터입니다. 클라이언트에서 그리드를 등록·해제하는 호출은 서버의 AI 경로 탐색에 영향을 주지 않으며, 예기치 않은 동작을 유발할 수 있습니다. 반드시 서버 권한(Authority)을 가진 환경에서 호출해야 합니다.

 

 

UNavLocalGridManager는 NavMesh 재빌드 없이 런타임에 AI 경로 탐색 영향 영역을 동적으로 추가하고 제거할 수 있는 경량 시스템입니다. 동적 위험 구역, 이동 장애물, 플레이어 설치형 오브젝트처럼 NavMesh가 실시간으로 반응하기 어려운 상황에서 활용하면, AI의 경로 탐색 품질을 크게 향상시킬 수 있습니다. 특히 UGridPathFollowingComponent와의 연동과 그리드 핸들 생명주기 관리를 정확하게 이해하는 것이 이 시스템을 안정적으로 사용하기 위한 핵심입니다.

반응형