← 모든 글

Unreal C++를 이용한 플랫폼 게임 4 - 상호작용과 UI, 게임 종료 흐름

플레이어가 키 오브젝트에 접근하고 입력을 유지하면 진행 UI를 갱신한 뒤 완료 레벨로 이동하고 게임을 종료하는 흐름을 정리했습니다.

Unreal Engine 5 C++Unreal Engine 5UMGOverlapEnhanced InputLevel TransitionUserWidget

충돌 감지에서 완료 화면까지 연결하기

발판 코스를 통과하는 것만으로는 게임의 끝이 분명하지 않다. 이번에는 마지막에 놓인 키 오브젝트에 접근해 상호작용 키를 일정 시간 누르면 완료 화면으로 넘어가도록 만들었다.

이 흐름에는 여러 클래스가 참여한다.

상호작용 대상 만들기

AKeyProp은 루트, 메시, 구체 충돌 컴포넌트로 구성된다.

SceneRoot = CreateDefaultSubobject<USceneComponent>(TEXT("SceneRoot"));
StaticMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("StaticMesh"));
SphereCollision = CreateDefaultSubobject<USphereComponent>(TEXT("SphereCollision"));

SetRootComponent(SceneRoot);
StaticMesh->SetupAttachment(SceneRoot);
SphereCollision->SetupAttachment(SceneRoot);

메시는 키의 외형을 보여주고, SphereCollision은 플레이어가 가까이 왔는지 판정할 영역을 만든다. 실제 상호작용 처리는 키가 아니라 플레이어 쪽에 있다. 키 클래스는 대상이라는 정보와 충돌 영역만 제공한다.

충돌 영역이 설정된 키 오브젝트

에디터에서는 키 메시를 감싸는 구체 형태의 충돌 영역을 확인할 수 있다. 이 영역은 키 자체의 물리 충돌을 처리하기 위한 것이 아니라, 플레이어가 상호작용할 수 있는 거리 안에 들어왔는지를 감지하기 위한 Trigger 역할을 한다. 메시보다 넓게 설정해 플레이어가 키에 정확히 닿지 않아도 가까이 접근하면 상호작용을 시작할 수 있도록 했다.

플레이어의 상호작용 범위

플레이어도 USphereComponent인 InteractionCollision을 가지고 있다. 생성자에서 겹침 시작과 종료 델리게이트를 함수에 연결한다.

플레이어 Blueprint의 Interaction Collision

플레이어 Blueprint에는 기본 이동 충돌을 담당하는 Capsule Component와 별도로 Interaction Collision이 붙어 있다. 화면의 붉은 구체가 상호작용 탐색 범위다. 이동용 Capsule과 상호작용 범위를 분리했기 때문에 캐릭터의 물리 충돌 크기를 바꾸지 않고도 아이템을 감지하는 거리만 독립적으로 조절할 수 있다.

InteractionCollision->OnComponentBeginOverlap.AddDynamic(
    this, &ATpsPlayer::OnOverlapBegin);

InteractionCollision->OnComponentEndOverlap.AddDynamic(
    this, &ATpsPlayer::OnOverlapEnd);

겹침이 시작되면 상대 Actor가 AKeyProp인지 확인한다.

TObjectPtr<AKeyProp> Key = Cast<AKeyProp>(OtherActor);
if (nullptr == Key)
{
    return;
}

KeyProp = Key;

MainUiInstance->SetVisibilities(
    ESlateVisibility::Visible,
    ESlateVisibility::Hidden,
    ESlateVisibility::Hidden);

캐스팅에 성공하면 현재 상호작용 대상을 KeyProp에 저장하고 안내 UI를 표시한다. 다른 Actor와 겹쳤다면 캐스팅이 실패하므로 아무 일도 일어나지 않는다.

키와 겹쳤을 때 표시되는 상호작용 안내

