TIL_045: 메뉴 UI , HP HUD

김펭귄·2025년 10월 14일

Today What I Learned (TIL)

목록 보기
45/91
post-thumbnail

오늘 학습 키워드

  • 메뉴 UI 만들기

  • HP HUD 만들기

1. 메뉴 UI 만들기

  1. 플레이 버튼을 누르면 Menu UI가 가장 먼저 뜨기
  2. 게임 시작 시 자동으로 HUD를 띄우기
  3. 메뉴에선, UI 입력 모드로 전환하여 입력이 UI에만 작동하도록 구현
  4. 게임이 종료되면 메뉴가 다시 뜨도록 만들기
  • Boarder를 추가하여 색깔과 투명도를 제공

  • 아래와 같은 계층구조로 UI 생성
  • Start버튼은 IsVariable 체크하여 변수로 만들고, On Clicked 이벤트로 게임 시작하도록 함
Start Game은 잠시 후 구현할 함수

2. 메뉴 스테이지 생성

  • 게임 초반, 메뉴와 함께 생성될 메뉴 스테이지(MenuLevel) 생성
  • 프로젝트 세팅에서 게임 시작 맵과 에디터 기본 맵을 해당 맵으로 설정

3. 메뉴 UI 구현

Controller

// header
UCLASS()
class SPARTPROJECT_API AMyPlayerController : public APlayerController
{
	// ... //
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Menu")
	TSubclassOf<UUserWidget> MainMenuWidgetClass;
	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Menu")
	UUserWidget* MainMenuWidgetInstance;
    
    UFUNCTION(BlueprintCallable, Category = "HUD")
	void ShowHUD();
	UFUNCTION(BlueprintCallable, Category = "Menu")
	void ShowMainMenu(bool bIsRestart);
	UFUNCTION(BlueprintCallable, Category = "Menu")
	void StartGame();
};
  • ShowMainMenu : 게임 재시작일 경우 Start대신 Restart 텍스트 출력
  • StartGame : Start 또는 Restart 버튼 클릭 시 게임 시작시키는 함수
// cpp
void AMyPlayerController::BeginPlay()
{
	// ... //

	// 레벨(맵)이 바뀔 때마다, 플레이어컨트롤도 새롭게 생성됨
	// 따라서 여기 BeginPlay에서 현재 Menu일경우, 메뉴 UI 띄우도록 설정
	FString CurrentMapName = GetWorld()->GetMapName();
	if (CurrentMapName.Contains("MenuLevel"))
	{
		ShowMainMenu(false);
	}
}

void AMyPlayerController::ShowMainMenu(bool bIsRestart)
{
	// HUD가 화면에 띄어져있다면, 화면에서 제거
	if (HUDWidgetInstance)
	{
		HUDWidgetInstance->RemoveFromParent();
		HUDWidgetInstance = nullptr;
	}
	// MainMenu도 혹시나 있다면, 제거
	if (MainMenuWidgetInstance)
	{
		MainMenuWidgetInstance->RemoveFromParent();
		MainMenuWidgetInstance = nullptr;
	}

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

			// 메뉴창에서는 마우스 커서가 보여야 함
			bShowMouseCursor = true;
			// Input이 UI에만 영향줘야지, 게임에 영향주면 안 됨
			SetInputMode(FInputModeUIOnly());

			// 텍스트 연동
			if (UTextBlock* TextBlock = Cast<UTextBlock>(MainMenuWidgetInstance
            	->GetWidgetFromName("StartButtonText")))
			{
				if (bIsRestart)
                	TextBlock->SetText(FText::FromString(TEXT("Restart")));
				else
                	TextBlock->SetText(FText::FromString(TEXT("Start")));
			}
		}
	}
}

// ShowMainMenu와 비슷
void AMyPlayerController::ShowHUD()
{
	// HUD, MainMenu 인스턴스 제거 로직

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

			// 인게임이므로, 마우스 안 보이게
			bShowMouseCursor = false;
			// Input이 게임에만 영향주도록
			SetInputMode(FInputModeGameOnly());
		}
	}
}

void AMyPlayerController::StartGame()
{
	// 게임을 재시작 시키려면, GameInstance에 있는 데이터들을 처음으로 초기화해주면 됨
	if (UMyGameInstance* MyGameInstance = Cast<UMyGameInstance>(
    	UGameplayStatics::GetGameInstance(this)))
	{
		MyGameInstance->TotalScore = 0;
		MyGameInstance->CurrentLevelIndex = 0;
	}

	// 그리고 게임 시작해주기(레벨 열기)
	UGameplayStatics::OpenLevel(GetWorld(), FName("BasicLevel"));
}
  • RemoveFromViewport : 옛날 방식
  • RemoveFromParent : 부모 위젯이 있으면 부모로부터 제거. 부모 없이 그냥 뷰포트에 있는경우 뷰포트에서 제거
  • 컨트롤러 블루프린트에서 HUD와 Menu 위젯 클래스 연결해주기

GameState

// cpp

void AMyGameState::StartLevel()
{
	// ... //
    // 레벨 시작했으니 HUD 인스턴스 있으면 표시
	if (APlayerController* PlayerController = GetWorld()->GetFirstPlayerController())
	{
		if (AMyPlayerController* MyPlayerController = 
        	Cast<AMyPlayerController>(PlayerController))
		{
			MyPlayerController->ShowHUD();
            // 게임 일시정지 해제
			MyPlayerController->SetPause(false);
		}
	}
    // ... //
}

