본문 바로가기

언리얼 + cpp

언리얼C++ 10일차(메뉴 위젯, 게임 흐름에 따라 UI 생성하기)

저번 시간에 게임중 나오는 HUD위젯을 구현했다면 이번에는 게임을 중단하거나 시작 전 나오는 메뉴 위젯을 만들어본다.

메뉴 위젯 만들기

Widget Blueprint를 새로 만들어서 캔버스와 보더, 버튼 위을 배치한다.

보더는 검은색을 주고 투명도를 조절하여 게임에서 일시정지 할 때 나오는거처럼 구현한다.

버튼은 화면의 가운데에 오도록 앵커를 전체로 놓고 Offset을 0으로 두거나 해상도 절반씩 위치하도록 조절한다음 Alignment를 0.5씩 주면 가운데로 정렬이 된다.

1. Anchors를 전체로 놓고 모두 0
2. 해상도 절반씩 위치하고 Alignment 0.5

텍스트 위젯을 버튼에 드래그해서 놓고 텍스트를 Start로 변경한다

흔히 게임 개발 시 메뉴 전용 맵을 만들어서 그 맵에서는 메뉴만 띄우도록 하고 실제 게임이 시작되면 게임 레벨로 넘어가도록 구현을 한다. 이 방식을 쓰면 Menu UI와 Game Level이 완전히 분리되어 구조가 더 명확해진다.

메뉴 전용 맵을 Basic 템플릿으로 생성 후 저장을 하고 시작시 이 맵이 불러와지도록 프로젝트 세팅에서 변경한다.

게임 흐름 내에 메뉴 UI와 HUD 배치하기

이제 C++에서 메뉴 위젯과 HUD가 필요할 때 표시되고 숨기고를 구현해야한다.

  1. 플레이 버튼을 누르면 Menu UI가 가장 먼저 뜨기
  2. 게임 시작 시 자동으로 HUD를 띄우기
  3. 메뉴가 나타날 때마다, UI 입력 모드로 전환하여 버튼 클릭에 집중하게 만드는 방식으로 수정
  4. 게임이 종료되면 메뉴가 다시 뜨도록 만들기

프로젝트마다 위젯을 어디서 만들고 관리하는지는 다르지만 PlayerController가 UI 담당하기 자연스럽다고한다.

// JinPlayerController.h
	// 메뉴 UI
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Menu")
	TSubclassOf<UUserWidget> MainMenuWidgetClass;
	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Menu")
	UUserWidget* MainMenuWidgetInstance;
    
	UFUNCTION(BlueprintCallable, Category = "HUD")
	void ShowGameHUD(); // HUD 표시
	UFUNCTION(BlueprintCallable, Category = "Menu")
	void ShowMainMenu(bool bIsRestart); // 메인 메뉴 표시
	UFUNCTION(BlueprintCallable, Category = "Menu")
	void StartGame(); // 게임 시작
// JinPlayerController.cpp
#include "JinGameInstance.h"
#include "Kismet/GameplayStatics.h"
#include "Components/TextBlock.h"

	// 생성자에 새로 만든 변수 초기화
	MainMenuWidgetClass(nullptr),
	MainMenuWidgetInstance(nullptr)
      
void AJinPlayerController::BeginPlay()
{
	Super::BeginPlay();

	if (ULocalPlayer* LocalPlayer = GetLocalPlayer())
	{
		if (UEnhancedInputLocalPlayerSubsystem* Subsystem =
			LocalPlayer->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>())
		{
			if (InputMappingContext)
			{
				Subsystem->AddMappingContext(InputMappingContext, 0);
			}
		}
	}

	// 게임 실행 시 메뉴 레벨에서 메뉴 UI 먼저 표시
	FString CurrentMapName = GetWorld()->GetMapName();
	if (CurrentMapName.Contains("MenuLevel"))
	{
		ShowMainMenu(false);
	}
}

void AJinPlayerController::ShowMainMenu(bool bIsRestart) // 메뉴 UI 표시
{
	if (HUDWidgetInstance) // HUD가 켜져 있다면 제거
	{
		HUDWidgetInstance->RemoveFromParent();
		HUDWidgetInstance = nullptr;
	}

	if (MainMenuWidgetInstance) // 이미 메뉴가 켜져 있다면 제거
	{
		MainMenuWidgetInstance->RemoveFromParent();
		MainMenuWidgetInstance = nullptr;
	}

	if (MainMenuWidgetClass) // 메뉴 UI 생성
	{
		MainMenuWidgetInstance = CreateWidget<UUserWidget>(this, MainMenuWidgetClass);
		if (MainMenuWidgetInstance)
		{
			MainMenuWidgetInstance->AddToViewport();

			bShowMouseCursor = true; // 메뉴에서 마우스 커서 보이기
			SetInputMode(FInputModeUIOnly()); // InputMode를 UI전용으로 변경
		}

		if (UTextBlock* ButtonText = Cast<UTextBlock>(MainMenuWidgetInstance->GetWidgetFromName(TEXT("StartButtonText"))))
		{
			if (bIsRestart) // 게임 새로 시작인지, 재시작인지에 따라 버튼의 텍스트 바꾸기
			{
				ButtonText->SetText(FText::FromString(TEXT("Restart")));
			}
			else
			{
				ButtonText->SetText(FText::FromString(TEXT("start")));
			}
		}
	}
}

