UE5 C++ UMG 위젯 시스템
개요 — UMG C++ 연동의 필요성
Section titled “개요 — UMG C++ 연동의 필요성”UMG(Unreal Motion Graphics) 위젯은 블루프린트로도 완전히 구현 가능하지만, 복잡한 UI 로직, 데이터 바인딩, 성능 최적화는 C++ 기반 구현이 훨씬 유리합니다. C++ UUserWidget 서브클래스를 기반으로 BP 위젯에 로직 레이어를 분리하는 구조가 권장됩니다.
1. UUserWidget 서브클래싱
Section titled “1. UUserWidget 서브클래싱”1.1 기본 구조
Section titled “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 구현
Section titled “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에서 위젯 생성 및 관리
Section titled “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. 위젯 간 통신 패턴
Section titled “3. 위젯 간 통신 패턴”3.1 외부에서 위젯 API 호출
Section titled “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 이벤트 바인딩으로 자동 갱신
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)); }}4. 위젯 애니메이션 C++ 제어
Section titled “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. 성능 최적화
Section titled “5. 성능 최적화”5.1 Tick 최소화
Section titled “5.1 Tick 최소화”// 틱이 필요 없는 위젯은 반드시 비활성화UMyStaticWidget::UMyStaticWidget(){ // 생성자에서 틱 비활성화 — NativeTick 호출 없음 bCanEverTick = false;}
// 이벤트 기반으로만 갱신void UMyStaticWidget::UpdateScore(int32 NewScore){ if (ScoreText) ScoreText->SetText(FText::AsNumber(NewScore));}5.2 Invalidation Box 활용
Section titled “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
Section titled “5.3 SetVisibility vs SetRenderOpacity”// ❌ 느림: 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 대신) |