void AMyGameState::OnGameOver()
{
	// 게임 종료 시, 메인 메뉴 UI 띄우기
	if (APlayerController* PlayerController = GetWorld()->GetFirstPlayerController())
	{
		if (AMyPlayerController* MyPlayerController = 
        	Cast<AMyPlayerController>(PlayerController))
		{
        	// 게임 일시정지
			MyPlayerController->SetPause(true);
			MyPlayerController->ShowMainMenu(true);
		}
	}
}

4. 결과

5. 게임 오버 UI

  • 기존 메인 메뉴 UI를 수정하여 게임 오버 시, 사진과 같이 게임 오버와 점수 뜨도록 구현

  • Game Over!의 경우, Render Opacity 애니메이션을 통해 효과를 추가

  • 게임 오버, 점수 텍스트는 게임 오버시에만 나타나야하므로, Hidden 처리

  • 그리고 아래와 같이 OnGameOver 함수를 만들어, 게임 오버시 함수 호출하여 게임오버 UI가 보이도록 함

6. 게임 오버 UI 코드 구현

MyPlayerController

void AMyPlayerController::ShowMainMenu(bool bIsRestart)
{
	// TextBlock 찾아서 Total Score 연동해주는 로직
	UTextBlock* TotalScoreTextBlock = Cast<UTextBlock>(MainMenuWidgetInstance
    	->GetWidgetFromName(FName(TEXT("TotalScoreText"))));
	if (TotalScoreTextBlock)
	{
		UMyGameInstance* MyGameInstance = GetWorld()
        	->GetGameInstance<UMyGameInstance>();
		if (MyGameInstance)
		{
			TotalScoreTextBlock->SetText(FText::FromString(FString::Printf(
            	TEXT("Total Score : %d"), MyGameInstance->TotalScore)));
		}			
	}
    
    // 재시작 시, OnGameOver 함수 호출
	if (bIsRestart)
	{
		// 이름으로 함수 찾기
		UFunction* OnGameOver = MainMenuWidgetInstance
        	->FindFunction(TEXT("OnGameOver"));
		if (OnGameOver)
		{
			MainMenuWidgetInstance->ProcessEvent(OnGameOver, nullptr);
		}
	}
}
  • ProcessEvent : 이벤트 실행. 함수의 매개인자 없을경우 nullptr 넣어줌

7. 플레이어 체력 UI 구현

  • 체력 UI도 마찬가지로, 위젯 블루프린트(WBP_HP)를 새로 만들고, 텍스트만 추가

  • 캐릭터 클래스에 위젯 컴포넌트 추가하여 만든 위젯을 사용

위젯 컴포넌트

  • 위젯을 3D World에 배치할 수 있게 해주는 도구

  • 2D로만 보이던 위젯을 객체에 붙여, 3D월드에 붙여주는 컴포넌트

  1. 스크린 모드 : UI가 화면에 고정. 항상 정면을 보여줌
  2. 월드 모드 : UI가 부모 객체의 회전에 따라 같이 회전함
  • 체력 UI는 캐릭터 회전과 상관없이 정면으로 보여야하므로, 스크린 모드 선택

코드 구현

// MyCharacter.h
class UWidgetComponent;

UCLASS()
class SPARTPROJECT_API AMyCharacter : public ACharacter
{
	// ... //
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI")
	UWidgetComponent* OverHeadWidget;
    
    void UpdateOverHeadHP();
};


// MyCharacter.cpp
#include "Components/WidgetComponent.h"

AMyCharacter::AMyCharacter()
{
	OverHeadWidget= CreateDefaultSubobject<UWidgetComponent>(TEXT("OverHeadWidget"));
    // 스켈레탈 메시 컴포넌트에 부착
	OverHeadWidget->SetupAttachment(GetMesh());
	OverHeadWidget->SetWidgetSpace(EWidgetSpace::Screen);
}

void AMyCharacter::UpdateOverHeadHP()
{
	if (!OverHeadWidget) return;

	UUserWidget* OverHeadWidgetInstance = OverHeadWidget->GetUserWidgetObject();
	if (OverHeadWidgetInstance)
	{
		UTextBlock* OverHeadText = Cast<UTextBlock>(OverHeadWidgetInstance
        	->GetWidgetFromName(FName(TEXT("OverHeadHP"))));
		if (OverHeadText)
		{
			OverHeadText->SetText(FText::FromString(
            	FString::Printf(TEXT("HP : %.0f / %.0f"), Health, MaxHealth)));
		}
	}
}
  • GetMesh : 부모 클래스(캐릭터)에 있는 함수로, 스켈레탈 메시 컴포넌트를 가져옴
  • SetWidgetSpace : 스크린 모드, 월드 모드 중에 선택
    • EWidgetSpace::Screen : 스크린 모드
    • EWidgetSpace::World : 월드 모드
  • GetUserWidgetObject : 위젯 컴포넌트에 연결된 '사용자 정의 UserWidget 객체' 반환. 내가 생성한 UI 클래스 인스턴스를 얻을 때 사용

    • GetWidget : 내부 UI 구조에서 좀 더 낮은 레벨의 위젯 포인터를 반환
    • 보통 UserWidget 객체가 필요할 때 GetUserWidgetObject()를 사용
  • UpdateOverHeadHP를 BeginPlay, AddHealth, TakeDamage 함수에 추가하여 호출

캐릭터 블루프린트에 적용

  • OverHeadWidget컴포넌트 디테일 창에서 Widget Class를 설정.

  • 위젯 위치를 확인하기 위해, Space를 World로 바꾸고 위치 수정 후, 다시 Screen으로

8. Game Over UI 결과 영상

profile
반갑습니다

0개의 댓글