Unreal Engine - AI (1)
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의 값들을 조정하기 위해서 아래 값들을 수정할 수 있다.