나만의 작은 도서관
[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가 호출된다.