UE5 Common UI — 플랫폼 통합 UI 시스템
Common UI는 Epic Games가 Fortnite에서 추출한 멀티플랫폼 UI 프레임워크입니다. PC/콘솔/모바일에서 동일한 위젯 코드로 플랫폼별 입력(마우스, 게임패드, 터치)을 처리하고 포커스 내비게이션, 백 버튼, 액션 버튼을 일관되게 관리합니다.
기존 UMG는 “어떤 버튼이 눌렸는지”를 위젯마다 직접 처리해야 했습니다. Common UI는 입력을 태그 기반 액션으로 추상화해 플랫폼이 바뀌어도 위젯 코드를 수정할 필요가 없습니다.
1. 플러그인 활성화 및 설정
섹션 제목: “1. 플러그인 활성화 및 설정”Edit → Plugins → UI → Common UI Plugin → Enable빌드 모듈:
PublicDependencyModuleNames.AddRange(new[]{ "CommonUI", "CommonInput", "GameplayTags", "UMG",});프로젝트 설정에서 UI 레이어 등록:
Project Settings → Game → Common UI Framework → Registered UI Layers 항목에 GameplayTag 레이어 추가2. CommonActivatableWidget — 화면 스택
섹션 제목: “2. CommonActivatableWidget — 화면 스택”일반 UUserWidget 대신 UCommonActivatableWidget을 사용하면 화면 스택이 자동 관리됩니다. 위젯을 Push하면 이전 위젯 위에 올라가고, Deactivate하면 자동으로 팝됩니다.
#pragma once#include "CommonActivatableWidget.h"#include "MainMenuWidget.generated.h"
UCLASS()class MYGAME_API UMainMenuWidget : public UCommonActivatableWidget{ GENERATED_BODY()
UPROPERTY(meta = (BindWidget)) class UCommonButtonBase* PlayButton;
UPROPERTY(meta = (BindWidget)) class UCommonButtonBase* SettingsButton;
// 위젯 오픈 애니메이션 (BP에서 연결) UPROPERTY(Transient, meta = (BindWidgetAnim)) UWidgetAnimation* OpenAnimation;
protected: virtual void NativeOnInitialized() override; virtual void NativeOnActivated() override; virtual void NativeOnDeactivated() override; virtual void NativeOnBackAction() override;
// 포커스 시작 위젯 지정 (게임패드 자동 포커스) virtual UWidget* NativeGetDesiredFocusTarget() const override;};void UMainMenuWidget::NativeOnInitialized(){ Super::NativeOnInitialized();
// 버튼 클릭 바인딩 if (PlayButton) PlayButton->OnClicked().AddUObject(this, &UMainMenuWidget::OnPlayClicked); if (SettingsButton) SettingsButton->OnClicked().AddUObject(this, &UMainMenuWidget::OnSettingsClicked);}
void UMainMenuWidget::NativeOnActivated(){ Super::NativeOnActivated(); PlayAnimation(OpenAnimation); // 진입 애니메이션}
void UMainMenuWidget::NativeOnDeactivated(){ Super::NativeOnDeactivated(); // 리소스 해제, 애니메이션 중단 등}
void UMainMenuWidget::NativeOnBackAction(){ // ESC 키 / 게임패드 B 버튼 → 이전 화면으로 DeactivateWidget(); // 스택에서 팝}
UWidget* UMainMenuWidget::NativeGetDesiredFocusTarget() const{ // 게임패드 연결 시 첫 포커스 위젯 return PlayButton;}
void UMainMenuWidget::OnSettingsClicked(){ // 설정 화면을 현재 레이어 스택에 푸시 if (auto* PC = GetOwningPlayer()) { UCommonUIExtensions::PushContentToLayer_ForPlayer( GetOwningLocalPlayer(), FGameplayTag::RequestGameplayTag("UI.Layer.Menu"), USettingsWidget::StaticClass()); }}3. CommonUI Widget Stack 관리
섹션 제목: “3. CommonUI Widget Stack 관리”레이어 구조와 스택 Push/Pop 패턴입니다.
[UI 레이어 구조]UI.Layer.Modal ← 최상단 (확인 다이얼로그)UI.Layer.Menu ← 메뉴 화면 스택 (일시정지, 인벤토리...)UI.Layer.HUD ← 항상 표시 (체력바, 미니맵)// PlayerController에서 위젯 스택 관리#include "CommonUIExtensions.h"
// 일시정지 메뉴 열기void AMyPlayerController::OpenPauseMenu(){ // 위젯 스택에 푸시 — 이전 위젯은 뒤로 밀림 UCommonUIExtensions::PushContentToLayer_ForPlayer( GetLocalPlayer(), FGameplayTag::RequestGameplayTag("UI.Layer.Menu"), UPauseMenuWidget::StaticClass());}
// 확인 다이얼로그 표시void AMyPlayerController::ShowConfirmDialog( const FText& Message, FSimpleDelegate OnConfirm){ // Modal 레이어 → 입력이 다이얼로그로 집중됨 auto* Dialog = UCommonUIExtensions::PushContentToLayer_ForPlayer<UConfirmDialog>( GetLocalPlayer(), FGameplayTag::RequestGameplayTag("UI.Layer.Modal"), UConfirmDialog::StaticClass());
if (Dialog) { Dialog->SetMessage(Message); Dialog->OnConfirmed.AddLambda([OnConfirm]() { OnConfirm.ExecuteIfBound(); }); }}
// 특정 레이어의 최상단 위젯 강제 닫기void AMyPlayerController::CloseTopMenu(){ UCommonUIExtensions::PopContentFromLayer_ForPlayer( GetLocalPlayer(), FGameplayTag::RequestGameplayTag("UI.Layer.Menu"));}위젯에서 자신이 현재 최상단인지 확인:
void UMyWidget::NativeOnActivated(){ Super::NativeOnActivated();
// 게임패드 포커스를 이 위젯으로 이동 // (CommonUI가 스택 최상단에 자동 처리하지만 수동 제어 시) if (UCommonInputSubsystem* IS = UCommonInputSubsystem::Get(GetOwningLocalPlayer())) { SetUserFocus(GetOwningPlayer()); }}4. CommonButton — 플랫폼별 스타일
섹션 제목: “4. CommonButton — 플랫폼별 스타일”UCommonButtonBase는 스타일 에셋 기반으로 PC/콘솔/모바일에서 다른 시각적 표현을 자동으로 처리합니다.
// ActionButton.h — 게임패드 힌트 아이콘이 자동 표시되는 버튼#include "CommonButtonBase.h"
UCLASS()class MYGAME_API UActionButton : public UCommonButtonBase{ GENERATED_BODY()
// 버튼에 표시할 레이블 UPROPERTY(EditAnywhere, Category="Button") FText ButtonLabel;
// 버튼과 연결된 입력 액션 태그 // → 게임패드 연결 시 대응하는 버튼 아이콘이 자동으로 표시됨 UPROPERTY(EditAnywhere, Category="Button") FGameplayTag BoundInputAction;
protected: virtual void NativePreConstruct() override { Super::NativePreConstruct(); // 에디터 프리뷰에서 레이블 업데이트 SetButtonText(ButtonLabel); }
virtual void NativeOnClicked() override { Super::NativeOnClicked(); // 클릭 효과음 PlayClickSound(); }
virtual void NativeOnHovered() override { Super::NativeOnHovered(); // 호버 피드백 }};버튼 스타일 설정:
Content Browser → 우클릭 → User Interface → Common Button Style→ 상태별 스타일 설정: Normal / Hovered / Pressed / Disabled 각 상태에서 Brush, 텍스트 색상, 패딩 지정→ CommonButtonStyleBase 에셋에서 플랫폼별 분기 가능5. 입력 액션 바인딩
섹션 제목: “5. 입력 액션 바인딩”Common UI의 입력 바인딩은 Enhanced Input과 별개로 위젯 스코프 내에서만 동작합니다. 위젯이 활성화된 동안만 입력을 소비해 게임플레이 입력과 충돌하지 않습니다.
#include "CommonActivatableWidget.h"#include "Input/CommonBoundActionButton.h"
UCLASS()class UInventoryWidget : public UCommonActivatableWidget{ GENERATED_BODY()
// 하단 힌트 버튼 표시용 UPROPERTY(meta = (BindWidget)) class UCommonBoundActionBar* ActionBar;
protected: virtual void NativeOnActivated() override; virtual void NativeOnDeactivated() override;
private: FUIActionBindingHandle SortHandle; FUIActionBindingHandle DropHandle; FUIActionBindingHandle EquipHandle;};void UInventoryWidget::NativeOnActivated(){ Super::NativeOnActivated();
// 게임패드 Y버튼 → 정렬 // FBindUIActionArgs: 태그, 람다/함수, 표시 우선순위 FBindUIActionArgs SortArgs( FUIActionTag::ConvertChecked( FGameplayTag::RequestGameplayTag("CommonInput.Menu.Sort")), false, // bDisplayInActionBar FSimpleDelegate::CreateUObject(this, &UInventoryWidget::SortItems)); SortArgs.bDisplayInActionBar = true; SortHandle = RegisterUIActionBinding(SortArgs);
// 게임패드 X버튼 → 버리기 FBindUIActionArgs DropArgs( FUIActionTag::ConvertChecked( FGameplayTag::RequestGameplayTag("CommonInput.Menu.Drop")), false, FSimpleDelegate::CreateUObject(this, &UInventoryWidget::DropItem)); DropArgs.bDisplayInActionBar = true; DropHandle = RegisterUIActionBinding(DropArgs);
// 게임패드 A버튼 → 장착 FBindUIActionArgs EquipArgs( FUIActionTag::ConvertChecked( FGameplayTag::RequestGameplayTag("CommonInput.Menu.Confirm")), false, FSimpleDelegate::CreateUObject(this, &UInventoryWidget::EquipSelected)); EquipHandle = RegisterUIActionBinding(EquipArgs);}
void UInventoryWidget::NativeOnDeactivated(){ // 위젯 비활성화 시 바인딩 해제 (메모리 누수 방지) SortHandle.Unregister(); DropHandle.Unregister(); EquipHandle.Unregister();
Super::NativeOnDeactivated();}6. CommonTextBlock — 자동 텍스트 스타일
섹션 제목: “6. CommonTextBlock — 자동 텍스트 스타일”// 스타일 에셋 기반 텍스트 (폰트, 크기, 색상 일괄 관리)UPROPERTY(meta = (BindWidget))UCommonTextBlock* TitleText;
UPROPERTY(meta = (BindWidget))UCommonTextBlock* SubtitleText;
// CommonTextStyle 에셋 레퍼런스UPROPERTY(EditDefaultsOnly, Category="Style")TSubclassOf<UCommonTextStyle> TitleStyleClass;
void UMyWidget::NativeConstruct(){ Super::NativeConstruct();
// 스타일 에셋 할당 — 에디터에서도 미리보기 반영 if (TitleStyleClass) TitleText->SetStyle(TitleStyleClass);
TitleText->SetText(NSLOCTEXT("MyGame", "MainMenuTitle", "메인 메뉴"));
// 스크롤 설정 (긴 텍스트 자동 마퀴 스크롤) SubtitleText->SetScrollingEnabled(true); SubtitleText->SetScrollSpeed(50.f);}UCommonTextStyle 에셋에서 설정 가능한 항목:
- Font: 폰트 페이스와 크기
- Color: 기본/강조/비활성화 색상
- Margins: 텍스트 여백
- Line Height Percentage: 줄 간격
7. 플랫폼 감지 및 UI 분기
섹션 제목: “7. 플랫폼 감지 및 UI 분기”입력 방식이 변경될 때(예: 게임패드 연결/해제) UI를 동적으로 업데이트하는 패턴입니다.
#include "CommonInputSubsystem.h"#include "CommonInputTypeEnum.h"
UCLASS()class UHUDWidget : public UCommonActivatableWidget{ GENERATED_BODY()
// 게임패드 힌트 패널 (버튼 아이콘 표시) UPROPERTY(meta = (BindWidget)) UPanelWidget* GamepadHintPanel;
// 키보드/마우스 힌트 패널 UPROPERTY(meta = (BindWidget)) UPanelWidget* KeyboardHintPanel;
protected: virtual void NativeConstruct() override { Super::NativeConstruct();
if (auto* IS = UCommonInputSubsystem::Get(GetOwningLocalPlayer())) { // 입력 방식 변경 감지 (마우스 ↔ 게임패드 ↔ 터치) IS->OnInputMethodChangedNative.AddUObject( this, &UHUDWidget::OnInputMethodChanged);
// 초기 상태 즉시 반영 OnInputMethodChanged(IS->GetCurrentInputType()); } }
virtual void NativeDestruct() override { if (auto* IS = UCommonInputSubsystem::Get(GetOwningLocalPlayer())) IS->OnInputMethodChangedNative.RemoveAll(this);
Super::NativeDestruct(); }
private: void OnInputMethodChanged(ECommonInputType NewInputType) { const bool bIsGamepad = (NewInputType == ECommonInputType::Gamepad); const bool bIsTouch = (NewInputType == ECommonInputType::Touch);
// 힌트 패널 전환 GamepadHintPanel->SetVisibility( bIsGamepad ? ESlateVisibility::SelfHitTestInvisible : ESlateVisibility::Collapsed); KeyboardHintPanel->SetVisibility( (!bIsGamepad && !bIsTouch) ? ESlateVisibility::SelfHitTestInvisible : ESlateVisibility::Collapsed);
// 크로스헤어 크기 조정 (게임패드는 더 크게) CrosshairImage->SetDesiredSizeOverride( bIsGamepad ? FVector2D(24.f, 24.f) : FVector2D(16.f, 16.f)); }};콘솔 전용 UI 분기:
// 플랫폼 타입 직접 쿼리bool IsConsole(){#if PLATFORM_PS5 || PLATFORM_XSX return true;#else return false;#endif}
// 또는 런타임 감지 (개발 중 에디터에서 시뮬레이션 가능)ECommonInputType GetPlatformDefaultInput(){ if (UCommonInputPlatformSettings* PlatformSettings = UCommonInputPlatformSettings::Get()) { return PlatformSettings->GetDefaultInputType(); } return ECommonInputType::MouseAndKeyboard;}8. UI 레이어 구조 권장 설계
섹션 제목: “8. UI 레이어 구조 권장 설계”UI.Layer.Modal ← 확인 다이얼로그 (블로킹, 최상단 입력 독점)UI.Layer.Menu ← 일시정지, 인벤토리, 설정 (스택)UI.Layer.Game ← 게임 중 오버레이 (컷신 자막 등)UI.Layer.HUD ← 체력바, 미니맵 (항상 표시, 스택 없음)레이어를 DefaultGame.ini에 등록:
[/Script/CommonUI.CommonUISettings]+RegisteredUILayers=(LayerTag=(TagName="UI.Layer.HUD"), LayerZOrder=0)+RegisteredUILayers=(LayerTag=(TagName="UI.Layer.Game"), LayerZOrder=1)+RegisteredUILayers=(LayerTag=(TagName="UI.Layer.Menu"), LayerZOrder=2)+RegisteredUILayers=(LayerTag=(TagName="UI.Layer.Modal"), LayerZOrder=3)레이어 간 입력 포커스 규칙:
Modal 레이어가 열려 있으면: → 하위 레이어의 버튼/입력 모두 차단 → 게임패드 포커스도 Modal 위젯으로 고정
Menu 레이어가 열려 있으면: → HUD는 렌더링은 되지만 입력 수신 안 함 → 게임 입력(이동, 사격 등)도 차단됨9. 실전 팁과 주의사항
섹션 제목: “9. 실전 팁과 주의사항”위젯 풀링: PushContentToLayer는 매번 새 위젯을 생성합니다. 자주 열리는 위젯(인벤토리 등)은 UCommonActivatableWidgetContainerBase::GetActiveWidget()으로 재사용 여부를 확인하세요.
게임패드 포커스 손실: 스택에서 위젯을 팝한 뒤 포커스가 올바른 위젯으로 돌아가지 않는 경우, NativeGetDesiredFocusTarget()을 이전 위젯에서 명시적으로 구현하세요.
입력 블로킹: UCommonActivatableWidget의 bIsFocusable = true(기본값)이면 위젯이 열린 동안 아래 레이어의 게임플레이 입력이 차단됩니다. 순수 오버레이(데미지 텍스트 등)는 bIsFocusable = false로 설정하세요.
멀티플레이어 스플릿 스크린: PushContentToLayer_ForPlayer의 LocalPlayer 파라미터로 각 플레이어의 UI 스택을 독립적으로 관리할 수 있습니다.
Common UI는 플랫폼별 입력 처리와 포커스 내비게이션을 추상화해 PC/콘솔/모바일에서 하나의 위젯 코드로 동작하는 UI를 만들 수 있게 합니다. CommonActivatableWidget 스택으로 화면 전환을 관리하고, RegisterUIActionBinding으로 게임패드 액션을 위젯 스코프에 바인딩하세요. UCommonInputSubsystem::OnInputMethodChangedNative를 구독해 입력 방식 전환에 즉각 반응하는 UI를 구현하면 PC와 콘솔 모두에서 완성도 높은 UX를 제공할 수 있습니다.