안동민 개발노트 아이콘

안동민 개발노트

6장 : UI 개발 (UMG)

C++에서 위젯 생성 및 제어

이전 절에서 우리는 UMG 시스템의 개요와 위젯의 계층적 구조에 대해 알아보았습니다. UMG는 시각적 디자인을 위해 블루프린트를 주로 사용하지만, 실제 게임 개발에서는 C++ 코드에서 위젯을 생성하고 제어해야 하는 경우가 많습니다. 특히 게임의 핵심 UI 관리 로직, 성능이 중요한 업데이트, 또는 복잡한 데이터 바인딩 등은 C++로 구현하는 것이 효율적입니다.

이번 절에서는 C++ 코드에서 UMG 위젯을 생성하고, 화면에 추가하며, 속성을 제어하고, 제거하는 방법에 대해 자세히 살펴보겠습니다.


C++에서 위젯 생성의 필요성

대부분의 UI는 위젯 블루프린트(.uasset)로 존재합니다. C++에서 이러한 위젯 블루프린트 인스턴스를 생성하고 화면에 띄우기 위해서는 다음 단계를 거쳐야 합니다.

  • 동적 생성: 게임 플레이 중 특정 조건(예: 게임 시작 시 HUD 표시, 아이템 획득 시 알림 팝업)에 따라 UI를 동적으로 생성해야 할 때.
  • 중앙 집중식 관리: APlayerControllerAHUD 클래스에서 모든 UI 위젯의 생명주기를 관리하고 싶을 때.
  • 데이터 주입: C++의 게임플레이 데이터를 UI 위젯에 전달하여 표시해야 할 때.
  • 성능 최적화: 매우 빈번하게 업데이트되거나 복잡한 연산이 필요한 UI 로직을 C++로 구현할 때.

위젯 블루프린트 클래스 참조

C++에서 위젯 블루프린트 인스턴스를 생성하려면, 먼저 해당 위젯 블루프린트의 클래스(Class)를 참조해야 합니다. 이는 UPROPERTY(EditDefaultsOnly)TSubclassOf<UUserWidget>를 사용하여 수행하는 것이 가장 일반적입니다.

AMyPlayerController.h (혹은 AMyHUD.h, AMyGameModeBase.h)
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "MyPlayerController.generated.h"

// UUserWidget을 사용하기 위해 포함
class UUserWidget;

UCLASS()
class MYPROJECT_API AMyPlayerController : public APlayerController
{
    GENERATED_BODY()

public:
    AMyPlayerController();

protected:
    virtual void BeginPlay() override;

    // 블루프린트에서 할당할 위젯 블루프린트 클래스에 대한 참조
    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI")
    TSubclassOf<UUserWidget> HUDWidgetClass; // 예를 들어, BP_HUD_Main 위젯 블루프린트 클래스 할당

    // 생성된 위젯 인스턴스에 대한 포인터
    UPROPERTY()
    UUserWidget* CurrentHUDWidget;

    // 인벤토리 위젯 클래스
    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI")
    TSubclassOf<UUserWidget> InventoryWidgetClass;

    UPROPERTY()
    UUserWidget* CurrentInventoryWidget;

public:
    // HUD를 화면에 표시하는 함수
    UFUNCTION(BlueprintCallable, Category = "UI")
    void ShowHUD();

    // HUD를 화면에서 제거하는 함수
    UFUNCTION(BlueprintCallable, Category = "UI")
    void HideHUD();

    // 인벤토리 위젯을 토글하는 함수
    UFUNCTION(BlueprintCallable, Category = "UI")
    void ToggleInventory();
};
  • TSubclassOf<UUserWidget> HUDWidgetClass;: 이 변수는 에디터에서 UMG 위젯 블루프린트 에셋을 할당할 수 있도록 해줍니다. TSubclassOf는 특정 C++ 클래스(여기서는 UUserWidget)를 상속받는 모든 블루프린트 클래스를 드롭다운 목록으로 보여줍니다.

위젯 인스턴스 생성 및 화면에 추가하기

C++에서 위젯 생성 및 제어에서 리플렉션 경계, 객체 수명, 엔진 호출을 정리한 것입니다.

CreateWidget() 함수를 사용하여 위젯 인스턴스를 생성하고, AddToViewport() 또는 AddToPlayerScreen() 함수를 사용하여 화면에 표시합니다.

AMyPlayerController.cpp
#include "MyPlayerController.h"
#include "Blueprint/UserWidget.h" // CreateWidget 함수를 위해 포함

AMyPlayerController::AMyPlayerController()
{
    // ...
}

void AMyPlayerController::BeginPlay()
{
    Super::BeginPlay();

    // 게임 시작 시 HUD를 자동으로 표시 (예시)
    ShowHUD();
}

