HUD(Heads-Up Display)
이번에는 현재 레벨, 현재 남은 시간, 현재 점수같은 정보를 화면에 표시한다.
- Canvas 기반 HUD
- HUD 클래스를 상속하여 구현
- 텍스트나 이미지를 2D 형태로 그림
- 예전 방식
- 요즘은 전부 UMG로 HUD 구현
- UMG(Unreal Motion Graphics)
- Widget Blueprint 기능 사용
UI 만들기


위젯 블루프린트는 접두어로 WBP를 붙여준다.


기본적으로 UI요소를 Hierarchy로 끌어서 배치하게 된다.
대표적 위젯
표시(Display) 위젯 – 화면에 정보를 보여주는 요소
- Text: 문자열 출력, 점수·상태·안내 문구 표시
- Image: 아이콘·배경 등 이미지 표시
- Progress Bar: 값의 비율을 시각화 (체력, 로딩 등)
- Border: 테두리와 배경 제공, 내부 위젯을 묶어 스타일링
입력(Input) 위젯 – 사용자 상호작용 요소
- Button: 클릭 이벤트 처리 (메뉴, 확인 등)
- Check Box: 체크/해제 상태 선택
- Editable Text Box: 텍스트 입력 (닉네임, 채팅 등)
- Slider: 값 조절 (볼륨, 밝기 등)
컨테이너(Layout) 위젯 – UI 배치의 핵심
- Canvas Panel: 좌표 기반 자유 배치, 반응형에는 취약
- Horizontal Box: 자식 위젯을 가로로 정렬
- Vertical Box: 자식 위젯을 세로로 정렬
- Overlay: 위젯을 겹쳐서 배치
- Grid Panel: 행/열 구조로 배치 (인벤토리 등)
- Scroll Box: 스크롤 가능한 영역 제공
- Size Box: 위젯 크기를 강제 지정
고급/보조 위젯 – 특정 기능을 위한 요소
- Widget Switcher: 여러 위젯 중 하나만 표시 (탭 전환 등)
- Spacer: 빈 공간으로 간격 조절
- Scale Box: 화면 크기에 따라 자동 스케일 조정
- Rich Text: 스타일이 적용된 텍스트 출력











UI는 플레이어로부터 입력을 받아야하므로 플레이어 컨트롤러에 연결해준다.
SpartaPlayerController.h에 아래와 같이 변수를 추가한다.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "UI")
TSubclassOf<UUserWidget> HUDWidgetClass;
SpartaPlayerController.cpp에는 아래와 같이 헤더를 추가하고 BeginPlay()를 수정한다.
#include "Blueprint/UserWidget.h"
void ASpartaPlayerController::BeginPlay()
{
Super::BeginPlay();
// 플레이어를 가져오기 -> 향상된 입력 관리하는 서브시스템 획득하기 -> IMC 객체 확인하고 서브시스템에 IMC 추가하며 우선순위를 최우선으로 두고 활성화시키기
if (ULocalPlayer* LocalPlayer = GetLocalPlayer()) // GetLocalPlayer는 플레이어의 로컬 객체를 가져옴
{
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = LocalPlayer->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>()) // 입력 시스템을 관리하는 서브시스템. IMC를 추가하거나 삭제하는 역할을 한다.
{
if (InputMappingContext) // 인풋 매핑 컨텍스트가 존재하면
{
Subsystem->AddMappingContext(InputMappingContext, 0); // InputMappingContext로 활성화함. 0은 우선순위를 뜻함.
}
}
}
// HUDWidget 인스턴스 생성
if (HUDWidgetClass)
{
UUserWidget* HUDWidget = CreateWidget<UUserWidget>(this, HUDWidgetClass); // UI 위젯 인스턴스 생성. this는 위젯의 소유자. HUDWidgetClass는 생성하려는 클래스
if (HUDWidget) {
HUDWidget->AddToViewport(); // 뷰포트상에 렌더링
}
}
}
이렇게 하고 나서 build.cs를 수정해야한다.
// Copyright Epic Games, Inc. All Rights Reserved.
using UnrealBuildTool;
public class SpartaProject : ModuleRules
{
public SpartaProject(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[] {
"Core", // 엔진의 기본 기능
"CoreUObject", // 주로 리플렉션 시스템
"Engine", // 게임 엔진
"InputCore", // 입력 시스템
"EnhancedInput", // 향상된 입력 시스템
"UMG" // UMG라는 모듈을 넣어줘야 HUD를 사용할 수 있다.
});
PrivateDependencyModuleNames.AddRange(new string[] { });
// Uncomment if you are using Slate UI
// PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
// Uncomment if you are using online features
// PrivateDependencyModuleNames.Add("OnlineSubsystem");
// To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true
}
}
build.cs에 UMG를 추가해준다.
PublicDependencyModuleNames은 공개된 종속 모듈이다. 필수적으로 사용하는 엔진 기능이라고 보면 된다.
이제 에디터를 켜서 HUD를 지정해준다.






