Unreal Engine

Unreal Engine - AI (1)

gbleem 2025. 4. 25. 16:01

1. NavMeshBoundVolume


1 - 1. Overview

NavMeshBoundVolume은 AI가 움직이는 영역을 정의할 수 있는 범위를 말한다.

  • 액터 배치 -> 볼륨 -> 내비메시 바운드 볼륨

 

해당 볼륨의 특징으로는 기본적인 세팅에서는 동적인 범위 생성이 불가능하다는 점이다.

  • 아래와 같이 레벨에 설치 후 단축키 P를 통해서 내비메시가 깔린 범위를 체크할 수 있다.

 

그러나 게임 플레이 도중, 장애물이나 여러 움직임에 따라 내비메시의 범위를 조절하고 싶다면, Dynamic 설정을 해주어야 한다.

  • 프로젝트 세팅 -> 엔진 -> 네비게이션 메시 -> 런타임 -> 런타임 생성
  • 디폴트 값으로는 static으로 되어있지만, 동적으로 생성하기 위해서는 dymamic을 선택해주면 된다.

 

1 - 2. Dynamic 종류

Dynamic

  • 동적으로 경로를 재구성하고, 경로를 빼기 및 더하기 작업을 동시에 수행한다.
  • 사진에서 파란색 액터 위에 영역 생성 (더하기 작업)

Dynamic Modifiers Only

  • 동적으로 경로를 재구성하지만, 경로를 빼기만 한다는 것이 차이점
  • 사진에서 위의 사진과 달리 영역이 생기지는 않고, 빼기 작업만 수행
  • 사용하기 위해서는 NavModifier 컴포넌트가 필요하다.

 

1 - 3. Navigation Invoker

언리얼 엔진의 네비메시는 브러시 방식을 사용하기 때문에, 런타임 중 동적 생성이 불가능하다는 단점이 존재한다.

그렇기 때문에, AI가 레벨 전체에서 돌아다니는 경우 레벨 전체에 네비매시를 깔아두어야하는 비효율적인 결과가 생긴다.

  • 이것을 해결하기 위해 존재하는 것이 "네비 인보커" 이다.
  • 네비 인보커는 Navigation Invoker 컴포넌트를 가진 액터의 주변 영역만 계산하여 리소스를 아끼는 시스템이다.

네비 인보커를 사용하기

  • 먼저 Build.cs에 아래 처럼 NavigationSystem 모듈 추가
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput", "NavigationSystem" });
  • NPC에 컴포넌트 추가
//헤더
#pragma region NavInvoker
	UPROPERTY(BlueprintReadWrite, Category = Navigation, meta = (AllowPrivateAccess = "true"))
	TObjectPtr<UNavigationInvokerComponent> NavInvoker;

	float NavGenerationRadius;

	float NavRemovalRadius;

	FORCEINLINE class UNavigationInvokerComponent* GetNavInvoker()const { return NavInvoker; }
#pragma endregion

//cpp
NavInvoker = CreateDefaultSubobject<UNavigationInvokerComponent>(TEXT("NavInvoker"));
NavInvoker->SetGenerationRadii(NavGenerationRadius, NavRemovalRadius);
  • 프로젝트 세팅
    • 내비게이션 매시의 런타임 생성은 Dynamic으로 설정 해준다.
    • 내비게이션 시스템 -> 내비게이션 실행 -> 내비게이션 인보커 주변에만 내비게이션 실행 -> true

 

실행 모습

  • 레벨 전체에 내비메시가 생기지 않고 NPC(AI) 주변에만 생기는 것을 확인할 수 있다.

 

 

2. 장애물 설정하기 (Nav Modifier Volume)


2 - 1. Overview

내비메시 내에 장애물을 두어, "런타임"에 AI가 장애물을 피해서 이동하는 것을 구현해보기

 

대체로 게임 내에서 경로찾기에 있어서는 A*나 다익스트라 방식을 사용한다.

  • 이 방식들은 경로의 가중치를 모두 체크해본 다음 가장 코스트가 적은 경로를 선택하는 방식이다.
  • 언리얼 내부적으로 어떤 방식을 쓰는지는 모르지만, 실습을 통해 동작하는 모습은 체크해 볼 수 있다.
  • 장애물을 런타임에 생성하기 위해 Nav Modifier를 사용한다.

장애물 설정

  • Nav Modifier volume을 레벨에 설치하고,
  • 설정을 Movable과 Obstacle로 바꾸기

  • 추가적으로 장애물을 피해서 이동할 경로를 만들기 위해 액터 배치 -> 타깃 포인트 를 배치해준다.

내비 모디파이어를 Obstacle로 설정하면 아래와 같이 빨간색으로 표시가 되는데, 이것은 해당 지역이 주변 지역(초록색) 보다 계산 cost가 높다는 의미이다. (피해가야 한다는 의미가 된다.)

 

2 - 2. 실습

AI 캐릭터에 아래와 같이 컨트롤러 세팅을 해준 후

//헤더
UPROPERTY()
AAIController* AIController;

//cpp
void AUE5_AIStudyCharacter::BeginPlay()
{
	Super::BeginPlay();

	AIController = Cast<AAIController>(GetController());

	if (IsValid(AIController))
	{
		AIController->ReceiveMoveCompleted.RemoveDynamic(this, &ThisClass::OnMoveCompleted);
		AIController->ReceiveMoveCompleted.AddDynamic(this, &ThisClass::OnMoveCompleted);

		FindTargetPoints();
		StartMoving();
	}
}

