안동민 개발노트

본문 시작

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

C++에서 Widget Blueprint 클래스를 참조하고 CreateWidget로 생성해 뷰포트와 데이터를 제어합니다.

이전 절에서 우리는 UMG 시스템의 개요와 위젯의 계층적 구조에 대해 알아보았습니다.

UMG는 시각적 디자인을 위해 블루프린트를 주로 사용하지만, 실제 게임 개발에서는 C++ 코드에서 위젯을 생성하고 제어해야 하는 경우가 많습니다.

특히 게임의 핵심 UI 관리 로직, 성능이 중요한 업데이트, 또는 복잡한 데이터 바인딩 등은 C++로 구현하는 것이 효율적입니다.

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


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

대부분의 UI는 위젯 블루프린트(.uasset)로 존재합니다.

C++에서 이러한 위젯 블루프린트 인스턴스를 생성하고 화면에 띄우기 위해서는 다음 단계를 거쳐야 합니다.

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

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

C++에서 위젯 블루프린트 인스턴스를 생성하려면, 먼저 해당 위젯 블루프린트의 클래스(Class)를 참조해야 합니다.

이는 UPROPERTY(EditDefaultsOnly)와 TSubclassOf<UUserWidget>를 사용하여 수행하는 것이 가장 일반적입니다.

MyPlayerController.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();

    virtual void PlayerTick(float DeltaTime) override;
};
  • TSubclassOf<UUserWidget> HUDWidgetClass;: 이 변수는 에디터에서 UMG 위젯 블루프린트 에셋을 할당할 수 있도록 해줍니다. TSubclassOf는 특정 C++ 클래스(여기서는 UUserWidget)를 상속받는 모든 블루프린트 클래스를 드롭다운 목록으로 보여줍니다.

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

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

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

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

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

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