void AMyPlayerController::ShowHUD()
{
    // 1. HUDWidgetClass가 유효한지 확인
    if (HUDWidgetClass)
    {
        // 2. 이미 HUD가 생성되어 있다면 다시 생성하지 않음
        if (CurrentHUDWidget)
        {
            // 이미 존재한다면, 다시 보이게만 할 수도 있습니다.
            CurrentHUDWidget->SetVisibility(ESlateVisibility::Visible);
            return;
        }

        // 3. 위젯 인스턴스 생성
        // CreateWidget(WorldContextObject, 위젯클래스, 플레이어컨트롤러)
        // 월드 컨텍스트는 현재 액터(PlayerController) 자신을 사용
        // 이 위젯을 소유할 PlayerController 지정
        CurrentHUDWidget = CreateWidget<UUserWidget>(this, HUDWidgetClass, TEXT("MainHUD")); // 이름 부여 (선택 사항)

        // 4. 위젯이 성공적으로 생성되었는지 확인
        if (CurrentHUDWidget)
        {
            // 5. 위젯을 뷰포트에 추가하여 화면에 표시
            CurrentHUDWidget->AddToViewport();
            UE_LOG(LogTemp, Warning, TEXT("HUD Widget spawned and added to viewport."));
        }
        else
        {
            UE_LOG(LogTemp, Error, TEXT("Failed to create HUD Widget."));
        }
    }
    else
    {
        UE_LOG(LogTemp, Warning, TEXT("HUDWidgetClass is not set in PlayerController."));
    }
}

void AMyPlayerController::HideHUD()
{
    if (CurrentHUDWidget)
    {
        // 뷰포트에서 위젯 제거
        CurrentHUDWidget->RemoveFromParent();
        // 포인터를 nullptr로 초기화하여 다시 생성할 수 있도록 함
        CurrentHUDWidget = nullptr;
        UE_LOG(LogTemp, Warning, TEXT("HUD Widget removed from viewport."));
    }
}

void AMyPlayerController::ToggleInventory()
{
    if (InventoryWidgetClass)
    {
        if (CurrentInventoryWidget && CurrentInventoryWidget->IsInViewport())
        {
            // 인벤토리 위젯이 이미 표시되어 있다면 제거
            CurrentInventoryWidget->RemoveFromParent();
            UE_LOG(LogTemp, Warning, TEXT("Inventory Widget removed."));

            // 게임플레이 입력 활성화 (UI 열기 전 상태로 복귀)
            SetInputMode(FInputModeGameOnly());
            bShowMouseCursor = false;
        }
        else
        {
            // 인벤토리 위젯이 없다면 생성하고 추가
            if (!CurrentInventoryWidget)
            {
                CurrentInventoryWidget = CreateWidget<UUserWidget>(this, InventoryWidgetClass, TEXT("PlayerInventory"));
            }

            if (CurrentInventoryWidget)
            {
                CurrentInventoryWidget->AddToViewport();
                UE_LOG(LogTemp, Warning, TEXT("Inventory Widget added."));

                // UI와 상호작용할 수 있도록 입력 모드 변경
                SetInputMode(FInputModeUIOnly()); // UI 전용 입력 모드
                // SetInputMode(FInputModeGameAndUI()); // 게임과 UI 모두 입력 가능
                bShowMouseCursor = true; // 마우스 커서 표시
            }
        }
    }
    else
    {
        UE_LOG(LogTemp, Warning, TEXT("InventoryWidgetClass is not set in PlayerController."));
    }
}
핵심 함수
  • CreateWidget<T>(WorldContextObject, WidgetClass, WidgetName):
  • T: 생성할 위젯의 C++ 타입 (UUserWidget 또는 이를 상속받는 커스텀 C++ 위젯 클래스).
  • WorldContextObject: 월드 컨텍스트를 제공하는 UObject (예: APlayerController, APawn, AGameModeBase 등). 주로 위젯을 소유할 APlayerController를 사용합니다.
  • WidgetClass: 생성할 위젯 블루프린트의 TSubclassOf 변수.
  • WidgetName: (선택 사항) 생성된 위젯 인스턴스에 부여할 이름.
  • UUserWidget::AddToViewport(ZOrder = 0): 위젯을 현재 플레이어의 뷰포트에 추가하여 화면에 표시합니다. ZOrder는 위젯이 겹칠 때의 순서를 결정하며, 숫자가 높을수록 위에 표시됩니다.
  • UUserWidget::AddToPlayerScreen(ZOrder = 0): AddToViewport와 유사하지만, 특정 로컬 플레이어 스크린에 추가하는 데 사용됩니다.
  • UUserWidget::RemoveFromParent(): 위젯을 화면에서 제거하고 부모 위젯(뷰포트 또는 다른 패널 위젯)으로부터 분리합니다. 이 함수를 호출하면 위젯은 더 이상 렌더링되지 않지만, 메모리에서는 즉시 해제되지 않습니다. 언리얼 엔진의 가비지 컬렉터가 이후에 이를 처리합니다.
  • APlayerController::SetInputMode(): 플레이어의 입력 모드를 전환합니다.
  • FInputModeGameOnly(): 오직 게임 월드 입력만 받습니다 (마우스 커서 숨김).
  • FInputModeUIOnly(): 오직 UI 입력만 받습니다 (마우스 커서 표시, 게임 월드 입력 비활성화).
  • FInputModeGameAndUI(): 게임과 UI 입력 모두 받습니다 (마우스 커서 표시, UI 클릭 시 게임 입력 비활성화).
  • bShowMouseCursor: 마우스 커서의 가시성을 제어합니다.