void AJinPlayerController::ShowGameHUD() // 게임 HUD 표시
{
	if (HUDWidgetInstance) // HUD가 켜져 있다면 제거
	{
		HUDWidgetInstance->RemoveFromParent();
		HUDWidgetInstance = nullptr;
	}

	if (MainMenuWidgetInstance) // 이미 메뉴가 켜져 있다면 제거
	{
		MainMenuWidgetInstance->RemoveFromParent();
		MainMenuWidgetInstance = nullptr;
	}

	if (HUDWidgetClass) // HUD 생성
	{
		HUDWidgetInstance = CreateWidget<UUserWidget>(this, HUDWidgetClass);
		if (HUDWidgetInstance)
		{
			HUDWidgetInstance->AddToViewport();

			bShowMouseCursor = false; // 마우스 커서 숨기기
			SetInputMode(FInputModeGameOnly()); // InputMode를 Game전용으로 변경
		}

		AJinGameState* JinGameState = GetWorld() ? GetWorld()->GetGameState<AJinGameState>() : nullptr;
		if (JinGameState)
		{
			JinGameState->UpdateHUD(); // HUD 갱신(=초기화)
		}
	}
}

void AJinPlayerController::StartGame() // 게임 시작, 버튼 누를 때 호출될 함수
{
	if (UJinGameInstance* JinGameInstance = Cast<UJinGameInstance>(UGameplayStatics::GetGameInstance(this)))
	{   // GameInstance 데이터 리셋
		JinGameInstance->CurrentLevelIndex = 0;
		JinGameInstance->TotalScore = 0;
	}

	UGameplayStatics::OpenLevel(GetWorld(), FName("BasicLevel")); // BasicLevel 오픈
}
  • 게임 입력 vs UI 입력
    • 게임 플레이 중 메뉴가 활성화되면, UI만 입력을 받도록 하거나, UI + 게임 둘 다 입력을 받도록 할 수 있습니다.
    • 이때 UI에 마우스 포커스가 가도록 만들려면 SetInputMode 계열 함수를 사용해야 합니다. SetInputMode 계열 함수를 통해 PlayerController가 어느 입력을 우선으로 처리할지 결정합니다.
    • UI 전용 입력 모드로 전환하면, 플레이어의 마우스 입력과 키 입력이 먼저 UI로 전달됩니다. 캐릭터 이동이나 시야 회전 등 게임 월드 입력은 잠시 비활성화되고, 버튼 클릭에 집중할 수 있습니다.
    • FInputModeUIOnly 구조체 생성 → SetWidgetToFocus로 포커스할 UI 위젯 지정
    • SetInputMode(InputMode) 호출 → 이제 마우스 클릭이나 키 입력이 UI를 먼저 처리
    • bShowMouseCursor = true로 마우스 커서를 보이게 설정

BP_JinPlayerController에서 WBP_MainMenu를 넣어줘야한다.

이제 GameState에서 흐름에 맞게 PlayerController에서 구현한 함수들을 호출해주면 끝이다.

// JinGameState.cpp
void AJinGameState::StartLevel() // 게임 시작시 처음에 HUD 띄우기
{
	if (APlayerController* PlayerController = GetWorld()->GetFirstPlayerController())
	{
		if (AJinPlayerController* JinPlayerController = Cast<AJinPlayerController>(PlayerController))
		{
			JinPlayerController->ShowGameHUD();
		}
	}
    
void AJinGameState::OnGameOver() // 게임오버되면 메뉴 UI 띄우기
{
	if (APlayerController* PlayerController = GetWorld()->GetFirstPlayerController())
	{
		if (AJinPlayerController* JinPlayerController = Cast<AJinPlayerController>(PlayerController))
		{
			JinPlayerController->ShowMainMenu(true); // true: 텍스트는 Restart로
		}
	}
}

이제 마지막으로 메뉴UI에서 버튼을 누르면 JinPlayerController의 StartGame 함수가 호출되도록 바인딩한다.

이 작업은 에디터에서 한다.

 

버튼을 클릭하고 Detail 패널에서 맨 위에 Is Variable을 체크하면 맨 하단에 Events가 활성화 된다.

버튼 클릭 시 함수가 호출될거니까 On Clicked의 +를 누르면 그래프탭이 열리면서 OnClicked 이벤트 노드가 자동으로 생성된다.

GetPlayerController 노드를 생성하고 호출할 함수가 있는 BP_JinCharacter로 Cast를 해서 Start Game 함수를 가져온다.

 

이제 게임을 실행하면 처음에 메뉴 UI가 나오고 버튼을 누르면 게임이 실행된다.

강의에서는 나오지 않았지만 배운 내용으로 HUD에 체력과 남은 코인도 연동을 해보았다.