게임 흐름에 맞춘 메뉴 UI 구현하기
게임 메뉴 UI 디자인하기
1️⃣ 메뉴 위젯 생성하고 버튼 추가하기
- 그동안은 게임 중에 나오는 위젯을 설계했다면, 이번에는 게임을 일시적으로 중단하고 시작하거나 종료하는 메뉴 위젯을 만들어보도록 하겠습니다.
- Content Browser에서 UI - Widgets 폴더에서 마우스 우클릭 → User Interface → Widget Blueprint 선택합니다. 새로 생성되는 위젯의 이름은 WBP_MainMenu라고 지어 주세요.
- 더블 클릭하여 열면, 중앙에 Designer 탭, 오른쪽에 Details 탭, 왼쪽에는 Hierarchy & Palette가 보입니다.
- Canvas Panel을 놓아서 기본적인 바탕을 마련합니다.
- 그 아래에 Border을 생성해서 바탕을 투명도를 주고 검정색 바탕화면으로 지정해줍니다. 그래서 게임 실행 시 레벨의 모습이 잘 보이지 않도록 설정합니다.
- Common - Button 위젯을 각각 Start 버튼 하나만 Canvas Panel 아래에 둡니다. Button의 이름은 StartButton으로 변경해줍니다.
- Alignment를 (0.5, 0.5)로 놓고 X, Y의 위치를 화면 크기의 절반으로 두면 가운데에 딱 배치될 수 있습니다.
- 혹은 그냥 Anchors 를 정 중앙으로 하면 됩니다. (Ctrl+Shift+ 원하는 Anchor 위치 선택하면 반영됩니다)
- 각 버튼마다 Common - Text를 끌어다 놓습니다. 왼쪽 하단에 Hierarchy창에 드래그 앤 드랍을 하면 편합니다. Text의 이름은 StartButtonText로 지정해줍니다.



2️⃣ 메뉴 레벨 생성하고 설정하기
- 흔히 게임 개발 시 메뉴 전용 맵을 만들어, 그 맵에서는 메뉴만 띄우도록 하고, 실제 게임이 시작되면 게임 레벨로 넘어가게 합니다.
- 이 방식을 쓰면 “Menu UI와 Game Level”이 완전히 분리되어, 구조가 좀 더 명확해집니다.
- File - New Level 을 선택하고 Basic 템플릿으로 생성을 합니다. Maps 폴더에 MenuLevel이라는 이름으로 저장을 해줍니다.
- 단축키: Ctrl+N
- Editor - Project Settings에 들어가서 Maps & Modes에서 Default Maps를 MenuLevel로 지정해서 저장해주도록 합니다.

