UE5 C++ UMG 위젯 시스템
개요 — UMG C++ 연동의 필요성
섹션 제목: “개요 — UMG C++ 연동의 필요성”UMG(Unreal Motion Graphics) 위젯은 블루프린트로도 완전히 구현 가능하지만, 복잡한 UI 로직, 데이터 바인딩, 성능 최적화는 C++ 기반 구현이 훨씬 유리합니다. C++ UUserWidget 서브클래스를 기반으로 BP 위젯에 로직 레이어를 분리하는 구조가 권장됩니다.
1. UUserWidget 서브클래싱
섹션 제목: “1. UUserWidget 서브클래싱”1.1 기본 구조
섹션 제목: “1.1 기본 구조”#pragma once
#include "CoreMinimal.h"#include "Blueprint/UserWidget.h"#include "MyHUDWidget.generated.h"
UCLASS()class MYGAME_API UMyHUDWidget : public UUserWidget{ GENERATED_BODY()
public: // BindWidget — UMG 에디터의 동일 이름 위젯에 자동 바인딩 UPROPERTY(meta = (BindWidget)) TObjectPtr<class UProgressBar> HealthBar;
UPROPERTY(meta = (BindWidget)) TObjectPtr<class UProgressBar> StaminaBar;
UPROPERTY(meta = (BindWidget)) TObjectPtr<class UTextBlock> AmmoText;
// BindWidgetOptional — 없어도 컴파일 오류 없음 UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<class UTextBlock> DebugText;
// BindWidgetAnim — 위젯 애니메이션 바인딩 UPROPERTY(meta = (BindWidgetAnim), Transient) TObjectPtr<UWidgetAnimation> DamageFlashAnim;
// 외부에서 호출하는 업데이트 API UFUNCTION(BlueprintCallable, Category = "HUD") void UpdateHealth(float CurrentHP, float MaxHP);
UFUNCTION(BlueprintCallable, Category = "HUD") void UpdateAmmo(int32 Current, int32 Reserve);
void PlayDamageFlash();
protected: // 위젯 초기화 — AddToViewport 직후 호출 virtual void NativeConstruct() override;
// 위젯 제거 시 호출 virtual void NativeDestruct() override;
// 매 프레임 (bCanEverTick = true 필요) virtual void NativeTick(const FGeometry& MyGeometry, float InDeltaTime) override;};1.2 구현
섹션 제목: “1.2 구현”#include "MyHUDWidget.h"#include "Components/ProgressBar.h"#include "Components/TextBlock.h"#include "MyPlayerController.h"#include "MyCharacter.h"
void UMyHUDWidget::NativeConstruct(){ Super::NativeConstruct();
// 초기값 설정 if (HealthBar) HealthBar->SetPercent(1.f); if (StaminaBar) StaminaBar->SetPercent(1.f); if (AmmoText) AmmoText->SetText(FText::FromString(TEXT("0 / 0")));
// 틱 필요 여부 — 필요한 경우만 활성화 (성능) SetTickableWhenPaused(false);}
void UMyHUDWidget::NativeDestruct(){ // 모든 타이머 및 델리게이트 해제 Super::NativeDestruct();}
void UMyHUDWidget::NativeTick(const FGeometry& MyGeometry, float InDeltaTime){ Super::NativeTick(MyGeometry, InDeltaTime); // 매 프레임 갱신이 필요한 로직 (크로스헤어 위치, 나침반 등)}
void UMyHUDWidget::UpdateHealth(float CurrentHP, float MaxHP){ if (!HealthBar) return;
float Percent = MaxHP > 0.f ? CurrentHP / MaxHP : 0.f; HealthBar->SetPercent(Percent);
// 체력 낮을 때 색상 변경 FLinearColor BarColor = FLinearColor::LerpUsingHSV( FLinearColor::Red, FLinearColor::Green, Percent); HealthBar->SetFillColorAndOpacity(BarColor);}
void UMyHUDWidget::UpdateAmmo(int32 Current, int32 Reserve){ if (!AmmoText) return;
FText AmmoDisplay = FText::Format( FTextFormat::FromString(TEXT("{0} / {1}")), FText::AsNumber(Current), FText::AsNumber(Reserve));
AmmoText->SetText(AmmoDisplay);
// 탄약 부족 경고 색상 FSlateColor TextColor = (Current <= 5) ? FSlateColor(FLinearColor::Red) : FSlateColor(FLinearColor::White); AmmoText->SetColorAndOpacity(TextColor);}
void UMyHUDWidget::PlayDamageFlash(){ if (DamageFlashAnim) { // 이미 재생 중이면 처음부터 재시작 if (IsAnimationPlaying(DamageFlashAnim)) { StopAnimation(DamageFlashAnim); } PlayAnimation(DamageFlashAnim); }}2. PlayerController에서 위젯 생성 및 관리
섹션 제목: “2. PlayerController에서 위젯 생성 및 관리”UCLASS()class MYGAME_API AMyPlayerController : public APlayerController{ GENERATED_BODY()
public: virtual void BeginPlay() override; virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
void ShowHUD(); void HideHUD();
protected: // TSubclassOf — 에디터에서 BP 위젯 클래스 할당 UPROPERTY(EditDefaultsOnly, Category = "HUD") TSubclassOf<UMyHUDWidget> HUDWidgetClass;
private: TObjectPtr<UMyHUDWidget> HUDWidgetInstance;};void AMyPlayerController::BeginPlay(){ Super::BeginPlay();
if (IsLocalController() && HUDWidgetClass) { HUDWidgetInstance = CreateWidget<UMyHUDWidget>(this, HUDWidgetClass); if (HUDWidgetInstance) { HUDWidgetInstance->AddToViewport(0); // ZOrder } }}
void AMyPlayerController::EndPlay(const EEndPlayReason::Type EndPlayReason){ if (HUDWidgetInstance) { HUDWidgetInstance->RemoveFromViewport(); HUDWidgetInstance = nullptr; } Super::EndPlay(EndPlayReason);}
void AMyPlayerController::ShowHUD(){ if (HUDWidgetInstance) HUDWidgetInstance->SetVisibility(ESlateVisibility::Visible);}
void AMyPlayerController::HideHUD(){ if (HUDWidgetInstance) HUDWidgetInstance->SetVisibility(ESlateVisibility::Collapsed);}3. 위젯 간 통신 패턴
섹션 제목: “3. 위젯 간 통신 패턴”3.1 외부에서 위젯 API 호출
섹션 제목: “3.1 외부에서 위젯 API 호출”// 캐릭터 체력 변경 시 HUD 업데이트void AMyCharacter::OnHealthChanged(float NewHP, float MaxHP){ APlayerController* PC = Cast<APlayerController>(GetController()); if (!PC) return;
AMyPlayerController* MyPC = Cast<AMyPlayerController>(PC); if (!MyPC) return;
if (UMyHUDWidget* HUD = MyPC->GetHUDWidget()) { HUD->UpdateHealth(NewHP, MaxHP); if (NewHP < MaxHP) // 피격 시 플래시 { HUD->PlayDamageFlash(); } }}3.2 이벤트 바인딩으로 자동 갱신
섹션 제목: “3.2 이벤트 바인딩으로 자동 갱신”// 위젯에서 GameState 이벤트 구독void UMyHUDWidget::NativeConstruct(){ Super::NativeConstruct();
// GameInstance를 통해 글로벌 이벤트 구독 if (UMyGameInstance* GI = GetGameInstance<UMyGameInstance>()) { GI->OnScoreChanged.AddDynamic(this, &UMyHUDWidget::HandleScoreChanged); }}
void UMyHUDWidget::NativeDestruct(){ if (UMyGameInstance* GI = GetGameInstance<UMyGameInstance>()) { GI->OnScoreChanged.RemoveDynamic(this, &UMyHUDWidget::HandleScoreChanged); } Super::NativeDestruct();}
UFUNCTION()void UMyHUDWidget::HandleScoreChanged(int32 NewScore){ if (ScoreText) { ScoreText->SetText(FText::AsNumber(NewScore)); }}4. 위젯 애니메이션 C++ 제어
섹션 제목: “4. 위젯 애니메이션 C++ 제어”// BindWidgetAnim으로 바인딩된 애니메이션 제어void UMyHUDWidget::ShowWeaponPanel(){ if (WeaponPanelShowAnim) { PlayAnimation(WeaponPanelShowAnim, 0.f, 1, EUMGSequencePlayMode::Forward, 1.0f); }}
void UMyHUDWidget::HideWeaponPanel(){ if (WeaponPanelShowAnim) { // 역방향 재생으로 숨김 효과 PlayAnimation(WeaponPanelShowAnim, 0.f, 1, EUMGSequencePlayMode::Reverse, 1.0f); }}
// 애니메이션 완료 후 콜백void UMyHUDWidget::NativeConstruct(){ Super::NativeConstruct();
// 애니메이션 완료 이벤트 바인딩 BindToAnimationFinished(DamageFlashAnim, FWidgetAnimationDynamicEvent::CreateUObject( this, &UMyHUDWidget::OnDamageFlashFinished));}
UFUNCTION()void UMyHUDWidget::OnDamageFlashFinished(){ // 애니메이션 완료 후 처리}5. 성능 최적화
섹션 제목: “5. 성능 최적화”5.1 Tick 최소화
섹션 제목: “5.1 Tick 최소화”// 틱이 필요 없는 위젯은 반드시 비활성화UMyStaticWidget::UMyStaticWidget(){ // 생성자에서 틱 비활성화 — NativeTick 호출 없음 bCanEverTick = false;}
// 이벤트 기반으로만 갱신void UMyStaticWidget::UpdateScore(int32 NewScore){ if (ScoreText) ScoreText->SetText(FText::AsNumber(NewScore));}5.2 Invalidation Box 활용
섹션 제목: “5.2 Invalidation Box 활용”Invalidation Box로 감싼 자식 위젯은 변경이 없으면 리페인트를 생략합니다. 스크롤 가능한 목록, 미니맵 등 갱신 빈도가 낮은 UI에 효과적입니다.
// C++에서 Invalidation Box 생성void UMyInventoryWidget::NativeConstruct(){ Super::NativeConstruct();
// 인벤토리 슬롯 목록은 Invalidation Box로 래핑 if (InventoryInvalidationBox) { // 슬롯 업데이트 시에만 명시적으로 무효화 InventoryInvalidationBox->InvalidateLayoutAndVolatility(); }}5.3 SetVisibility vs SetRenderOpacity
섹션 제목: “5.3 SetVisibility vs SetRenderOpacity”// ❌ 느림: Collapsed/Hidden은 레이아웃 재계산 발생Widget->SetVisibility(ESlateVisibility::Collapsed);
// ✅ 빠름: 렌더링만 생략, 레이아웃 계산 유지Widget->SetRenderOpacity(0.f);Widget->SetVisibility(ESlateVisibility::HitTestInvisible);
// 완전히 비활성화가 필요할 때만 Collapsed 사용6. 정리
섹션 제목: “6. 정리”| 목적 | 방법 |
|---|---|
| 위젯 변수 C++ 접근 | UPROPERTY(meta = (BindWidget)) |
| 위젯 애니메이션 접근 | UPROPERTY(meta = (BindWidgetAnim), Transient) |
| 위젯 초기화 | NativeConstruct() |
| 매 프레임 갱신 | NativeTick() + bCanEverTick = true |
| 성능 — 정적 영역 | Invalidation Box |
| 성능 — 숨김 처리 | SetRenderOpacity (Collapsed 대신) |
7. ListView & IUserObjectListEntry — 동적 목록 UI
섹션 제목: “7. ListView & IUserObjectListEntry — 동적 목록 UI”UListView는 항목 수에 관계없이 화면에 보이는 만큼만 위젯을 생성(가상화)해 인벤토리·로비 목록에 적합합니다.
// InventoryEntryWidget.h — 각 행 위젯#pragma once
#include "CoreMinimal.h"#include "Blueprint/UserWidget.h"#include "Blueprint/IUserObjectListEntry.h"#include "InventoryEntryWidget.generated.h"
UCLASS()class MYGAME_API UInventoryEntryWidget : public UUserWidget, public IUserObjectListEntry{ GENERATED_BODY()
public: // ListView가 항목 데이터 주입 시 호출 virtual void NativeOnListItemObjectSet(UObject* ListItemObject) override;
UPROPERTY(meta = (BindWidget)) TObjectPtr<class UTextBlock> ItemNameText;
UPROPERTY(meta = (BindWidget)) TObjectPtr<class UImage> ItemIconImage;
UPROPERTY(meta = (BindWidget)) TObjectPtr<class UTextBlock> QuantityText;};
// InventoryEntryWidget.cppvoid UInventoryEntryWidget::NativeOnListItemObjectSet(UObject* ListItemObject){ // UObject 서브클래스로 캐스팅해 데이터 적용 if (UInventoryItemData* ItemData = Cast<UInventoryItemData>(ListItemObject)) { if (ItemNameText) ItemNameText->SetText(ItemData->DisplayName); if (QuantityText) QuantityText->SetText(FText::AsNumber(ItemData->Quantity));
// 아이콘 비동기 로드 if (!ItemData->IconRef.IsNull()) { ItemData->IconRef.LoadSynchronous(); if (UTexture2D* Icon = ItemData->IconRef.Get()) { ItemIconImage->SetBrushFromTexture(Icon); } } }}// InventoryWidget.h — ListView를 포함하는 인벤토리 패널UCLASS()class MYGAME_API UInventoryWidget : public UUserWidget{ GENERATED_BODY()
UPROPERTY(meta = (BindWidget)) TObjectPtr<class UListView> InventoryList;
// 진입점 — 인벤토리 아이템 배열을 ListView에 공급 void PopulateList(const TArray<UInventoryItemData*>& Items);};
void UInventoryWidget::PopulateList(const TArray<UInventoryItemData*>& Items){ InventoryList->ClearListItems();
for (UInventoryItemData* Item : Items) { InventoryList->AddItem(Item); // UObject* 로 전달 }}8. 위젯 풀링 — 반복 생성 비용 절감
섹션 제목: “8. 위젯 풀링 — 반복 생성 비용 절감”스킬 아이콘, 대미지 숫자 등 자주 생성·제거되는 위젯은 풀을 사용합니다.
UCLASS(ClassGroup = (UI), meta = (BlueprintSpawnableComponent))class MYGAME_API UWidgetPoolComponent : public UActorComponent{ GENERATED_BODY()
public: // 풀에서 위젯 획득 (없으면 새로 생성) UUserWidget* AcquireWidget(TSubclassOf<UUserWidget> WidgetClass, APlayerController* PC);
// 사용 완료 후 풀에 반환 void ReturnWidget(UUserWidget* Widget);
private: TMap<TSubclassOf<UUserWidget>, TArray<UUserWidget*>> Pool;};
UUserWidget* UWidgetPoolComponent::AcquireWidget( TSubclassOf<UUserWidget> WidgetClass, APlayerController* PC){ TArray<UUserWidget*>& Available = Pool.FindOrAdd(WidgetClass);
if (Available.Num() > 0) { UUserWidget* Recycled = Available.Pop(false); Recycled->SetVisibility(ESlateVisibility::Visible); return Recycled; }
// 풀 소진 시 새로 생성 return CreateWidget<UUserWidget>(PC, WidgetClass);}
void UWidgetPoolComponent::ReturnWidget(UUserWidget* Widget){ if (!Widget) return;
Widget->SetVisibility(ESlateVisibility::Collapsed); Pool.FindOrAdd(Widget->GetClass()).Add(Widget);}