플레이어와 키의 상호작용 충돌 영역이 겹치면 화면에 Press 'E' And Hold 안내가 나타난다. 안내 문구는 항상 표시되는 것이 아니라 유효한 AKeyProp을 감지한 동안에만 보인다. 따라서 플레이어는 현재 상호작용이 가능한지 즉시 알 수 있고, 키에서 멀어지면 Overlap End 이벤트를 통해 다시 숨겨진다.

겹침이 끝나면 대상을 비우고 모든 안내 UI를 숨긴다.

KeyProp = nullptr;

MainUiInstance->SetVisibilities(
    ESlateVisibility::Hidden,
    ESlateVisibility::Hidden,
    ESlateVisibility::Hidden);

C++과 Blueprint UI 연결하기

UMainUI는 실제 위젯 모양을 C++에서 만들지 않는다. 대신 Blueprint가 구현할 이벤트를 선언한다.

UFUNCTION(BlueprintCallable, BlueprintImplementableEvent, Category="MainUI")
void SetVisibilities(
    ESlateVisibility InAlertVisibility,
    ESlateVisibility InProgressBarVisibility,
    ESlateVisibility InCongratulationVisibility);

UFUNCTION(BlueprintCallable, BlueprintImplementableEvent, Category="MainUI")
void SetProgressBarPercent(float InPercent);

BlueprintImplementableEvent에는 C++ 함수 본문이 없다. C++은 어떤 상태를 표시할지만 전달하고, 텍스트나 프로그레스 바를 실제로 보이거나 숨기는 일은 Widget Blueprint가 담당한다.

플레이어는 BeginPlay()에서 위젯을 생성해 화면에 추가한다.

MainUiInstance = CreateWidget<UMainUI>(GetWorld(), MainUiClass);
if (nullptr == MainUiInstance)
{
    UE_LOG(LogTemp, Error, TEXT("MainUiInstance is nullptr."));
    return;
}

MainUiInstance->AddToViewport();

MainUiClass는 TSubclassOf<UMainUI>이므로 에디터에서 C++ 클래스를 상속한 Widget Blueprint를 지정할 수 있다.

입력을 누른 시간으로 진행률 계산하기

상호작용 액션은 Ongoing과 Canceled 이벤트에 연결된다.

EnhancedInput->BindAction(
    CastedController->InteractAction,
    ETriggerEvent::Ongoing,
    this,
    &ATpsPlayer::OnInteractOngoing);

EnhancedInput->BindAction(
    CastedController->InteractAction,
    ETriggerEvent::Canceled,
    this,
    &ATpsPlayer::OnInteractCanceled);

키를 누르고 있는 동안 FInputActionInstance::GetElapsedTime()으로 액션이 진행된 시간을 얻는다. 목표 시간으로 나눈 값이 프로그레스 바의 비율이 된다.

float ElapsedTime = Value.GetElapsedTime();
float Percent = ElapsedTime / InteractionTargetElapsedTime;

MainUiInstance->SetVisibilities(
    ESlateVisibility::Hidden,
    ESlateVisibility::Visible,
    ESlateVisibility::Hidden);

MainUiInstance->SetProgressBarPercent(Percent);

계산 전에 KeyProp이 있는지 검사하므로 키 가까이에 있을 때만 진행된다. 또한 목표 시간이 0이면 나눗셈을 할 수 없으므로 FMath::IsNearlyZero()로 방어한다.

입력을 목표 시간까지 유지하면 IsFinished를 확인한 뒤 완료 레벨을 연다.

if (Percent >= 1.0f && !IsFinished)
{
    IsFinished = true;

    if (CongratulationLevel.IsNull())
    {
        UE_LOG(LogTemp, Error, TEXT("CongratulationLevel is null"));
        return;
    }

    UGameplayStatics::OpenLevelBySoftObjectPtr(
        this, CongratulationLevel);
}

IsFinished는 Ongoing 이벤트가 여러 번 들어와 레벨 전환을 반복 요청하지 않도록 막는다. 완료 레벨은 TSoftObjectPtr<UWorld>로 참조해 필요할 때 연다.