게임 흐름 내에 메뉴 UI 배치하기
- 우선, 우리가 만든 이 메뉴 위젯 (WBP_MainMenu)을 다양한 상황에서 표시하고, 숨기고, 다시 띄우는 로직을 단계별로 구현해야 합니다.
- 플레이 버튼을 누르면 Menu UI가 가장 먼저 뜨기
- 게임 시작 시 자동으로 HUD를 띄우기
- 메뉴가 나타날 때마다, UI 입력 모드로 전환하여 버튼 클릭에 집중하게 만드는 방식으로 수정
- 게임이 종료되면 메뉴가 다시 뜨도록 만들기
1️⃣ PlayerController에 기본 위젯 오픈 함수 구현
- 위젯을 어디서 만들고 관리할지는 프로젝트마다 다른데, 일반적으로 PlayerController가 UI를 다루기 좋습니다. 게임 모드도 가능하지만, 멀티플레이를 고려하면 PlayerController가 UI 담당이 좀 더 자연스러운 편입니다.
- 게임 입력 vs UI 입력
- 게임 플레이 중 메뉴가 활성화되면, UI만 입력을 받도록 하거나, UI + 게임 둘 다 입력을 받도록 할 수 있습니다.
- 이때 UI에 마우스 포커스가 가도록 만들려면 SetInputMode 계열 함수를 사용해야 합니다. SetInputMode 계열 함수를 통해 PlayerController가 어느 입력을 우선으로 처리할지 결정합니다.
- UI 전용 입력 모드로 전환하면, 플레이어의 마우스 입력과 키 입력이 먼저 UI로 전달됩니다. 캐릭터 이동이나 시야 회전 등 게임 월드 입력은 잠시 비활성화되고, 버튼 클릭에 집중할 수 있습니다.
- FInputModeUIOnly 구조체 생성 → SetWidgetToFocus로 포커스할 UI 위젯 지정
- SetInputMode(InputMode) 호출 → 이제 마우스 클릭이나 키 입력이 UI를 먼저 처
- bShowMouseCursor = true로 마우스 커서를 보이게 설정
- 빌드한 후, BP_SpartaPlayerController 에서 MainMenuWidgetClass에 WBP_MainMenu 를 할당해줍니다.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "MainPlayerController.generated.h"
// Declaration
class UInputMappingContext; // IMC 관련 전방 선언
class UInputAction; // IA 관련 전방 선언
// Enhanced Input에서 액션 값을 받을 때 사용하는 구조체
struct FInputActionValue;
UCLASS()
class BC_CH3_ASSIGNMENT_5_API AMainPlayerController : public APlayerController
{
GENERATED_BODY()
public:
AMainPlayerController();
// 에디터에서 세팅할 IMC
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Input")
UInputMappingContext* InputMappingContext;
// IA
// IA_Move를 지정할 변수
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Input")
UInputAction* MoveAction;
// IA_Jump를 지정할 변수
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Input")
UInputAction* JumpAction;
// IA_Look를 지정할 변수
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Input")
UInputAction* LookAction;
// IA_Sprint를 지정할 변수
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input")
UInputAction* SprintAction;
// UMG 위젯 클래스를 에디터에서 할당받을 변수
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HUD")
TSubclassOf<UUserWidget> HUDWidgetClass;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HUD")
UUserWidget* HUDWidgetInstance;
UFUNCTION(BlueprintPure, Category = "HUD")
UUserWidget* GetHUDWidget() const;
// HUD 표시
UFUNCTION(BlueprintCallable, Category = "HUD")
void ShowGameHUD();
// 메뉴 UI
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Menu")
TSubclassOf<UUserWidget> MainMenuWidgetClass;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Menu")
UUserWidget* MainMenuWidgetInstance;
// 메인 메뉴 표시
UFUNCTION(BlueprintCallable, Category = "Menu")
void ShowMainMenu(bool bIsRestart);
// 게임 시작
UFUNCTION(BlueprintCallable, Category = "Menu")
void StartGame();
protected:
virtual void BeginPlay() override;
/*
//UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Input", meta=(AllowPrivateAccess="true"))
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input")
UInputAction* ToggleInventoryAction;
bool bIsInventoryOpen;
UFUNCTION(BlueprintCallable)
void ToggleInventory();
void ActivateCharacterInput();
void ActivateUIInput();
*/
};
#include "MainPlayerController.h"
#include "EnhancedInputSubsystems.h"
#include "EnhancedInputSubsystems.h" // Enhanced Input System의 Local Player Subsystem을 사용하기 위해 포함
#include "MainGameState.h"
#include "MainGameInstance.h"
#include "Blueprint/UserWidget.h"
#include "Kismet/GameplayStatics.h"
#include "Components/TextBlock.h"
// 어차피 블루프린트 상에서 전부 다 초기화를 하기 때문에 여기서는 전부 다 nullptr 처리
AMainPlayerController::AMainPlayerController()
: InputMappingContext(nullptr),
MoveAction(nullptr),
JumpAction(nullptr),
LookAction(nullptr),
SprintAction(nullptr),
HUDWidgetClass(nullptr),
HUDWidgetInstance(nullptr),
MainMenuWidgetClass(nullptr),
MainMenuWidgetInstance(nullptr)
{
}
void AMainPlayerController::BeginPlay()
{
Super::BeginPlay();
// GetLocalPlayer():현재 PlayerController에 연결된 Local Player 객체를 가져옴
// Local Player 는 그 플레이어의 입력이나 화면 뷰 같은 것을 관리하는 어떤 객체
if (ULocalPlayer* LocalPlayer = GetLocalPlayer())
{
// Local Player에서 EnhancedInputLocalPlayerSubsystem을 획득
// UEnhancedInputLocalPlayerSubsystem: 입력 시스템을 관리 (IMC 추가 혹은 삭제하는 역할)
if (UEnhancedInputLocalPlayerSubsystem* Subsystem =
LocalPlayer->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>())
{
if (InputMappingContext)
{
// Subsystem을 통해 우리가 할당한 IMC를 활성화
// 우선순위(Priority)는 0이 가장 높은 우선순위
Subsystem->AddMappingContext(InputMappingContext, 0);
}
}
}
// HUD 위젯 생성 및 표시
if (HUDWidgetClass)
{
HUDWidgetInstance = CreateWidget<UUserWidget>(this, HUDWidgetClass);
if (HUDWidgetInstance)
{
HUDWidgetInstance->AddToViewport();
}
}
AMainGameState* MainGameState = GetWorld() ? GetWorld()->GetGameState<AMainGameState>() : nullptr;
if (MainGameState)
{
MainGameState->UpdateHUD();
}
}
UUserWidget* AMainPlayerController::GetHUDWidget() const
{
return HUDWidgetInstance;
}
// 메뉴 UI 표시
void AMainPlayerController::ShowMainMenu(bool bIsRestart)
{
// HUD가 켜져 있다면 닫기
if (HUDWidgetInstance)
{
HUDWidgetInstance->RemoveFromParent();
HUDWidgetInstance = nullptr;
}
// 이미 메뉴가 떠 있으면 제거
if (MainMenuWidgetInstance)
{
MainMenuWidgetInstance->RemoveFromParent();
MainMenuWidgetInstance = nullptr;
}
// 메뉴 UI 생성
if (MainMenuWidgetClass)
{
MainMenuWidgetInstance = CreateWidget<UUserWidget>(this, MainMenuWidgetClass);
if (MainMenuWidgetInstance)
{
MainMenuWidgetInstance->AddToViewport();
bShowMouseCursor = true;
SetInputMode(FInputModeUIOnly());
}
if (UTextBlock* ButtonText = Cast<UTextBlock>(
MainMenuWidgetInstance->GetWidgetFromName(TEXT("StartButtonText"))))
{
if (bIsRestart)
{
ButtonText->SetText(FText::FromString(TEXT("Restart")));
}
else
{
ButtonText->SetText(FText::FromString(TEXT("Start")));
}
}
}
}
// 게임 HUD 표시
void AMainPlayerController::ShowGameHUD()
{
// HUD가 켜져 있다면 닫기
if (HUDWidgetInstance)
{
HUDWidgetInstance->RemoveFromParent();
HUDWidgetInstance = nullptr;
}
// 이미 메뉴가 떠 있으면 제거
if (MainMenuWidgetInstance)
{
MainMenuWidgetInstance->RemoveFromParent();
MainMenuWidgetInstance = nullptr;
}
if (HUDWidgetClass)
{
HUDWidgetInstance = CreateWidget<UUserWidget>(this, HUDWidgetClass);
if (HUDWidgetInstance)
{
HUDWidgetInstance->AddToViewport();
bShowMouseCursor = false;
SetInputMode(FInputModeGameOnly());
AMainGameState* MainGameState = GetWorld() ? GetWorld()->GetGameState<AMainGameState>() : nullptr;
if (MainGameState)
{
MainGameState->UpdateHUD();
}
}
}
}
// 게임 시작 - BasicLevel 오픈, GameInstance 데이터 리셋
void AMainPlayerController::StartGame()
{
if (UMainGameInstance* MainGameInstance = Cast<UMainGameInstance>(UGameplayStatics::GetGameInstance(this)))
{
MainGameInstance->CurrentLevelIndex = 0;
MainGameInstance->TotalScore = 0;
}
UGameplayStatics::OpenLevel(GetWorld(), FName("BasicLevel"));
}
2️⃣ GameState에서 게임 흐름에 따라 UI 호출
#include "MainGameState.h"
#include "MainGameInstance.h"
#include "MainPlayerController.h"
#include "Kismet/GameplayStatics.h"
#include "SpawnVolume.h"
#include "CoinItem.h"
#include "Components/TextBlock.h"
#include "Blueprint/UserWidget.h"
AMainGameState::AMainGameState()
{
Score = 0;
SpawnedCoinCount = 0;
CollectedCoinCount = 0;
LevelDuration = 30.0f; // 한 레벨당 30초
CurrentLevelIndex = 0;
MaxLevels = 3;
}
void AMainGameState::BeginPlay()
{
Super::BeginPlay();
// 게임 시작 시 첫 레벨부터 진행
StartLevel();
UpdateHUD();
GetWorldTimerManager().SetTimer(
HUDUpdateTimerHandle,
this,
&AMainGameState::UpdateHUD,
0.1f,
true
);
}
int32 AMainGameState::GetScore() const
{
return Score;
}
void AMainGameState::AddScore(int32 Amount)
{
if (UGameInstance* GameInstance = GetGameInstance())
{
UMainGameInstance* MainGameInstance = Cast<UMainGameInstance>(GameInstance);
if (MainGameInstance)
{
MainGameInstance->AddToScore(Amount);
}
}
}
void AMainGameState::StartLevel()
{
if (APlayerController* PlayerController = GetWorld()->GetFirstPlayerController())
{
if (AMainPlayerController* SpartaPlayerController = Cast<AMainPlayerController>(PlayerController))
{
SpartaPlayerController->ShowGameHUD();
}
}
if (UGameInstance* GameInstance = GetGameInstance())
{
UMainGameInstance* MainGameInstance = Cast<UMainGameInstance>(GameInstance);
if (MainGameInstance)
{
CurrentLevelIndex = MainGameInstance->CurrentLevelIndex;
}
}
// 레벨 시작 시, 코인 개수 초기화
SpawnedCoinCount = 0;
CollectedCoinCount = 0;
// 현재 맵에 배치된 모든 SpawnVolume을 찾아 아이템 40개를 스폰
TArray<AActor*> FoundVolumes;
UGameplayStatics::GetAllActorsOfClass(GetWorld(), ASpawnVolume::StaticClass(), FoundVolumes);
const int32 ItemToSpawn = 40;
for (int32 i = 0; i < ItemToSpawn; i++)
{
if (FoundVolumes.Num() > 0)
{
ASpawnVolume* SpawnVolume = Cast<ASpawnVolume>(FoundVolumes[0]);
if (SpawnVolume)
{
AActor* SpawnedActor = SpawnVolume->SpawnRandomItem();
// 만약 스폰된 액터가 코인 타입이라면 SpawnedCoinCount 증가
if (SpawnedActor && SpawnedActor->IsA(ACoinItem::StaticClass()))
{
SpawnedCoinCount++;
}
}
}
}
// 30초 후에 OnLevelTimeUp()가 호출되도록 타이머 설정
GetWorldTimerManager().SetTimer(
LevelTimerHandle,
this,
&AMainGameState::OnLevelTimeUp,
LevelDuration,
false
);
/*UE_LOG(LogTemp, Warning, TEXT("Level %d Start!, Spawned %d coin"),
CurrentLevelIndex + 1,
SpawnedCoinCount);*/
}
void AMainGameState::OnLevelTimeUp()
{
// 시간이 다 되면 레벨을 종료
EndLevel();
}
void AMainGameState::OnCoinCollected()
{
CollectedCoinCount++;
/*UE_LOG(LogTemp, Warning, TEXT("Coin Collected: %d / %d"),
CollectedCoinCount,
SpawnedCoinCount)*/
// 현재 레벨에서 스폰된 코인을 전부 주웠다면 즉시 레벨 종료
if (SpawnedCoinCount > 0 && CollectedCoinCount >= SpawnedCoinCount)
{
EndLevel();
}
}
void AMainGameState::EndLevel()
{
// 타이머 해제
GetWorldTimerManager().ClearTimer(LevelTimerHandle);
// 다음 레벨 인덱스로
//CurrentLevelIndex++;
if (UGameInstance* GameInstance = GetGameInstance())
{
UMainGameInstance* MainGameInstance = Cast<UMainGameInstance>(GameInstance);
if (MainGameInstance)
{
AddScore(Score);
CurrentLevelIndex++;
MainGameInstance->CurrentLevelIndex = CurrentLevelIndex;
}
}
// 모든 레벨을 다 돌았다면 게임 오버 처리
if (CurrentLevelIndex >= MaxLevels)
{
OnGameOver();
return;
}
// 레벨 맵 이름이 있다면 해당 맵 불러오기
if (LevelMapNames.IsValidIndex(CurrentLevelIndex))
{
UGameplayStatics::OpenLevel(GetWorld(), LevelMapNames[CurrentLevelIndex]);
}
else
{
// 맵 이름이 없으면 게임오버
OnGameOver();
}
}
void AMainGameState::OnGameOver()
{
if (APlayerController* PlayerController = GetWorld()->GetFirstPlayerController())
{
if (AMainPlayerController* SpartaPlayerController = Cast<AMainPlayerController>(PlayerController))
{
SpartaPlayerController->ShowMainMenu(true);
}
}
//UpdateHUD();
//UE_LOG(LogTemp, Warning, TEXT("Game Over!!"));
}
void AMainGameState::UpdateHUD()
{
if (APlayerController* PlayerController = GetWorld()->GetFirstPlayerController())
{
AMainPlayerController* MainPlayerController = Cast<AMainPlayerController>(PlayerController);
{
if (UUserWidget* HUDWidget = MainPlayerController->GetHUDWidget())
{
// requires
// #include "Components/TextBlock.h"
// #include "Blueprint/UserWidget.h"
//추후 변경 방안
// UPROPERTY(meta = (BindWidget))
// class UButton* MyAwesomeButton;
if (UTextBlock* TimeText = Cast<UTextBlock>(HUDWidget->GetWidgetFromName(TEXT("Time"))))
{
float RemainingTime = GetWorldTimerManager().GetTimerRemaining(LevelTimerHandle);
TimeText->SetText(FText::FromString(FString::Printf(TEXT("Time: %.1f"), RemainingTime)));
}
//
if (UTextBlock* ScoreText = Cast<UTextBlock>(HUDWidget->GetWidgetFromName(TEXT("Score"))))
{
if (UGameInstance* GameInstance = GetGameInstance())
{
UMainGameInstance* MainGameInstance = Cast<UMainGameInstance>(GameInstance);
if (MainGameInstance)
{
ScoreText->SetText(FText::FromString(FString::Printf(TEXT("Score: %i"), MainGameInstance->TotalScore)));
}
}
}
if (UTextBlock* LevelIndexText = Cast<UTextBlock>(HUDWidget->GetWidgetFromName(TEXT("Level"))))
{
LevelIndexText->SetText(FText::FromString(FString::Printf(TEXT("Level: %d"), CurrentLevelIndex + 1)));
}
}
}
}
}
3️⃣ Start 버튼 클릭 이벤트에 함수 바인딩
BP_MainPlayerController Setting:Menu -> WBP_MainMenu

