💭회고
오늘은 언리얼 엔진에서 AI가 동적으로 변화하는 환경에 자연스럽게 대응하는 방법을 살펴봤다. 이전 학습에서 다루었던 정적 장애물 회피와 달리, 이번에는 움직이는 장애물을 효과적으로 회피하는 RVO(Reciprocal Velocity Obstacles) 시스템을 구현했다. 이 기술은 복잡한 환경에서 여러 AI 캐릭터들이 자연스럽게 상호작용하는 게임을 개발할 때 필수적인 요소이다.
이번 학습을 통해 다음 세 가지를 중점적으로 다룬다:
- RVO 시스템의 개념과 작동 원리 이해
- 언리얼 엔진에서 RVO를 구현하는 방법과 핵심 코드
- RVO 파라미터 조정을 통한 AI 움직임 최적화
이 내용은 향후 더 복잡한 AI 행동 패턴을 설계하고, 플레이어 추적 같은 고급 기능을 구현하는 데 토대가 된다.
📒학습 내용
RVO란 무엇인가?
RVO(Reciprocal Velocity Obstacles)는 움직이는 객체들이 서로 충돌하지 않도록 속도와 방향을 동적으로 조절하는 알고리즘이다. 쉽게 말해, 사람들이 붐비는 거리에서 서로 부딪히지 않고 이동하는 방식을 컴퓨터로 구현한 것이다.
RVO는 실시간으로 다른 객체의 위치와 속도를 고려하여 최적의 회피 경로를 계산한다. 이는 NavMesh 기반의 정적 경로 탐색과는 다른 접근 방식이다.
정적 경로 탐색(Pathfinding)과 RVO의 차이
특성 | 정적 경로 탐색(Pathfinding) | RVO(동적 충돌 회피) |
회피 대상 | 고정된 장애물 | 움직이는 객체 |
작동 방식 | 미리 계산된 경로 따라가기 | 실시간 속도와 방향 조정 |
경로 재계산 | 필요함 | 불필요(방향만 조정) |
계산 비용 | 경로 변경 시 비용 높음 | 지속적이나 효율적 |
적합한 상황 | 정적 환경, 단일 객체 | 복잡한 다중 객체 환경 |
RVO 구현 핵심 코드
언리얼 엔진에서 RVO를 구현하는 가장 기본적인 방법은 CharacterMovementComponent의 RVO 관련 속성을 설정하는 것이다. 다음 세 줄의 코드가 RVO 구현의 핵심이다:
GetCharacterMovement()->bUseRVOAvoidance = true;
GetCharacterMovement()->AvoidanceConsiderationRadius = 200.0f;
GetCharacterMovement()->AvoidanceWeight = 0.5f;
각 코드의 의미를 자세히 살펴보자:
1. RVO 활성화 설정
GetCharacterMovement()->bUseRVOAvoidance = true;
이 코드는 캐릭터의 RVO 회피 시스템을 켜고 끄는 스위치와 같다. true로 설정하면 자동 회피 기능이 작동하고, false로 설정하면 회피 기능이 비활성화된다.
2. 회피 고려 반경 설정
GetCharacterMovement()->AvoidanceConsiderationRadius = 200.0f;
이 설정은 AI가 다른 객체를 감지하고 회피를 시작하는 거리를 결정한다. 이 값은 언리얼 유닛(UU)으로 측정되며, 1UU는 약 1cm에 해당한다.
3. 회피 가중치 설정
GetCharacterMovement()->AvoidanceWeight = 0.5f;
이 값은 0.0f에서 1.0f 사이로 설정하며, 여러 AI 간의 회피 우선순위를 결정한다. 값이 클수록 다른 AI들이 이 AI를 먼저 피하게 된다.
🔹 실무 팁: 회피 가중치를 설정할 때 캐릭터의 중요도나 크기에 따라 차등을 두는 것이 좋다. 예를 들어, 보스 몬스터는 0.9, 일반 몬스터는 0.5, NPC는 0.3 정도로 설정하면 자연스러운 계층 구조의 회피 행동을 구현할 수 있다.
RVO를 적용한 캐릭터 클래스 구현하기
이제 실제로 RVO를 적용한 캐릭터 클래스를 구현해보자. 먼저 헤더 파일(RVO_Character.h)을 작성한다:
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "RVO_Character.generated.h"
UCLASS()
class SCC_UEAI_API ARVO_Character : public ACharacter
{
GENERATED_BODY()
public:
// 기본값 설정
ARVO_Character();
// 게임 시작 시 호출
virtual void BeginPlay() override;
// 매 프레임 호출
virtual void Tick(float DeltaTime) override;
// 입력 바인딩
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
// 타겟 위치로 이동
UFUNCTION(BlueprintCallable, Category = "AI Movement")
void MoveToTarget();
// RVO 회피 활성화/비활성화
UFUNCTION(BlueprintCallable, Category = "RVO")
void SetRVOAvoidanceEnabled(bool bEnable);
public:
// 이동할 타겟 액터
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AI Movement")
AActor* TargetActor;
// RVO 회피 설정
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "RVO")
float AvoidanceRadius = 300.0f;
// RVO 계급 설정
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "RVO")
float AvoidanceWeight = 0.5f;
private:
// AI 컨트롤러 캐싱
class AAIController* AIController;
};
그리고 소스 파일(RVO_Character.cpp)을 구현한다:
#include "RVO_Character.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "AIController.h"
// 기본값 설정
ARVO_Character::ARVO_Character()
{
// 매 프레임 Tick 함수 호출 활성화
PrimaryActorTick.bCanEverTick = true;
// RVO 회피 시스템 활성화
UCharacterMovementComponent* MovementComponent = GetCharacterMovement();
if(MovementComponent)
{
MovementComponent->bUseRVOAvoidance = true;
MovementComponent->AvoidanceConsiderationRadius = AvoidanceRadius;
MovementComponent->AvoidanceWeight = AvoidanceWeight;
}
}
// 게임 시작 시 호출
void ARVO_Character::BeginPlay()
{
Super::BeginPlay();
// AI 컨트롤러 참조 얻기
AIController = Cast<AAIController>(GetController());
// AI 컨트롤러가 없으면 로그 출력
if(!AIController)
{
UE_LOG(LogTemp, Warning, TEXT("%s is not controlled by an AIController. Movement may not work correctly."), *GetName());
}
else if(TargetActor)
{
// 타겟 액터가 설정되어 있으면 자동으로 이동 시작
MoveToTarget();
}
}
// 매 프레임 호출
void ARVO_Character::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
// 입력 바인딩
void ARVO_Character::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
}
// 타겟 위치로 이동
void ARVO_Character::MoveToTarget()
{
if(!AIController)
{
UE_LOG(LogTemp, Warning, TEXT("MoveToTarget failed: No AI Controller for %s"), *GetName());
return;
}
if(!TargetActor)
{
UE_LOG(LogTemp, Warning, TEXT("MoveToTarget failed: No Target Actor set for %s"), *GetName());
return;
}
// 타겟 액터를 향해 이동
AIController->MoveToActor(
TargetActor, // 목표 액터
50.0f, // 도착 판정 반경
true, // 충돌 영역이 겹치면 도착으로 간주
true, // 경로 탐색 사용
false // 목적지를 네비게이션 메시에 투영하지 않음
);
UE_LOG(LogTemp, Display, TEXT("%s moving to target: %s"), *GetName(), *TargetActor->GetName());
}
// RVO 회피 활성화/비활성화
void ARVO_Character::SetRVOAvoidanceEnabled(bool bEnable)
{
UCharacterMovementComponent* MovementComponent = GetCharacterMovement();
if(MovementComponent)
{
MovementComponent->bUseRVOAvoidance = bEnable;
}
}
🔹 실무 팁: 초기화 단계에서 RVO 설정을 하는 것이 중요하지만, 게임 플레이 중에도 상황에 따라 동적으로 RVO 파라미터를 조절할 수 있다. 예를 들어, 전투 상황에서는 회피 반경을 줄이고, 평화로운 도시 환경에서는 회피 반경을 늘려 자연스러운 군중 움직임을 구현할 수 있다.
실제 RVO 테스트 환경 구성하기
RVO 시스템을 테스트하기 위한 환경을 다음과 같이 구성한다:
- 네비게이션 시스템 설정
- Project Settings → Navigation System → Generate Navigation Only Around Navigation Invokers를 False로 설정한다.
- 이 설정은 전체 레벨에 네비게이션 메시를 생성하여 AI가 어디서든 경로를 찾을 수 있게 한다.
- 테스트 환경 구성
- NavMeshBoundsVolume을 배치하여 AI가 이동할 수 있는 영역을 지정한다.
- Nav Modifier Volume을 사용해 좁은 통로나 병목 지점을 만든다.
- 반대편에 각각 Target Point를 배치하여 AI들이 서로 마주 보고 이동하도록 설정한다.
- AI 캐릭터 배치
- 구현한 RVO_Character 클래스를 상속받는 블루프린트를 생성한다.
- 이벤트 그래프에서 블루프린트를 통해 Debug Arrow를 세팅한다.
- 각 AI 캐릭터가 반대편의 타겟 포인트를 향해 이동하도록 Target Actor 속성을 설정한다.
- RVO 비교 테스트
- RVO를 활성화한 상태와 비활성화한 상태를 비교하기 위해 블루프린트의 BeginPlay 이벤트에서 SetRVOAvoidanceEnabled 함수를 호출하여 테스트한다.
RVO 테스트 결과 비교 ➡️
RVO 활성화 시: AI 캐릭터들이 병목 지점에서 서로의 경로가 겹치면, 속도와 방향을 조정하여 자연스럽게 회피하는 모습을 관찰할 수 있다. 마치 사람들이 복도에서 서로를 피하는 것처럼 행동한다.
RVO 비활성화 시: AI 캐릭터들이 경로상 동적 장애물을 인식하지 못하고, 서로 충돌하거나 막히는 현상이 발생한다. 결국 움직임이 멈추거나 부자연스러운 경로를 선택하게 된다.
RVO 시스템의 가장 큰 장점은 명시적인 경로 재계산 없이도 자연스러운 회피 행동을 만들어낸다는 점이다. 이는 성능 면에서도 매우 효율적이다.
RVO 파라미터 조정을 통한 최적화
RVO 시스템의 효과적인 활용을 위해 다음과 같은 주요 파라미터를 조정할 수 있다:
- AvoidanceConsiderationRadius: 회피를 시작하는 감지 거리
- 권장 범위: 200-500 UU
- 값이 작을수록: 반응이 늦지만 계산 비용 감소
- 값이 클수록: 부드러운 회피 가능하지만 계산 비용 증가
- AvoidanceWeight: 회피 우선순위
- 범위: 0.0f (낮은 우선순위) ~ 1.0f (높은 우선순위)
- 값이 높을수록 다른 AI가 이 캐릭터를 우선적으로 피함
- NPC의 중요도나 물리적 크기에 따라 설정하는 것이 자연스러움
🔹 실무 팁:
다양한 NPC가 있는 게임에서는 NPC의 특성에 맞게 회피 파라미터를 설정하는 것이 중요하다. 예를 들어:
군인/경비병: AvoidanceWeight = 0.8 (다른 NPC가 우선적으로 피함)
일반 시민: AvoidanceWeight = 0.5 (중간 수준의 우선순위)
어린이/동물: AvoidanceWeight = 0.2 (대부분의 NPC를 피해감)
이렇게 설정하면 게임 세계에서 사회적 계층 구조가 자연스럽게 반영된다.
RVO 시스템 활용을 위한 고급 팁
1. 성능 최적화
여러 AI가 동시에 존재하는 환경에서 RVO 계산이 성능에 영향을 줄 수 있다. 다음과 같은 최적화 방법을 고려한다:
- LOD(Level of Detail) 시스템 구현: 플레이어에서 멀리 떨어진 AI는 더 간단한 회피 로직을 사용하거나 RVO를 비활성화한다.
- AvoidanceConsiderationRadius 조정: 필요 이상으로 크게 설정하지 않는다.
- 그룹별 처리: 서로 영향을 주지 않는 AI 그룹은 별도로 처리한다.
2. 특수 상황 처리
RVO만으로는 모든 상황을 완벽하게 처리할 수 없다. 다음과 같은 특수 상황에 대한 추가 로직이 필요할 수 있다:
- 좁은 통로 처리: 일시적으로 회피 가중치를 조정하여 통행 우선순위를 정한다.
- 긴급 상황: 화재 탈출과 같은 패닉 상황에서는 다른 회피 규칙을 적용한다.
- 그룹 이동: 군중이 함께 움직이는 경우, 포메이션 유지와 RVO 간의 균형을 맞춘다.
🔹 실무 팁: 게임에서 군중 시뮬레이션을 구현할 때, 멀리 있는 AI는 저해상도 시뮬레이션(간단한 RVO)을, 가까이 있는 AI는 고해상도 시뮬레이션(세밀한 RVO 파라미터)을 적용하는 하이브리드 방식을 사용하면 성능과 시각적 품질 사이의 좋은 균형을 맞출 수 있다.
🟣오늘의 옵시디언 현황
'Dev. > UE 언리얼 엔진' 카테고리의 다른 글
[TIL_250502]AI NPC의 애니메이션 세팅 (0) | 2025.05.02 |
---|---|
[TIL_250424] 언리얼 엔진 AI Controller로 플레이어 자동 추적 구현하기 (0) | 2025.04.24 |
[TIL_250422] 언리얼 엔진 AI 이동 명령 및 경로 탐색(Pathfinding) (0) | 2025.04.22 |
[TIL_250421] UE5 AI 시스템 : Behavior Tree ~ Navigation Invoker (1) | 2025.04.21 |
[TIL_250417] 액터 이동 블루프린트 로직 (0) | 2025.04.17 |