데이터 연동하기


점수는 GameState에서 만들었던 GameInstance의 TotalScore에서 가져오면 된다.







하지만 이 방식으로는 Tick 함수를 통해 매 Tick에 점수를 계속 불러오는 것이라서 리소스 낭비가 된다. 데이터가 변경될 때만 갱신을 하는 것이 효율적이다. 이를 코드로 구현해본다.

PlayerController에서 붙여주는 HUDWidget을 GameState에서 갱신할 수 있게 해줘야한다.
SpartaPlayerController.h에 아래와 같은 멤버 변수와 멤버 함수를 설정한다.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HUD")
UUserWidget* HUDWidgetInstance;
UFUNCTION(BlueprintCallable Category = "HUD")
UUserWidget* GetHUDWidget() const;
cpp 파일의 BeginPlay()를 변경하고 GetHUDWidget 함수를 만든다.
void ASpartaPlayerController::BeginPlay()
{
Super::BeginPlay();
// 플레이어를 가져오기 -> 향상된 입력 관리하는 서브시스템 획득하기 -> IMC 객체 확인하고 서브시스템에 IMC 추가하며 우선순위를 최우선으로 두고 활성화시키기
if (ULocalPlayer* LocalPlayer = GetLocalPlayer()) // GetLocalPlayer는 플레이어의 로컬 객체를 가져옴
{
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = LocalPlayer->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>()) // 입력 시스템을 관리하는 서브시스템. IMC를 추가하거나 삭제하는 역할을 한다.
{
if (InputMappingContext) // 인풋 매핑 컨텍스트가 존재하면
{
Subsystem->AddMappingContext(InputMappingContext, 0); // InputMappingContext로 활성화함. 0은 우선순위를 뜻함.
}
}
}
// HUDWidget 인스턴스 생성
if (HUDWidgetClass)
{
HUDWidgetInstance = CreateWidget<UUserWidget>(this, HUDWidgetClass); // UI 위젯 인스턴스 생성. this는 위젯의 소유자. HUDWidgetClass는 생성하려는 클래스
if (HUDWidgetInstance) {
HUDWidgetInstance->AddToViewport(); // 뷰포트상에 렌더링
}
}
}
UUserWidget* ASpartaPlayerController::GetHUDWidget() const
{
return HUDWidgetInstance;
}
SpartaGameState에 UpdateHUD() 함수와 FTimerHandle HUDUpdateTimerHandle를 추가한다.
void UpdateHUD();
FTimerHandle HUDUpdateTimerHandle;
// Fill out your copyright notice in the Description page of Project Settings.
#include "SpartaGameState.h"
#include "SpawnVolume.h"
#include "CoinItem.h"
#include "SpartaGameInstance.h"
#include "SpartaPlayerController.h"
#include "Kismet/GameplayStatics.h"
#include "Components/TextBlock.h"
#include "Blueprint/UserWidget.h"
ASpartaGameState::ASpartaGameState()
{
// 변수 초기화
Score = 0;
SpawnedCoinCount = 0;
CollectedCoinCount = 0;
LevelDuration = 30.0;
CurrentLevelIndex = 0;
MaxLevels = 3;
}
void ASpartaGameState::BeginPlay()
{
Super::BeginPlay();
StartLevel(); // 레벨 시작
GetWorldTimerManager().SetTimer(
HUDUpdateTimerHandle,
this,
&ASpartaGameState::UpdateHUD,
0.1f,
true
);
}
int32 ASpartaGameState::GetScore() const
{
return Score;
}
void ASpartaGameState::AddScore(int32 Amount)
{
if (UGameInstance* GameInstance = GetGameInstance())
{
USpartaGameInstance* SpartaGameInstance = Cast<USpartaGameInstance>(GameInstance);
if (SpartaGameInstance)
{
SpartaGameInstance->AddToScore(Amount);
}
}
Score += Amount; // 점수 획득
UpdateHUD();
UE_LOG(LogTemp, Warning, TEXT("Score: %d"), Score);
}
void ASpartaGameState::OnLevelTimeUp()
{
EndLevel();
}
void ASpartaGameState::EndLevel()
{
GetWorldTimerManager().ClearTimer(LevelTimerHandle);
CurrentLevelIndex++;
if (UGameInstance* GameInstance = GetGameInstance())
{
USpartaGameInstance* SpartaGameInstance = Cast<USpartaGameInstance>(GameInstance);
if (SpartaGameInstance)
{
AddScore(Score);
SpartaGameInstance->CurrentLevelIndex = CurrentLevelIndex;
}
}
if (CurrentLevelIndex >= MaxLevels)
{
OnGameOver();
return;
}
if (LevelMapNames.IsValidIndex(CurrentLevelIndex))
{
UGameplayStatics::OpenLevel(GetWorld(), LevelMapNames[CurrentLevelIndex]); // 다음 레벨 열기 / 다음 레벨이 열리면 GameState도 새로 생성됨
}
else
{
OnGameOver();
}
}
void ASpartaGameState::OnGameOver()
{
UpdateHUD();
// UI를 열어주거나 재시작
UE_LOG(LogTemp, Warning, TEXT("Game Over!!"));
}
void ASpartaGameState::StartLevel()
{
// -- 규칙 지정 --
// UI - 게임 시작 버튼
// 레벨에서 40개의 아이템이 소환이 된다.
// 코인을 다 얻으면 시간제한 상관없이 다음 레벨로 넘어간다.
// 한 레벨당 30초의 시간이 주어지며, 타임오버가 되어도 바로 다음 레벨로 넘어갈 수 있다.
// 체력이 0이 되면 바로 게임오버가 된다.
// UI - 게임 종료 이후 재시작을 할 수 있다.
// 레벨이 시작될 때 동작
if (UGameInstance* GameInstance = GetGameInstance())
{
USpartaGameInstance* SpartaGameInstance = Cast<USpartaGameInstance>(GameInstance);
if (SpartaGameInstance)
{
CurrentLevelIndex = SpartaGameInstance->CurrentLevelIndex;
}
}
SpawnedCoinCount = 0; // 스폰된 코인 카운트
CollectedCoinCount = 0; // 수집된 코인 카운트
TArray<AActor*> FoundVolumes; // SpawnVolume 찾기
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();
if (SpawnedActor && SpawnedActor->IsA(ACoinItem::StaticClass())) // IsA는 해당 클래스인지 아닌지 체크하며 하위 클래스까지 확인을 해준다.
{
SpawnedCoinCount++;
}
}
}
}
UpdateHUD();
GetWorldTimerManager().SetTimer(
LevelTimerHandle,
this,
&ASpartaGameState::OnLevelTimeUp,
LevelDuration,
false
);
UE_LOG(LogTemp, Warning, TEXT("Level %d Start! Spawned %d coin."), CurrentLevelIndex + 1, SpawnedCoinCount);
}
void ASpartaGameState::OnCoinCollected()
{
CollectedCoinCount++;
UE_LOG(LogTemp, Warning, TEXT("Coin Collected: %d / %d"), CollectedCoinCount, SpawnedCoinCount); // 코인 몇 개를 취득했는지 표시
if (SpawnedCoinCount > 0 && CollectedCoinCount >= SpawnedCoinCount)
{
EndLevel();
}
}
void ASpartaGameState::UpdateHUD()
{
UE_LOG(LogTemp, Warning, TEXT("UpdateHUD Called"));
if (APlayerController* PlayerController = GetWorld()->GetFirstPlayerController())
{
UE_LOG(LogTemp, Warning, TEXT("PC Called"));
if (ASpartaPlayerController* SpartaPlayerController = Cast<ASpartaPlayerController>(PlayerController))
{
UE_LOG(LogTemp, Warning, TEXT("SpartaPlayer Called"));
if (UUserWidget* HUDWidget = SpartaPlayerController->GetHUDWidget())
{
UE_LOG(LogTemp, Warning, TEXT("Widget Called"));
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())
{
USpartaGameInstance* SpartaGameInstance = Cast<USpartaGameInstance>(GameInstance);
if (SpartaGameInstance)
{
ScoreText->SetText(FText::FromString(FString::Printf(TEXT("Score: %d"), SpartaGameInstance->TotalScore)));
}
}
}
if (UTextBlock* LevelText = Cast<UTextBlock>(HUDWidget->GetWidgetFromName(TEXT("Level"))))
{
LevelText->SetText(FText::FromString(FString::Printf(TEXT("Level: %d"), CurrentLevelIndex + 1))); // 위젯의 텍스트를 세팅
}
}
}
}
}
SpartaPlayerController.cpp도 수정해준다.
// Fill out your copyright notice in the Description page of Project Settings.
#include "SpartaPlayerController.h"
#include "EnhancedInputSubsystems.h"
#include "SpartaGameState.h"
#include "Blueprint/UserWidget.h"
ASpartaPlayerController::ASpartaPlayerController():
InputMappingContext(nullptr), // 아무것도 할당하지 않는 것보다는 nullptr을 할당해주는 것이 좋다.
MoveAction(nullptr),
JumpAction(nullptr),
LookAction(nullptr),
SprintAction(nullptr),
HUDWidgetClass(nullptr),
HUDWidgetInstance(nullptr)
{
}
void ASpartaPlayerController::BeginPlay()
{
Super::BeginPlay();
// 플레이어를 가져오기 -> 향상된 입력 관리하는 서브시스템 획득하기 -> IMC 객체 확인하고 서브시스템에 IMC 추가하며 우선순위를 최우선으로 두고 활성화시키기
if (ULocalPlayer* LocalPlayer = GetLocalPlayer()) // GetLocalPlayer는 플레이어의 로컬 객체를 가져옴
{
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = LocalPlayer->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>()) // 입력 시스템을 관리하는 서브시스템. IMC를 추가하거나 삭제하는 역할을 한다.
{
if (InputMappingContext) // 인풋 매핑 컨텍스트가 존재하면
{
Subsystem->AddMappingContext(InputMappingContext, 0); // InputMappingContext로 활성화함. 0은 우선순위를 뜻함.
}
}
}
// HUDWidget 인스턴스 생성
if (HUDWidgetClass)
{
HUDWidgetInstance = CreateWidget<UUserWidget>(this, HUDWidgetClass); // UI 위젯 인스턴스 생성. this는 위젯의 소유자. HUDWidgetClass는 생성하려는 클래스
if (HUDWidgetInstance) {
HUDWidgetInstance->AddToViewport(); // 뷰포트상에 렌더링
}
}
ASpartaGameState* SpartaGameState = GetWorld() ? GetWorld()->GetGameState<ASpartaGameState>() : nullptr;
if (SpartaGameState)
{
SpartaGameState->UpdateHUD();
}
}
UUserWidget* ASpartaPlayerController::GetHUDWidget() const
{
return HUDWidgetInstance;
}'게임 개발 > 학습 일지' 카테고리의 다른 글
| 언리얼 엔진 심화 3주차 - 콜리전, 트레이스 및 데미지 타입 (0) | 2026.04.30 |
|---|---|
| 언리얼 C++ - UI 애니메이션 효과 및 3D 위젯 UI 구현하기 (0) | 2026.04.29 |
| 언리얼 C++ - State Machine 설계를 통한 캐릭터 동작 애니메이션 적용하기 (0) | 2026.04.24 |
| 언리얼 C++ - 캐릭터 체력 및 점수 관리 시스템 구현하기 (0) | 2026.04.23 |
| 언리얼 C++ - 아이템 스폰 및 레벨 데이터 관리하기 (0) | 2026.04.22 |