입력을 중간에 놓으면 OnInteractCanceled()가 실행되고, 완료 전이라면 프로그레스 바를 숨긴 뒤 다시 안내 문구를 보여준다.

프로그레스 바와 상호작용 결과

영상에서는 플레이어가 키에 접근했을 때 안내 문구가 나타나고, 상호작용 키를 누르고 있는 동안 프로그레스 바가 채워지는 과정을 확인할 수 있다. 진행률은 미리 만든 타이머를 따로 증가시키는 방식이 아니라 Enhanced Input Action의 경과 시간을 목표 시간으로 나눈 값이므로, 실제 입력 유지 시간과 UI가 같은 기준으로 움직인다.

입력을 중간에 놓으면 진행이 취소되고 안내 상태로 돌아간다. 반대로 프로그레스 바가 끝까지 차면 IsFinished가 설정되면서 완료 레벨을 한 번만 열고, 이후 완료 UI가 표시된다. 이 결과를 통해 Overlap 감지, 입력 유지 시간, UMG 프로그레스 바, 레벨 전환이 하나의 상호작용 흐름으로 연결된 것을 확인했다.

완료 레벨에서 UI 입력으로 전환하기

완료 레벨에는 ACongratulations Actor가 배치된다. 이 Actor는 BeginPlay()에서 완료 위젯을 생성한다.

CongratulationsWidgetInstance = CreateWidget<UCongratulationsWidget>(
    GetWorld(), CongratulationsWidgetClass);

CongratulationsWidgetInstance->AddToViewport();

완료 화면에서는 캐릭터를 움직이는 대신 UI를 클릭해야 한다. 따라서 첫 번째 Player Controller에서 마우스 커서를 표시하고 입력 모드를 UI 전용으로 변경한다.

APlayerController* PlayerController =
    GetWorld()->GetFirstPlayerController();

PlayerController->bShowMouseCursor = true;

FInputModeUIOnly InputMode;
PlayerController->SetInputMode(InputMode);

이제 키보드와 마우스 입력은 플레이용 Pawn보다 UI에 우선 전달된다.

완료 위젯을 클릭해 게임 종료하기

UCongratulationsWidget은 마우스 버튼 입력을 오버라이드한다.

FReply UCongratulationsWidget::NativeOnMouseButtonDown(
    const FGeometry& InGeometry,
    const FPointerEvent& InMouseEvent)
{
    Super::NativeOnMouseButtonDown(InGeometry, InMouseEvent);

    UKismetSystemLibrary::QuitGame(
        this, nullptr, EQuitPreference::Quit, false);

    return Super::NativeOnMouseButtonDown(InGeometry, InMouseEvent);
}

완료 위젯을 클릭하면 QuitGame()을 호출해 실행 중인 게임을 종료한다. 현재 구현에서는 위젯 전체가 종료 버튼처럼 동작한다.

전체 흐름 정리

상호작용은 한 클래스 안에서 끝나지 않고 다음 순서로 이어진다.

키 접촉부터 상호작용과 게임 종료까지의 순서도

  1. 플레이어의 충돌 범위가 AKeyProp과 겹친다.
  2. 안내 UI를 표시한다.
  3. 상호작용 입력을 누르는 동안 경과 시간을 진행률로 변환한다.
  4. 목표 시간에 도달하면 완료 레벨을 연다.
  5. 완료 레벨이 위젯을 띄우고 입력을 UI 전용으로 바꾼다.
  6. 완료 위젯을 클릭하면 게임을 종료한다.

충돌, 입력, UI 표현, 레벨 이동, 종료의 책임은 각각 나뉘어 있지만 이벤트와 함수 호출을 통해 하나의 플레이 흐름을 만든다. 특히 C++은 상태와 규칙을 담당하고 Widget Blueprint는 화면 표현을 담당하도록 연결한 점이 이번 구현의 핵심이다.