Skip to content

UE5 C++ UMG 위젯 시스템

UMG(Unreal Motion Graphics) 위젯은 블루프린트로도 완전히 구현 가능하지만, 복잡한 UI 로직, 데이터 바인딩, 성능 최적화는 C++ 기반 구현이 훨씬 유리합니다. C++ UUserWidget 서브클래스를 기반으로 BP 위젯에 로직 레이어를 분리하는 구조가 권장됩니다.


MyHUDWidget.h
#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;
};
MyHUDWidget.cpp
#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에서 위젯 생성 및 관리

Section titled “2. PlayerController에서 위젯 생성 및 관리”
MyPlayerController.h
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;
};
MyPlayerController.cpp
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);
}

// 캐릭터 체력 변경 시 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 이벤트 바인딩으로 자동 갱신

Section titled “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));
}
}

// 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()
{
// 애니메이션 완료 후 처리
}

// 틱이 필요 없는 위젯은 반드시 비활성화
UMyStaticWidget::UMyStaticWidget()
{
// 생성자에서 틱 비활성화 — NativeTick 호출 없음
bCanEverTick = false;
}
// 이벤트 기반으로만 갱신
void UMyStaticWidget::UpdateScore(int32 NewScore)
{
if (ScoreText)
ScoreText->SetText(FText::AsNumber(NewScore));
}

Invalidation Box로 감싼 자식 위젯은 변경이 없으면 리페인트를 생략합니다. 스크롤 가능한 목록, 미니맵 등 갱신 빈도가 낮은 UI에 효과적입니다.

// C++에서 Invalidation Box 생성
void UMyInventoryWidget::NativeConstruct()
{
Super::NativeConstruct();
// 인벤토리 슬롯 목록은 Invalidation Box로 래핑
if (InventoryInvalidationBox)
{
// 슬롯 업데이트 시에만 명시적으로 무효화
InventoryInvalidationBox->InvalidateLayoutAndVolatility();
}
}
// ❌ 느림: Collapsed/Hidden은 레이아웃 재계산 발생
Widget->SetVisibility(ESlateVisibility::Collapsed);
// ✅ 빠름: 렌더링만 생략, 레이아웃 계산 유지
Widget->SetRenderOpacity(0.f);
Widget->SetVisibility(ESlateVisibility::HitTestInvisible);
// 완전히 비활성화가 필요할 때만 Collapsed 사용

목적방법
위젯 변수 C++ 접근UPROPERTY(meta = (BindWidget))
위젯 애니메이션 접근UPROPERTY(meta = (BindWidgetAnim), Transient)
위젯 초기화NativeConstruct()
매 프레임 갱신NativeTick() + bCanEverTick = true
성능 — 정적 영역Invalidation Box
성능 — 숨김 처리SetRenderOpacity (Collapsed 대신)