void AUE5_AIStudyCharacter::NotifyControllerChanged()
{
	Super::NotifyControllerChanged();

	if (APlayerController* PlayerController = Cast<APlayerController>(Controller))
	{
		if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
		{
			Subsystem->AddMappingContext(DefaultMappingContext, 0);
		}
	}
	else //AI
	{
		AIController = Cast<AAIController>(Controller);
		if (IsValid(AIController))
		{
			AIController->ReceiveMoveCompleted.AddDynamic(this, &ThisClass::OnMoveCompleted);
		}
	}
}

 

움직임 함수와 바인딩을 하여, AI가 움직일 수 있도록  아래의 로직 순서로 세팅을 해준다.

  • 타겟 포인트를 찾는 함수(FindTargetPoints) 를 실행 후
  • 해당 타깃으로 이동하는 함수 실행 (MoveToTarget) 
  • 움직임이 완료되면, 바인딩된 OnMoveCompleted 함수를 통해 flag 값을 변경 후 타이머를 통해 다시 MoveToTarget 함수 실행

기억할 것 (AI를 움직이도록 하는 함수)

  • MoveToActor
EPathFollowingRequestResult::Type MoveResult = AIController->MoveToLocation(
TargetLocation,
AcceptanceRadius,
true,  // 목적지에 오버랩 되면 도착으로 판정할지 여부.
true,  // 경로 찾기 사용
false, // 프로젝션 사용 안함
true   // 네비게이션 데이터 사용
);
  • MoveToLocation
EPathFollowingRequestResult::Type MoveToLocation(
const FVector& Dest, //목적지 위치
float AcceptanceRadius = -1, //AI가 목적지에 도달했다고 판단하는 거리
bool bStopOnOverlap = true, //목적지와 캐릭터의 충돌 영역이 겹쳤을때 도착 여부 처리
bool bUsePathfinding = true, //경로 탐색 알고리즘 쓸지(false면 직선 이동)
bool bProjectDestinationToNavigation = true, //목적지를 네비메시에 투영할지
bool bCanStrafe = false, //AI가 측면이동을 할 수 있는지
TSubclassOf<UNavigationQueryFilter> FilterClass = nullptr, //필터 클래스
bool bAllowPartialPath = true //완전한 경로를 찾지 못하면 부분 경로를 가도록 허용할지
);

 

실행 결과 모습

 

 

3. RVO (Reciprocal Velocity Obstacles)


3 - 1. Overview

RVO란 움직이는 객체들이 서로의 속도와 방향을 고려하며 충돌을 피하는 방법을 말한다.

경로의 재계산 없이 속도와 방향을 조정하여 "실시간 회피"가 가능하기 때문에, 다수의 액터가 있을때 효율적인 움직임을 구현할 수 있다.

 

이미 언리얼에 구현되어 있기 때문에, 변수를 조절하여 움직임을 구현할 수 있다.

  • GetCharacterMovement()->bUseRVOAvoidance 
    • true로 설정하여 RVO를 켤 수 있다.
  • GetCharacterMovement()->AvoidanceConsiderationRadius
    • AI가 다른 오브젝트를 감지하고, 회피를 시작하는 반경을 뜻한다.
    • 대략적으로 200 ~ 500 사이의 값이 적당하다 (그러나 액터의 크기나 특징에 따라 다르게 설정할 수 있다)
  • GetCharacterMovement()->AvoidanceWeight
    • 회피 우선순위를 결정하는 변수
    • 클수록(1에 가까울수록) 다른 캐릭터들이 우선적으로 피하는 존재가 된다.

 

3 - 2. 실습

  • 새로운 캐릭터 클래스를 하나 만든 후, RVO 세팅을 해준다.
  • 이후 BeginPlay에서 MoveToActor를 사용하여 target을 향해 움직이도록 구현
ARVO_NPC::ARVO_NPC()
{
	PrimaryActorTick.bCanEverTick = false;

	// RVO 회피 시스템 활성화
	UCharacterMovementComponent* MovementComponent = GetCharacterMovement();
	if (MovementComponent)
	{
		MovementComponent->bUseRVOAvoidance = true;
		MovementComponent->AvoidanceConsiderationRadius = AvoidanceRadius;
		MovementComponent->AvoidanceWeight = 0.5f;
	}
}

void ARVO_NPC::BeginPlay()
{
	Super::BeginPlay();
	
	AIController = Cast<AAIController>(GetController());

	if (!AIController)
	{
    		return;
	}
	else if (TargetActor)
	{
		MoveToTarget();
	}
}

 

위처럼 구현하면 아래와 같이 좁은 구간에 있어서 AI들이 피해가며 원하는 목적지까지 움직인다.

  • 장애물은 Nav Modifier를 null로 설정

 

만약 RVO를 끈 경우 아래와 같이 좁은 길목에서 지나가지 못하는 상황이 생긴다.

 

 

4. 캐릭터 추적


 

 

5. 자동 네비게이션 링크


내비게이션 메시의 끊어진 부분을 자동으로 연결하여 AI 캐릭터의 경로 탐색을 향상시키는 기능

 

구현 방법

  • 레벨에 존재하는 RecastNavMesh에 Generate Nav Links 를 true로 체크

  • 추가적으로 GeneratedNavLinksProxy를 통해 기능을 추가해 줄 수 있다.
    • link를 만난 경우 Z 축으로 점프를 하는 기능을 구현했다.

  • Proxy 등록을 위해서는 아래의 화면에서 등록을 할 수 있다.
    • 추가적으로 Link의 값들을 조정하기 위해서 아래 값들을 수정할 수 있다.