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>를 사용하여 수행하는 것이 가장 일반적입니다.
#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() 함수를 사용하여 화면에 표시합니다.
#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++에서 직접 자식 위젯의 속성을 제어할 수 있습니다.
#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;를 선언합니다.
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!"));
}화면에서 분리해도 인스턴스 보관 정책은 다릅니다
독립 HUD H1은 분리/참조 비움 후 H2 생성, 인벤토리 I1은 분리/참조 유지 후 같은 I1 재부착이라는 수명/표시 상태 변화이다.
UNREAL C++ · CH6-2
화면에서 분리해도 인스턴스 보관 정책은 다릅니다
같은 RemoveFromParent 뒤에도 HUD는 참조를 비우고 인벤토리는 참조를 유지합니다. 다음 열기에 새로 생성하는지, 다시 부착하는지를 코드별로 비교합니다.
H1/H2와 I1은 생성·재사용을 구별하는 예시 인스턴스입니다. 새 HUD 생성이 이전 객체의 즉시 GC를 뜻하지 않으며, 인벤토리 재부착 뒤에도 실제 데이터 갱신은 별도입니다.
| 코드 경로 | 화면과 참조 | 다음 열기 |
|---|---|---|
| HideHUD | 부모에서 분리하고 CurrentHUDWidget을 비움 | 새 HUD 인스턴스를 생성 |
| 인벤토리 닫기 | 부모에서 분리하되 CurrentInventoryWidget 유지 | 같은 인스턴스를 다시 AddToViewport |
| 표시 속성 변경 | Visible로 바꾸기만 하면 분리된 부모 관계는 복구되지 않음 | 기존 HUD가 분리되었다면 부착 여부를 먼저 확인 |
- HideHUD
- 화면과 참조: 부모에서 분리하고 CurrentHUDWidget을 비움다음 열기: 새 HUD 인스턴스를 생성
- 인벤토리 닫기
- 화면과 참조: 부모에서 분리하되 CurrentInventoryWidget 유지다음 열기: 같은 인스턴스를 다시 AddToViewport
- 표시 속성 변경
- 화면과 참조: Visible로 바꾸기만 하면 분리된 부모 관계는 복구되지 않음다음 열기: 기존 HUD가 분리되었다면 부착 여부를 먼저 확인
RemoveFromParent는 화면 부모에서의 분리이며 즉시 객체 소멸을 뜻하지 않습니다. 이 예제의 인벤토리 닫기는 GameOnly·커서 숨김으로 정하고, 열기 전 임의 상태를 저장·복원하지 않습니다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 관계·조건 설명 | HUD와 인벤토리의 두 비교 경로는 독립이다. HUD를 붙여 둔 상태에서 HideHUD는 HUD를 분리하고 이 컨트롤러의 참조를 비워 다음 ShowHUD가 새 인스턴스를 만들도록 한다. 인벤토리 닫기는 화면에서 분리하되 참조를 보관하여 다음 열기에 같은 인스턴스를 다시 붙인다. 화면 분리는 즉시 객체 소멸을 뜻하지 않는다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 부착 상태 독립 HUD H1과 인벤토리 I1이 표시된다. |
|
|
| 분리 정책 H1은 참조를 비우고 I1은 참조를 유지한다. |
|
|
| 다시 표시 HUD는 H2를 만들고 인벤토리는 같은 I1을 붙인다. |
|
|
화면에서 분리해도 인스턴스 보관 정책은 다릅니다
독립 HUD H1은 분리/참조 비움 후 H2 생성, 인벤토리 I1은 분리/참조 유지 후 같은 I1 재부착이라는 수명/표시 상태 변화이다.
UNREAL C++ · CH6-2
화면에서 분리해도 인스턴스 보관 정책은 다릅니다
같은 RemoveFromParent 뒤에도 HUD는 참조를 비우고 인벤토리는 참조를 유지합니다. 다음 열기에 새로 생성하는지, 다시 부착하는지를 코드별로 비교합니다.
H1/H2와 I1은 생성·재사용을 구별하는 예시 인스턴스입니다. 새 HUD 생성이 이전 객체의 즉시 GC를 뜻하지 않으며, 인벤토리 재부착 뒤에도 실제 데이터 갱신은 별도입니다.
| 코드 경로 | 화면과 참조 | 다음 열기 |
|---|---|---|
| HideHUD | 부모에서 분리하고 CurrentHUDWidget을 비움 | 새 HUD 인스턴스를 생성 |
| 인벤토리 닫기 | 부모에서 분리하되 CurrentInventoryWidget 유지 | 같은 인스턴스를 다시 AddToViewport |
| 표시 속성 변경 | Visible로 바꾸기만 하면 분리된 부모 관계는 복구되지 않음 | 기존 HUD가 분리되었다면 부착 여부를 먼저 확인 |
- HideHUD
- 화면과 참조: 부모에서 분리하고 CurrentHUDWidget을 비움다음 열기: 새 HUD 인스턴스를 생성
- 인벤토리 닫기
- 화면과 참조: 부모에서 분리하되 CurrentInventoryWidget 유지다음 열기: 같은 인스턴스를 다시 AddToViewport
- 표시 속성 변경
- 화면과 참조: Visible로 바꾸기만 하면 분리된 부모 관계는 복구되지 않음다음 열기: 기존 HUD가 분리되었다면 부착 여부를 먼저 확인
RemoveFromParent는 화면 부모에서의 분리이며 즉시 객체 소멸을 뜻하지 않습니다. 이 예제의 인벤토리 닫기는 GameOnly·커서 숨김으로 정하고, 열기 전 임의 상태를 저장·복원하지 않습니다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 관계·조건 설명 | HUD와 인벤토리의 두 비교 경로는 독립이다. HUD를 붙여 둔 상태에서 HideHUD는 HUD를 분리하고 이 컨트롤러의 참조를 비워 다음 ShowHUD가 새 인스턴스를 만들도록 한다. 인벤토리 닫기는 화면에서 분리하되 참조를 보관하여 다음 열기에 같은 인스턴스를 다시 붙인다. 화면 분리는 즉시 객체 소멸을 뜻하지 않는다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 부착 상태 독립 HUD H1과 인벤토리 I1이 표시된다. |
|
|
| 분리 정책 H1은 참조를 비우고 I1은 참조를 유지한다. |
|
|
| 다시 표시 HUD는 H2를 만들고 인벤토리는 같은 I1을 붙인다. |
|
|
따라 할 때는 이 컨트롤러를 부모로 한 BP 클래스의 HUDWidgetClass와 InventoryWidgetClass 기본값에 각 Widget Blueprint 클래스를 지정하고, 현재 맵의 GameMode가 그 PlayerController 클래스를 사용하도록 준비합니다. 체력 갱신을 이어 보려면 HUD 위젯의 부모가 앞 절의 UMyUserWidget인지도 확인합니다. 같은 클래스의 생성자·BeginPlay와 NativeConstruct 예제를 여러 번 정의하지 않고 기존 정의에 필요한 동작을 합칩니다.
예시: 생성 횟수와 화면 부착 횟수 비교하기
시작 시 두 참조가 비어 있고 클래스와 생성 결과가 유효하다고 가정합니다. ShowHUD를 두 번 호출해도 두 번째는 기존 인스턴스를 사용합니다. HideHUD는 화면에서 분리한 뒤 이 컨트롤러의 참조를 비우므로, 다시 ShowHUD를 호출하면 새 인스턴스를 생성합니다. 이전 객체가 그 줄에서 즉시 메모리에서 사라졌다는 뜻은 아닙니다.
인벤토리는 ToggleInventory로 열기·닫기·다시 열기를 해도 참조를 비우지 않아 같은 인스턴스를 다시 붙입니다. 객체 재사용과 표시값 유지가 항상 같은 것은 아닙니다. Slate 재구성 때 NativeConstruct가 임시 표시를 다시 대입할 수 있어, 실제 게임 값은 다시 전달할 시점을 준비합니다.
원 코드의 두 경로 따라가기
위의 유효 조건에서 ShowHUD, ShowHUD, HideHUD, ShowHUD를 차례로 호출했습니다. 새 HUD 생성은 몇 번일까요? 별도로 ToggleInventory를 세 번 호출했을 때 인벤토리 생성 횟수와 마지막 부착 상태를 구하세요. 화면만 보이면 체력도 실제 데이터로 갱신되었다고 판단해도 되는지 설명하세요.
참조와 부착을 나누어 풀이하기
HUD는 처음과 마지막 호출에서 두 번 생성됩니다. 인벤토리는 한 번 생성하고 마지막에는 부착되어 있으며 이 예제에서는 UIOnly와 커서 표시 상태가 됩니다. 원 PlayerTick의 데이터 전달은 주석이므로, 표시된 HUD에 실제 체력이 자동으로 들어왔다는 판정은 할 수 없습니다. 조회·전달 경로를 구현하고 부착 뒤 초기 현재값도 동기화해야 합니다.
이 코드는 모듈의 UMG 의존성과 로컬 컨트롤러의 위젯 클래스 설정이 필요합니다. 인벤토리 닫기 버튼에서 ToggleInventory를 호출하도록 연결하고, UIOnly에서 게임 입력 바인딩만으로 닫으려 하지 않습니다. 위 PlayerTick 예시는 데이터 조회·전달 부분이 주석이라 실제 체력을 갱신하지 않습니다.
C++에서 UMG 위젯을 생성하고 제어하는 방법을 이해하는 것은 언리얼 엔진 UI 개발에 있어 필수적인 역량입니다.
TSubclassOf를 통한 클래스 참조, CreateWidget()을 통한 인스턴스 생성, AddToViewport()/RemoveFromParent()를 통한 화면 관리, 그리고 SetInputMode()를 통한 입력 처리까지, 이 모든 기능들은 견고하고 효율적인 게임 UI를 구축하는 데 중요한 역할을 합니다.