C++에서 위젯 생성 및 제어
C++에서 Widget Blueprint 클래스를 참조하고 CreateWidget로 생성해 뷰포트와 데이터를 제어합니다.
위젯 블루프린트를 만들었다고 해서 게임을 시작하면 저절로 화면에 나타나지는 않습니다. 누군가 그 위젯의 인스턴스를 만들고, 화면에 붙이고, 필요 없어지면 떼어 내야 합니다. 게임을 시작할 때 HUD를 띄우고, 아이템을 얻었을 때 알림을 보여 주고, I 키를 누르면 인벤토리를 열고 닫는 일이 모두 이 과정입니다. 이 흐름을 여러 블루프린트에 흩어 두면, 어떤 위젯이 지금 화면에 있고 누가 그것을 가지고 있는지 추적하기 어려워집니다.
UMG 시스템 소개와 위젯 구조에서 위젯의 구조와 C++ 위젯 클래스를 보았으니, 이 글에서는 C++에서 위젯 블루프린트의 인스턴스를 만들어 화면에 붙이고, 내용을 바꾸고, 제거하는 방법을 살펴봅니다. 특히 화면에서 떼어 내는 일과 인스턴스를 계속 보관할지 정하는 일이 서로 다르다는 점을 HUD와 인벤토리의 두 경로로 비교합니다.
C++에서 위젯을 만드는 경우
대부분의 UI는 위젯 블루프린트(.uasset)로 존재하며, C++에서 그 인스턴스를 만들어 화면에 띄우는 방식은 몇 가지 경우에 씁니다. 게임을 시작할 때 HUD를 표시하거나 아이템을 얻었을 때 알림을 띄우는 것처럼, 게임 중의 조건에 따라 UI를 동적으로 만들어야 할 때가 첫째입니다. APlayerController나 AHUD 같은 한 클래스에서 모든 UI 위젯의 수명을 함께 관리하고 싶을 때, C++의 게임플레이 데이터를 위젯에 넣어 보여 줘야 할 때, 아주 자주 갱신되거나 복잡한 계산이 필요한 UI 로직을 C++로 만들고 싶을 때도 이 방식을 씁니다.
위젯 블루프린트 클래스 참조하기
C++에서 위젯 블루프린트의 인스턴스를 만들려면 먼저 그 위젯 블루프린트의 클래스를 알아야 합니다. 가장 흔한 방법은 UPROPERTY(EditDefaultsOnly)와 TSubclassOf<UUserWidget>로 클래스를 담을 변수를 두는 것입니다.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "MyPlayerController.generated.h"
// UUserWidget 전방 선언 (헤더는 .cpp에서 포함)
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는 에디터에서 위젯 블루프린트 에셋을 지정할 수 있게 해 주는 변수입니다. TSubclassOf는 지정한 C++ 클래스, 여기서는 UUserWidget을 상속한 클래스만 드롭다운 목록에 보여 주므로 엉뚱한 클래스를 고를 수 없습니다. 만든 인스턴스는 CurrentHUDWidget, CurrentInventoryWidget처럼 UPROPERTY()를 붙인 포인터에 보관해, 가비지 컬렉터가 이 참조를 추적하게 합니다.
위젯을 만들어 화면에 붙이기
CreateWidget()으로 위젯 인스턴스를 만들고, AddToViewport()나 AddToPlayerScreen()으로 화면에 표시합니다. 아래 코드의 세 함수는 인스턴스를 다루는 방식이 서로 다릅니다. ShowHUD는 이미 만든 인스턴스가 있으면 새로 만들지 않고 다시 보이게 하고, HideHUD는 화면에서 떼어 낸 뒤 포인터를 비우며, ToggleInventory는 한 번 만든 인스턴스를 붙였다 떼면서 입력 모드도 함께 바꿉니다. ShowHUD와 ToggleInventory가 맨 앞에서 IsLocalController()를 확인하는 것은, 화면을 가진 로컬 플레이어의 컨트롤러에서만 위젯을 만들기 위해서입니다.
#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); // 이름을 생략하면 엔진이 겹치지 않는 이름을 붙임
// 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이나 그것을 상속한 커스텀 위젯 클래스입니다. OwningObject는 위젯을 소유할 객체로, C++ 템플릿이 지원하는 타입만 쓸 수 있습니다. 이 예제는 로컬 APlayerController인 this를 넘기며, 아무 APawn이나 AGameModeBase를 그대로 넘기는 호출은 지원되지 않습니다. WidgetClass는 만들 위젯 블루프린트의 TSubclassOf 변수이고, WidgetName은 인스턴스에 붙일 선택적인 이름입니다.
이름을 줄 때는 겹치지 않는지 확인해야 합니다. 로컬 컨트롤러로 만든 위젯은 게임 인스턴스를 아우터로 가지므로, 같은 이름을 다시 쓰면 아직 회수되지 않은 이전 인스턴스를 같은 자리에서 새 객체로 덮어씁니다. 그래서 HideHUD 뒤에 다시 만드는 HUD는 이름을 생략해 엔진이 겹치지 않는 이름을 붙이게 했습니다.
UUserWidget::AddToViewport(ZOrder = 0)는 위젯을 게임 뷰포트에 붙여 화면에 표시합니다. ZOrder는 위젯이 겹칠 때의 순서로, 숫자가 클수록 위에 그려집니다. 분할 화면에서 특정 로컬 플레이어의 영역에만 붙이려면 같은 형식의 AddToPlayerScreen(ZOrder = 0)을 씁니다.
UUserWidget::RemoveFromParent()는 위젯을 부모, 즉 뷰포트나 다른 패널 위젯에서 떼어 내 화면에서 없앱니다. 하지만 떼어 냈다고 메모리에서 바로 사라지지는 않습니다. 이 예제의 UPROPERTY 포인터처럼 강한 참조가 남아 있으면 인스턴스는 그대로 살아 있고, 도달 가능한 참조가 모두 없어져야 GC의 대상이 됩니다.
인벤토리처럼 마우스로 다뤄야 하는 UI를 열 때는 입력 모드도 바꿉니다. APlayerController::SetInputMode()가 입력 모드를 바꾸는 함수인데, FInputModeGameOnly()는 플레이어 입력과 컨트롤러만 입력을 받는 모드입니다. FInputModeUIOnly()는 UI만 입력을 받는 모드이므로, 닫기 버튼이나 포커스를 받은 위젯의 키 처리처럼 게임으로 돌아올 길을 따로 마련해야 합니다. FInputModeGameAndUI()는 UI가 먼저 입력을 처리하고, UI가 처리하지 않은 입력은 플레이어 입력과 컨트롤러로 넘어갈 수 있는 모드입니다. 마우스 커서의 표시는 bShowMouseCursor로 정합니다.
코드의 흐름을 보면, ShowHUD는 로컬 컨트롤러가 아니면 바로 돌아가고, 이미 인스턴스가 있으면 화면에서 떨어져 있을 때만 다시 붙인 뒤 보이게 합니다. 인스턴스가 없을 때만 새로 만들어 붙입니다. HideHUD는 위젯을 떼어 낸 뒤 포인터를 nullptr로 비우므로, 다음 ShowHUD에서는 새 인스턴스를 만듭니다. ToggleInventory는 인벤토리가 화면에 붙어 있으면 떼어 내고 게임 전용 입력과 커서 숨김으로 돌아가며, 붙어 있지 않으면 인스턴스가 없을 때만 만들고 화면에 붙인 뒤 UI 전용 입력과 커서 표시로 바꿉니다. 인벤토리 포인터는 비우지 않습니다.
만든 위젯에 데이터 넣기
CurrentHUDWidget 포인터로는 그 위젯 인스턴스의 함수를 부르거나 속성에 접근할 수 있습니다. 특히 앞 글의 UMyUserWidget처럼 C++ 위젯 클래스에서 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++ 위젯 클래스로 캐스팅한 뒤, 클래스에 정의된 UpdateHealth 같은 함수나 변수에 접근해 UI를 갱신합니다. 이 예제는 매 틱마다 갱신하는 구조를 보여 주지만, 실제로는 체력이 바뀔 때만 부르는 편이 효율적입니다. 또 캐릭터의 체력을 읽어 넘기는 부분은 주석으로 남겨 두었으므로, 이 코드 그대로는 실제 체력을 갱신하지 않습니다.
위젯의 생명 주기 관리
위젯의 생명 주기는 보통 CreateWidget으로 만들고, AddToViewport로 화면에 붙이고, RemoveFromParent로 떼어 내는 순서로 관리합니다. 위젯 쪽에서는 NativeConstruct()와 NativeDestruct()가 불리는데, 이 둘은 슬레이트 자원이 구성되고 해제될 때에 맞춰 불리므로 같은 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을 붙인다. |
|
|
위 그림은 같은 RemoveFromParent 뒤에도 HUD와 인벤토리가 서로 다른 보관 정책을 쓴다는 점을 두 경로로 나란히 보여 줍니다. 두 경로는 서로 독립입니다. HUD 경로에서는 H1을 붙여 둔 상태에서 HideHUD가 위젯을 떼어 내고 이 컨트롤러의 참조를 nullptr로 비우므로, 다음 ShowHUD가 새 인스턴스 H2를 만듭니다. 인벤토리 경로에서는 닫을 때 I1을 떼어 내지만 참조는 보관하므로, 다음에 열 때 같은 I1을 다시 붙입니다. 그림 아래의 설명처럼 H2를 새로 만든다고 이전 H1이 즉시 GC되는 것은 아니며, I1을 다시 붙인 뒤에도 실제 데이터를 갱신하는 일은 따로 해야 합니다.
그림의 표는 코드 경로마다 화면과 참조에 일어나는 일과 다음에 열 때의 동작을 정리합니다. HideHUD는 부모에서 떼어 내고 CurrentHUDWidget을 비우므로 다음에는 새 HUD 인스턴스를 만듭니다. 인벤토리 닫기는 부모에서 떼어 내되 CurrentInventoryWidget을 유지하므로 다음에는 같은 인스턴스를 다시 AddToViewport합니다. 표의 마지막 행처럼 가시성을 Visible로 바꾸기만 해서는 끊어진 부모 관계가 돌아오지 않으므로, 기존 HUD가 떨어져 있다면 붙어 있는지부터 확인해야 합니다. 이 예제의 인벤토리 닫기는 게임 전용 입력과 커서 숨김으로 정해 두었을 뿐, 열기 전의 임의 상태를 저장했다가 되돌리지는 않습니다. 단계 그림도 H1과 I1이 표시된 상태에서, H1은 참조를 비우고 I1은 참조를 유지한 뒤, 다시 표시할 때 HUD는 H2를 만들고 인벤토리는 같은 I1을 붙이는 과정을 따라갑니다.
구체적으로 따라가 봅시다. 처음에 두 참조가 비어 있고 클래스와 생성 결과가 모두 유효하다고 합시다. ShowHUD를 두 번 부르면 두 번째 호출은 이미 있는 인스턴스를 그대로 씁니다. HideHUD는 화면에서 떼어 낸 뒤 참조를 비우므로, 다시 ShowHUD를 부르면 새 인스턴스를 만듭니다. 이전 객체가 그 줄에서 바로 메모리에서 사라졌다는 뜻은 아닙니다.
인벤토리는 ToggleInventory로 열고, 닫고, 다시 열어도 참조를 비우지 않으므로 같은 인스턴스를 다시 붙입니다. 하지만 객체를 다시 쓴다고 표시값까지 유지되는 것은 아닙니다. 슬레이트가 다시 구성될 때 NativeConstruct가 임시 표시값을 다시 넣을 수 있으므로, 실제 게임 값을 다시 전달할 시점을 따로 준비합니다.
따라 해 볼 때는 이 컨트롤러를 부모로 블루프린트 클래스를 만들어 HUDWidgetClass와 InventoryWidgetClass 기본값에 각 위젯 블루프린트를 지정하고, 현재 맵의 게임 모드가 그 플레이어 컨트롤러를 쓰게 합니다. 체력 갱신까지 이어 보려면 HUD 위젯의 부모가 앞 글의 UMyUserWidget인지 확인합니다. 같은 클래스의 생성자, BeginPlay, NativeConstruct를 여러 번 정의하지 말고 기존 정의에 필요한 동작을 합칩니다. 모듈에는 UMG 의존성이 있어야 하고, 인벤토리의 닫기 버튼에서 ToggleInventory를 부르도록 연결합니다. UI 전용 입력 모드에서는 게임 입력 바인딩이 동작하지 않으므로, 게임 입력만으로 닫으려 하지 않습니다.
정리
- C++에서 위젯을 만들려면
TSubclassOf<UUserWidget>변수에 위젯 블루프린트 클래스를 지정하고, 만든 인스턴스는UPROPERTY()포인터에 보관합니다. CreateWidget<T>()의 소유 객체는 템플릿이 지원하는 타입이어야 하며, 이 예제는 로컬APlayerController를 씁니다.AddToViewport()는 뷰포트에,AddToPlayerScreen()은 특정 로컬 플레이어의 화면에 위젯을 붙이며,ZOrder가 클수록 위에 그려집니다.RemoveFromParent()는 화면에서 떼어 낼 뿐 메모리에서 바로 없애지 않으며, 강한 참조가 남으면 인스턴스는 살아 있습니다.- 참조를 비우면 다음에 새 인스턴스를 만들고, 참조를 유지하면 같은 인스턴스를 다시 붙입니다. 가시성만 바꿔서는 떨어진 위젯이 다시 붙지 않습니다.
NativeConstruct()와NativeDestruct()는 같은 인스턴스에서 반복될 수 있으므로, 실제 게임 값은 다시 전달할 시점을 따로 정합니다.- UI 전용 입력 모드에서는 닫기 버튼처럼 게임으로 돌아오는 경로를 UI 안에 마련합니다.
연습 문제
1. HUD를 몇 번 만들까
앞의 유효한 조건에서 ShowHUD, ShowHUD, HideHUD, ShowHUD를 차례로 불렀습니다. 새 HUD 인스턴스는 몇 번 만들어질까요?
해설 보기
두 번입니다. 첫 ShowHUD에서 처음 만들고, 두 번째 ShowHUD는 이미 있는 인스턴스를 그대로 씁니다. HideHUD가 참조를 비우므로 마지막 ShowHUD에서 새 인스턴스를 한 번 더 만듭니다. 이전 인스턴스가 그 순간 메모리에서 사라졌는지는 이 횟수와 별개입니다.
2. 인벤토리를 세 번 토글하면
다른 호출 없이 ToggleInventory를 세 번 불렀습니다. 인벤토리는 몇 번 만들어지고, 마지막에 화면에 붙어 있을까요? 입력 모드와 커서는 어떤 상태일까요?
해설 보기
인벤토리는 한 번만 만들어집니다. 첫 호출에서 만들어 붙이고, 두 번째 호출에서 떼어 내되 참조는 유지하며, 세 번째 호출에서 같은 인스턴스를 다시 붙입니다. 그래서 마지막에는 화면에 붙어 있고, 이 예제의 코드에 따라 UI 전용 입력 모드에 커서가 표시된 상태입니다.
3. 화면에 보이니 체력도 맞다
HUD가 화면에 잘 보이고 체력 바도 100%로 표시됩니다. 이것으로 HUD가 캐릭터의 실제 체력을 보여 주고 있다고 판단해도 될까요?
해설 보기
판단할 수 없습니다. 이 글의 PlayerTick에서 캐릭터의 체력을 읽어 넘기는 부분은 주석이므로, 실제 체력이 HUD로 전달되지 않습니다. 100%는 앞 글의 NativeConstruct가 넣은 초기 표시값일 가능성이 큽니다.
실제 체력을 보여 주려면 캐릭터에서 체력을 읽어 UpdateHealth로 넘기는 경로를 구현하고, 위젯을 붙인 직후에도 현재 값을 한 번 맞춰 줘야 합니다.
위젯을 만들고 붙이는 함수는 Epic의 UUserWidget API 문서에서 확인할 수 있습니다.
다음 글 데이터 바인딩과 HUD 시스템에서는 게임의 값이 바뀔 때 HUD가 따라 바뀌도록 연결하는 방법과, HUD가 준비되었을 때 현재 값을 한 번 읽어 와 표시를 맞추는 구조를 살펴봅니다.