생성된 위젯 인스턴스 제어 및 데이터 바인딩

생성된 UUserWidget* CurrentHUDWidget 포인터를 통해 해당 위젯 인스턴스의 함수를 호출하거나 속성에 접근할 수 있습니다. 특히 C++ 기반의 커스텀 UUserWidget 클래스를 사용하고 meta=(BindWidget)으로 자식 위젯을 노출했다면, C++에서 직접 자식 위젯의 속성을 제어할 수 있습니다.

AMyPlayerController.cpp (HUD 위젯의 체력 업데이트)
#include "MyUserWidget.h" // 커스텀 HUD 위젯 클래스 포함

// ... 기존 코드 ...

void AMyPlayerController::PlayerTick(float DeltaTime)
{
    Super::PlayerTick(DeltaTime);

    // 예시: 매 틱마다 HUD의 체력을 업데이트 (실제로는 체력 변동 시에만 호출하는 것이 효율적)
    if (CurrentHUDWidget)
    {
        UMyUserWidget* MyHUD = Cast<UMyUserWidget>(CurrentHUDWidget);
        if (MyHUD)
        {
            // 플레이어 캐릭터의 현재 체력과 최대 체력을 가져와서 HUD에 전달 (예시)
            // AMyCharacter* MyCharacter = Cast<AMyCharacter>(GetPawn());
            // if (MyCharacter)
            // {
            //     MyHUD->UpdateHealth(MyCharacter->GetCurrentHealth(), MyCharacter->GetMaxHealth());
            // }
        }
    }
}

이처럼 Cast<UMyUserWidget>(CurrentHUDWidget)를 통해 위젯 인스턴스를 해당 C++ 위젯 클래스로 캐스팅한 후, 그 클래스에 정의된 UFUNCTION(BlueprintCallable) 함수나 UPROPERTY(BlueprintReadWrite) 변수에 접근하여 UI를 동적으로 업데이트할 수 있습니다.


위젯 생명주기 관리

위젯의 생명주기는 일반적으로 CreateWidget (생성) -> AddToViewport (활성화/표시) -> RemoveFromParent (비활성화/숨김/제거) 순으로 관리됩니다.

  • NativeConstruct() / NativeDestruct(): C++에서 위젯이 생성되고 제거될 때 호출되는 가상 함수입니다. 블루프린트의 Event Construct / Event Destruct와 유사하며, C++에서 초기화 및 정리 로직을 구현할 때 오버라이드합니다.
UMyUserWidget.cpp
void UMyUserWidget::NativeConstruct()
{
    Super::NativeConstruct();
    // 위젯 초기화 로직 (예: 초기 텍스트 설정, 애니메이션 시작)
    UE_LOG(LogTemp, Warning, TEXT("MyUserWidget NativeConstruct called!"));
}

void UMyUserWidget::NativeDestruct()
{
    Super::NativeDestruct();
    // 위젯 정리 로직 (예: 타이머 해제, 이벤트 바인딩 해제)
    UE_LOG(LogTemp, Warning, TEXT("MyUserWidget NativeDestruct called!"));
}

위젯을 만들고 화면에 붙이는 코드는 단순해 보이지만, 중복 생성과 입력 모드 복구를 함께 관리해야 안정적인 UI 관리자가 됩니다.

위젯을 열고 닫는 코드는 표시 여부뿐 아니라 입력 모드와 인스턴스 재사용 정책까지 함께 결정해야 합니다.

UI가 중복 생성되거나 닫은 뒤 입력이 꼬이는 문제는 위젯 클래스, 인스턴스 포인터, 화면 부착 여부, 입력 복구를 같은 흐름에서 검사하면 단순해집니다.


C++에서 UMG 위젯을 생성하고 제어하는 방법을 이해하는 것은 언리얼 엔진 UI 개발에 있어 필수적인 역량입니다. TSubclassOf를 통한 클래스 참조, CreateWidget()을 통한 인스턴스 생성, AddToViewport()/RemoveFromParent()를 통한 화면 관리, 그리고 SetInputMode()를 통한 입력 처리까지, 이 모든 기능들은 견고하고 효율적인 게임 UI를 구축하는 데 중요한 역할을 합니다.

C++에서 위젯 생성 및 제어은 호출 경계, 소유권, 성능 영향, 재측정 기준으로 점검합니다.