나만의 작은 도서관

[TIL][C++] 251031 MMO 서버 개발 127일차: [언리얼] CreateWidget의 첫번째 인자에 대해…, CreateWidget을 WBP로 바인딩했을 경우 BindWidget 변수는 Nullptr이 아니다. 본문

Today I Learn

[TIL][C++] 251031 MMO 서버 개발 127일차: [언리얼] CreateWidget의 첫번째 인자에 대해…, CreateWidget을 WBP로 바인딩했을 경우 BindWidget 변수는 Nullptr이 아니다.

pledge24 2025. 11. 1. 00:33
주의사항: 해당 글은 일기와 같은 기록용으로, 다듬지 않은 날것 그대로인 글입니다. 

[언리얼] CreateWidget의 첫 번째 인자에 대해…

CreateWidget(OwnerType OwningObject, TSubclassOf<UUserWidget> UserWidgetClass, ...)
  • CreateWidget의 경우 첫 번째 인자로 OwningObject가 들어간다. 이는 생성할 위젯이 속할 소유자(Owner)를 결정하는 요소로, 소유자에 따라 위젯의 생명주기, 입력 포커스 처리, 로컬/서버 구분이 달라지게 된다.

 

OwningObject가 PlayerController인 경우

  • 가장 일반적인 형태
  • 생성하고자 하는 위젯이 특정 플레이어(로컬 클라이언트)에 속하게 된다.
  • 속한 곳이 특정 플레이어이므로, 1) 해당 플레이어의 입력만 받을 수 있으며, 2) AddToViewport()에 의한 표시도 해당 플레이어에게만 된다. 이러한 이유 때문에 멀티플레이에서도 각 플레이어가 자신만의 위젯을 가질 수 있다.

 

OwningObject가 World인 경우

  • 전역 위젯을 생성하는 형태
  • 생성하고자 하는 위젯이 특정 플레이어에게 귀속되지 않고, 월드 레벨의 콘텍스트에서 UI가 만들어진다.
  • 속한 곳이 월드이므로, 1) 어느 플레이어에게도 입력을 받지 못하며, 2) AddToViewport()에 의한 표시가 모든 플레이어에게 공유된다. 이러한 이유 때문에 멀티플레이에서 각 플레이어가 자신만의 위젯을 가져야 할 경우 World를 넣어줘선 안된다.

 

World를 넣어줬을 때 발생할 수 있는 문제

// ❌ 잘못된 예시
void AMyPlayerController::ShowInventory()
{
    UInventoryWidget* Widget = CreateWidget<UInventoryWidget>(GetWorld(), WidgetClass);
    Widget->AddToViewport();
}

 

 

문제점

  • 위젯 내부에서 GetOwningPlayer()를 호출하면 첫 번째 로컬 플레이어를 반환.
  • 멀티플레이어에서 Player 2, 3, 4가 자신의 인벤토리를 열면 Player 1의 데이터를 참조할 수 있음!
  • Split-screen에서 모든 플레이어가 같은 PlayerController를 참조하는 버그 발생

 


[언리얼] CreateWidget을 WBP로 바인딩했을 경우 BindWidget 변수는 Nullptr이 아니다.

UNameplateWidget* NameplateWidget = CreateWidget<UNameplateWidget>(this, NameplateWidgetClass);
  • 위 코드가 있다고 가정. 각 인자 및 변수는 아래와 같다.
    • this: PlayerController 계열
    • UNameplateWidget: C++ 위젯 클래스
    • NameplateWidgetClass: UNameplateWidget을 부모 클래스로 두는 WBP
  • 이 상황에서 UNameplateWidget 클래스의 멤버 변수가 아래와 같이 BindWidget으로 BP와 엮을 때,
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, meta = (BindWidget))
UTextBlock* NameTextBlock;

UPROPERTY(VisibleAnywhere, BlueprintReadWrite, meta = (BindWidget))
UProgressBarWidget* HpBar;
  • NameTextBlock과 HpBar는 CreateWidget 직후부터 Nullptr이 아님을 보장할 수 있다.

 

 

왜 보장할 수 있는가?

  • CreateWidget을 할 때 WBP를 넘겨줘서 위젯이 넘겨준 WBP를 기준으로 생성되었기 때문이다. 즉, C++에서 CreateWidget을 했어도 BP 클래스의 구조를 알고 그 구조대로 위젯을 만들었기 때문에 CreateWidget을 했을 때는 이미 WBP의 구조를 다 만든 상태이다.
    • 참고로, CreateWidget은 동기 함수이며, 반환된 시점에서 위젯은 모든 생성 과정이 끝난 상태이다.

 

 

그래서?

  • 그래서 CreateWidget으로 반환된 포인터를 사용할 경우, BindWidget으로 묶여있는 위젯을 C++에서 곧바로 초기화하는 것이 안전하다는 것이다. 아래와 같이 말이다.
UNameplateWidget* NameplateWidget = CreateWidget<UNameplateWidget>(this, NameplateWidgetClass);
if(NameplateWidget)
{
  NameplateWidget->InitializeWidget(Actor);
}

// NameplateWidget::InitializeWidget
NameTextBlock->SetVisibility(ESlateVisibility::Visible);
HpBar->SetVisibility(ESlateVisibility::Collapsed);

NameTextBlock->SetText(Player->GetPlayerName());
  • Null이 아님을 보장받기 때문에 곧바로 초기화 세팅을 할 수 있다.

 

 

추가 질문. AddToViewport()를 한 다음에 가시성 세팅을 해도 괜찮을까?

  • 기본적으로 괜찮다고 한다. 그 이유는 실제로 스크린에 위젯을 렌더링 하는 시점은 AddToViewport()를 호출한 다음 틱이기 때문. 가시성 세팅을 하고 AddToViewport()를 하든, AddToViewport()를 하고 가시성 세팅을 하든, 플레이어에게 보이는 건 똑같다는 것이다.
// 순서가 어떻든 결과는 똑같다. 다만, 두번째가 좀 더 합리적인 순서이다.
MyWidget->AddToViewport();
MyWidget->SetVisibility(ESlateVisibility::Hidden);

MyWidget->SetVisibility(ESlateVisibility::Hidden);
MyWidget->AddToViewport();

 

 

추가 질문 2. 위젯의 NativeConstruct는 뭘까?

  • 액터의 OnConstruction과 같은 역할. 각각 위젯/액터가 생성된 직후 초기화 로직을 실행하는 데 사용되는 함수이다.
  • 위젯의 NativeConstruct의 경우 액터의 OnConstruction처럼 에디터에서 프로퍼티 수정이 발생할 때마다 호출되지는 않고, 게임 실행 중에 위젯이 생성될 때만 호출된다.
    • 그래서 WBP로 위젯을 생성하면, WBP의 프로퍼티가 전부 설정된 다음, NativeConstruct가 호출된다.