
오늘은 AI에게 포인트 2개를 지정해 해당 포인트 사이를 가장 효율적인(저비용)의 경로로 이동할 수 있도록 로직을 구현해 보았다.

먼저 위 사진과 같이 NavMesh 상에 NavModifierVolume을 배치하여 경로를 우회 할 수 있는 환경을 만들어 주었다.

NavModifierVolume 메시에서 모빌리티를 '무버블'로 선택하고 디폴트의 영역 클래스를 NavArea_Obstacle로 설정해 주었다.

위 사진 처럼 NavModifierVolume가 배치된 곳을 NavMesh 상에 빨갛게 표시가 되어 해당 구역은 주변 대비 이동 비용이 높다는 것을 의미하게 된다.

타겟 포인트를 배치하여 AI가 이동해야할 지점을 설정 해준다.

위와 같이 타겟 포인트 중간에 이동 비용이 높은 구역을 NavModifierVolume로 만들어주어 AI가 해당 구역을 제외한 구역 기준으로 최적의 경로를 계산하여 이동한다.
CPP로 해당 NPC를 만드는 캐릭터 클래스에 타겟 포인트를 기준으로 이동하게 로직을 만들어 준다.
// AI Modifier 테스트 관련 변수 및 함수
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AI Movement")
bool bIsSucceeded;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AI Movement")
AActor* Target;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AI Movement")
AActor* Target2;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AI Movement")
float AcceptanceRadius = 50.0f;
UFUNCTION(BlueprintCallable, Category = "AI Movement")
void MoveToTarget();
UFUNCTION()
void OnMoveCompleted(FAIRequestID RequestID, EPathFollowingResult::Type Result);
UFUNCTION(BlueprintCallable, Category = "AI Movement")
void StartMoving();
UFUNCTION(BlueprintCallable, Category = "AI Movement")
void FindTargetPoints();
언리얼엔진 3인칭 템플릿에서 제공되는 기본 캐릭터를 기반으로 해당 CPP파일에 내용을 추가한 것이다.
헤더 파일에는 해당 로직을 구현하기 위해 필요한 변수들, 타겟 포인트, 이동 로직 함수 등을 선언하였고 소스 파일에는 캐릭터가 AI일 때(즉, 플레이어의 캐릭터가 아닐 때), AIController로 전환해 타겟 포인트를 자동으로 선정하고 해당 지점을 왕복하게 되는 로직을 구현하였다.
void ASCC_UEAI_LectureCharacter::BeginPlay()
{
Super::BeginPlay();
// AI 컨트롤러 찾기
AIController = Cast<AAIController>(GetController());
// AI 컨트롤러가 없으면 자동으로 생성하지 않음(필요시 생성 코드 추가)
if (AIController)
{
// 디버깅 에러 방지를 위해 언바인딩 코드 실행
AIController->ReceiveMoveCompleted.RemoveDynamic(this, &ASCC_UEAI_LectureCharacter::OnMoveCompleted);
// 이동 완료 이벤트 델리게이트 바인딩
AIController->ReceiveMoveCompleted.AddDynamic(this, &ASCC_UEAI_LectureCharacter::OnMoveCompleted);
// 타겟 포인트 찾기
FindTargetPoints();
StartMoving();
}
}
// AI Modifier Test 인풋 로직 구현
void ASCC_UEAI_LectureCharacter::NotifyControllerChanged()
{
Super::NotifyControllerChanged();
//Add Input Mapping Context
if (APlayerController* PlayerController = Cast<APlayerController>(Controller))
{
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
{
Subsystem->AddMappingContext(DefaultMappingContext, 0);
}
}
else // Input Mapping Context가 없는 경우(AI인 경우) 컨트롤러 추가
{
// 컨트롤러가 변경되었고 AI 컨트롤러인 경우
AIController = Cast<AAIController>(Controller);
if (AIController)
{
AIController->ReceiveMoveCompleted.AddDynamic(this, &ASCC_UEAI_LectureCharacter::OnMoveCompleted);
}
}
}
// AI Modifier 테스트용 Movement Logic 구현
void ASCC_UEAI_LectureCharacter::FindTargetPoints()
{
// 타겟 포인트가 설정되어 있지 아닣는 경우 자동으로 찾기
// "||"는 A or B 로 A 혹은 B가 True일 경우 True를 반환하는 논리 연산자
// 여기서는 역논리 연산자가 bool 변수 앞에 붙어있으므로 bool변수가 하나라도 false 일 경우 True로 판정
if (!Target || !Target2)
{
TArray<AActor*> FoundTargets;
UGameplayStatics::GetAllActorsOfClass(GetWorld(), ATargetPoint::StaticClass(), FoundTargets);
if (FoundTargets.Num() >= 2)
{
Target = FoundTargets[0];
Target2 = FoundTargets[1];
UE_LOG(LogTemplateCharacter, Display, TEXT("Found Target Points: %s, %s"), *Target->GetName(), *Target2->GetName());
}
else
{
UE_LOG(LogTemplateCharacter, Warning, TEXT("Not enough TargetPoints found in the level, need at least 2!"));
}
}
}
void ASCC_UEAI_LectureCharacter::StartMoving()
{
// 타겟 포인트를 찾고 이동 시작
FindTargetPoints();
MoveToTarget();
}
void ASCC_UEAI_LectureCharacter::MoveToTarget()
{
if (!AIController)
{
UE_LOG(LogTemplateCharacter, Error, TEXT("AIController is not valid! Make sure the character is possessed by an AIController"));
return;
}
if (bIsMoving)
{
// 이미 이동 중이면 중복 호출 방지
return;
}
// IsSucceeded 값에 따라 타겟 선택
AActor* SelectedTarget = bIsSucceeded ? Target : Target2;
if (SelectedTarget)
{
bIsMoving = true;
// AI MoveTo 함수 호출
FVector TargetLocation = SelectedTarget->GetActorLocation();
EPathFollowingRequestResult::Type MoveResult = AIController->MoveToLocation(
TargetLocation,
AcceptanceRadius,
true, // 목적지에 오버랩 되면 도착으로 판정할지 여부
true, // 경로 찾기 사용
false, // 프로젝션 사용 안 함
true // 네비게이션 데이터 사용
);
if (MoveResult == EPathFollowingRequestResult::Failed)
{
UE_LOG(LogTemplateCharacter, Warning, TEXT("Faild to start movement to target!"));
bIsMoving = false;
}
else
{
UE_LOG(LogTemplateCharacter, Display, TEXT("Moving to %s (IsSucceded: %s"),
*SelectedTarget->GetName(), bIsSucceeded ? TEXT("True") : TEXT("False"));
}
}
else
{
UE_LOG(LogTemplateCharacter, Error, TEXT("Selected target is not valid! Make sure Target and Target2 are set."));
}
}
void ASCC_UEAI_LectureCharacter::OnMoveCompleted(FAIRequestID RequestID, EPathFollowingResult::Type Result)
{
bIsMoving = false;
// 이동 결과에 따라 IsSucceeded 값 토글
if (Result == EPathFollowingResult::Success)
{
// 성공적으로 이동 완료됨
bIsSucceeded = !bIsSucceeded; // 값 토글
UE_LOG(LogTemplateCharacter, Display, TEXT("Move completed, successfully. IsSucceeded toggled to: %s"),
bIsSucceeded ? TEXT("True") : TEXT("False"));
// 지연 후 다음 이동 시작
FTimerHandle TimerHandle;
GetWorldTimerManager().SetTimer(TimerHandle, this, &ASCC_UEAI_LectureCharacter::MoveToTarget, 0.5f, false);
}
else
{
// 이동 실패
UE_LOG(LogTemplateCharacter, Warning, TEXT("Move faild with result: %d"), static_cast<int32>(Result));
// 실패 시에도 다시 시도
FTimerHandle TimerHandle;
GetWorldTimerManager().SetTimer(TimerHandle, this, &ASCC_UEAI_LectureCharacter::MoveToTarget, 1.0f, false);
}
}
해당 기능을 사용하여 다음 프로젝트에서 마을을 구현할 때 NPC에 활용하면 좋을 것 같다.
(예. 이동하는 상인, 마을에 돌아다니는 시민, 마을을 순찰 중인 경비병 등)