void AMyPlayerController::ShowHUD()
{
    if (!IsLocalController()) return;
    // 1. HUDWidgetClass가 유효한지 확인
    if (HUDWidgetClass)
    {
        // 2. 이미 HUD가 생성되어 있다면 다시 생성하지 않음
        if (CurrentHUDWidget)
        {
            // 화면에서 분리된 인스턴스는 다시 붙인 뒤 표시합니다.
            if (!CurrentHUDWidget->IsInViewport()) CurrentHUDWidget->AddToViewport();
            CurrentHUDWidget->SetVisibility(ESlateVisibility::Visible);
            return;
        }

        // 3. 위젯 인스턴스 생성
        // CreateWidget(소유 객체, 위젯 클래스, 선택적 인스턴스 이름)
        // 여기서는 로컬 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 (!IsLocalController()) return;
    if (InventoryWidgetClass)
    {
        if (CurrentInventoryWidget && CurrentInventoryWidget->IsInViewport())
        {
            // 인벤토리 위젯이 이미 표시되어 있다면 제거
            CurrentInventoryWidget->RemoveFromParent();
            UE_LOG(LogTemp, Warning, TEXT("Inventory Widget removed."));

            // 이 예제의 기본 게임 입력 모드로 복귀 (임의의 이전 상태 복원은 아님)
            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>(OwningObject, WidgetClass, WidgetName):
  • T: 생성할 위젯의 C++ 타입 (UUserWidget 또는 이를 상속받는 커스텀 C++ 위젯 클래스).
  • OwningObject: C++ 템플릿이 지원하는 소유 타입을 사용합니다. 이 예제는 로컬 APlayerController를 넘기며, 임의의 APawn이나 AGameModeBase를 그대로 넘기는 호출은 지원되지 않습니다.
  • WidgetClass: 생성할 위젯 블루프린트의 TSubclassOf 변수.
  • WidgetName: (선택 사항) 생성된 위젯 인스턴스에 부여할 이름.
  • UUserWidget::AddToViewport(ZOrder = 0): 게임 뷰포트에 추가하여 화면에 표시합니다. 분할 화면의 특정 플레이어 영역은 AddToPlayerScreen을 사용합니다. ZOrder는 위젯이 겹칠 때의 순서를 결정하며, 숫자가 높을수록 위에 표시됩니다.
  • UUserWidget::AddToPlayerScreen(ZOrder = 0): AddToViewport와 유사하지만, 특정 로컬 플레이어 스크린에 추가하는 데 사용됩니다.
  • UUserWidget::RemoveFromParent(): 위젯을 화면에서 제거하고 부모 위젯(뷰포트 또는 다른 패널 위젯)으로부터 분리합니다. 이 함수를 호출하면 위젯은 더 이상 렌더링되지 않지만, 메모리에서는 즉시 해제되지 않습니다. 이 예제의 UPROPERTY 포인터처럼 강한 참조가 남으면 인스턴스는 유지됩니다. 도달 가능한 참조가 없어져야 GC 대상이 됩니다.
  • APlayerController::SetInputMode(): 플레이어의 입력 모드를 전환합니다.
  • FInputModeGameOnly(): 플레이어 입력/컨트롤러가 입력을 받습니다.
  • FInputModeUIOnly(): UI만 입력을 받습니다. 닫기 버튼이나 포커스를 받은 위젯의 키 처리로 복귀 경로를 마련합니다.
  • FInputModeGameAndUI(): UI가 먼저 처리하고, 처리하지 않은 입력은 플레이어 입력/컨트롤러에 전달될 수 있습니다.
  • bShowMouseCursor: 마우스 커서의 가시성을 제어합니다.

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

생성된 UUserWidget* CurrentHUDWidget 포인터를 통해 해당 위젯 인스턴스의 함수를 호출하거나 속성에 접근할 수 있습니다.

특히 C++ 기반의 커스텀 UUserWidget 클래스를 사용하고 meta=(BindWidget)으로 자식 위젯을 노출했다면, C++에서 직접 자식 위젯의 속성을 제어할 수 있습니다.

MyPlayerController.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(): Slate 자원의 구성·해제에 대응하며 같은 UUserWidget 인스턴스에서 반복될 수 있습니다. 한 번의 UObject 생성·GC와 동일한 시점은 아닙니다. 아래 정의를 추가할 때 MyUserWidget.h에도 virtual void NativeDestruct() override;를 선언합니다.
MyUserWidget.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!"));
}
화면 제거와 인스턴스 재사용

같은 RemoveFromParent 호출 뒤에도 참조 정책에 따라 다음 열기가 달라집니다.

화면 제거와 인스턴스 재사용
코드 경로화면과 참조다음 열기
HideHUD부모에서 분리하고 CurrentHUDWidget을 비움새 HUD 인스턴스를 생성
인벤토리 닫기부모에서 분리하되 CurrentInventoryWidget 유지같은 인스턴스를 다시 AddToViewport
표시 속성 변경Visible로 바꾸기만 하면 분리된 부모 관계는 복구되지 않음기존 HUD가 분리되었다면 부착 여부를 먼저 확인
HideHUD
화면과 참조: 부모에서 분리하고 CurrentHUDWidget을 비움
다음 열기: 새 HUD 인스턴스를 생성
인벤토리 닫기
화면과 참조: 부모에서 분리하되 CurrentInventoryWidget 유지
다음 열기: 같은 인스턴스를 다시 AddToViewport
표시 속성 변경
화면과 참조: Visible로 바꾸기만 하면 분리된 부모 관계는 복구되지 않음
다음 열기: 기존 HUD가 분리되었다면 부착 여부를 먼저 확인

커서와 입력 모드는 별도 상태입니다. 이 예제는 닫을 때 GameOnly·커서 숨김으로 정하며, 열기 전의 임의 상태를 저장·복원하지 않습니다.

이 코드는 모듈의 UMG 의존성과 로컬 컨트롤러의 위젯 클래스 설정이 필요합니다. 인벤토리 닫기 버튼에서 ToggleInventory를 호출하도록 연결하고, UIOnly에서 게임 입력 바인딩만으로 닫으려 하지 않습니다. 위 PlayerTick 예시는 데이터 조회·전달 부분이 주석이라 실제 체력을 갱신하지 않습니다.


C++에서 UMG 위젯을 생성하고 제어하는 방법을 이해하는 것은 언리얼 엔진 UI 개발에 있어 필수적인 역량입니다.

TSubclassOf를 통한 클래스 참조, CreateWidget()을 통한 인스턴스 생성, AddToViewport()/RemoveFromParent()를 통한 화면 관리, 그리고 SetInputMode()를 통한 입력 처리까지, 이 모든 기능들은 견고하고 효율적인 게임 UI를 구축하는 데 중요한 역할을 합니다.