Designer 탭에서 StartButton 버튼을 선택하고, Details 패널에서 맨 위쪽에 버튼 이름을 지정해주는 부분이 있습니다. 그 옆에 Is Variable에 체크를 해줍니다. 그러면 맨 하단에 Events 들이 활성화되는 것을 볼 수 있습니다.
WBP_MainMenu Setting:
- Buttion set Is Variable true
- OnClicked


추천
[Unreal Engine/UE 기초] - 인터페이스 기반 아이템 클래스 설계하기 | [언리얼 엔진 C++ (Unreal Engine C++)]
인터페이스 기반 아이템 클래스 설계하기 | [언리얼 엔진 C++ (Unreal Engine C++)]
인터페이스 기반 아이템 클래스 설계하기 인터페이스 이해하기1️⃣ 인터페이스란?인터페이스 (Interface)란 클래스 (또는 오브젝트)가 반드시 구현해야 할 함수 목록만을 미리 정의해 두고, 실제
devcol.tistory.com
[Unreal Engine/UE 기초] - 충돌 이벤트로 획득되는 아이템 구현하기 | [언리얼 엔진 C++ (Unreal Engine C++)]
충돌 이벤트로 획득되는 아이템 구현하기 | [언리얼 엔진 C++ (Unreal Engine C++)]
충돌 이벤트로 획득되는 아이템 구현하기 이전 포스팅에서는 클래스 구조만 짜고 로직은 비여있던 상태.이전 포스팅: [Unreal Engine/UE 기초] - 인터페이스 기반 아이템 클래스 설계하기 | [언리얼
devcol.tistory.com
[Unreal Engine/UE 기초] - 아이템 스폰 및 레벨 데이터 관리하기 | [언리얼 엔진 C++ (Unreal Engine C++)]
아이템 스폰 및 레벨 데이터 관리하기 | [언리얼 엔진 C++ (Unreal Engine C++)]
아이템 스폰 및 레벨 데이터 관리하기 랜덤 위치에 아이템 스폰하기 1️⃣ 레벨 셋팅하기Resources 폴더에 Maps 폴더에는 3개의 레벨이 이미 존재합니다. 각각 난이도에 따라서 크기가 다른 맵이고,
devcol.tistory.com
[Unreal Engine/UE 기초] - 캐릭터 체력 및 점수 관리 시스템 구현하기 | [언리얼 엔진 C++ (Unreal Engine C++)]
캐릭터 체력 및 점수 관리 시스템 구현하기 | [언리얼 엔진 C++ (Unreal Engine C++)]
캐릭터 체력 및 점수 관리 시스템 구현하기 캐릭터 체력 시스템 구현하기 1️⃣ 캐릭터 클래스에 체력 변수 및 함수 선언PlayerState를 쓰지 않는 이유PlayerState: 각 플레이어마다의 어떤 정보를 관
devcol.tistory.com
[Unreal Engine/UE 기초] - 게임 루프 설계를 통한 게임 흐름 제어하기 | [언리얼 엔진 C++ (Unreal Engine C++)]
게임 루프 설계를 통한 게임 흐름 제어하기 | [언리얼 엔진 C++ (Unreal Engine C++)]
게임 루프 설계를 통한 게임 흐름 제어하기 GameState를 이용한 게임 루프 구현하기 게임루프: 보통 게임의 핵심적인 흐름을 얘기합니다. 즉 게임이 시작할 때부터 종료까지 수행하는 단계들게임
devcol.tistory.com
[Unreal Engine/UE 기초] - UI 위젯 설계와 실시간 데이터 연동하기 | [언리얼 엔진 C++ (Unreal Engine C++)]
UI 위젯 설계와 실시간 데이터 연동하기 | [언리얼 엔진 C++ (Unreal Engine C++)]
UI 위젯 설계와 실시간 데이터 연동하기 | [언리얼 엔진 C++ (Unreal Engine C++)] UMG (User Widget) 위젯 기초 디자인 이해하기 1️⃣ HUD (Heads-Up Display)란?HUD는 게임 내에서 플레이어에게 정보를 제공하기 위
devcol.tistory.com
[페이지] Unreal Engine | 언리얼 엔진