게임 개발/학습 일지

언리얼 C++ - UI 위젯 설계와 실시간 데이터 연동하기

이개 2026. 4. 27. 09:42

HUD(Heads-Up Display)

이번에는 현재 레벨, 현재 남은 시간, 현재 점수같은 정보를 화면에 표시한다.

 

  • Canvas 기반 HUD
    • HUD 클래스를 상속하여 구현
    • 텍스트나 이미지를 2D 형태로 그림
    • 예전 방식
    • 요즘은 전부 UMG로 HUD 구현
  • UMG(Unreal Motion Graphics)
    • Widget Blueprint 기능 사용

 

UI 만들기

UI 폴더 생성
Widet Blueprint로 WBP_HUD라는 제목의 파일 생성

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

 

Designer 탭에서는 캔버스 그리기, Graph 탭에서는 데이터 연동을 해줄 수 있다.

 

Palette의 UI 요소들

 

기본적으로 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: 스타일이 적용된 텍스트 출력

 

가장 기본이 되는 Canvas Panel.
Canvas Panel을 화면에 끌어다 놓고 배치를 시작한다.
캔버스 우상단 스크린 사이즈에서 16:9를 선택해준다.

 

Text를 드래그앤 드롭한다.

 

내용은 Score: 100, 폰트 크기는 60정도로 설정한다.
화면의 원하는 위치에 놓고 Justification으로 정렬도 해준다.
폰트를 추가하려면 폰트 파일을 콘텐트에 추가하면 된다.
Text를 복제하여 우측 상단에도 배치해준다.
Score와 Time의 위치를 표시한다.

 

Level까지 지정한다.

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를 지정해준다.

BP_SpartaPlayerController
플레이의 자세히 메뉴에서 Advanced Settings로 들어간다.

 

New WindowPosition을 1920x1080으로 수정해준다.

 

뷰포트가 아닌 New Editor Window 옵션으로 실행해주면 설정한 해상도로 바뀐다.
화면에 HUD가 보인다.

 

 

데이터 연동하기

위젯 블루프린트에서 Text 옆에 있는 Create Binding을 클릭한다.

 

 

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

 

이전에 만든 GameInstance 클래스

 

Create Binding 이벤트 그래프에서 Get Game Instance 노드를 생성한다.

 

Cast를 진행한다.
Get Total Score를 생성한다.

 

배경에 우클릭하여 Format Text를 진행한다.
노드를 연결, 저장하고 테스트해본다.

 

잘 연동되는 것을 볼 수 있다.

 

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

 

Remove Binding으로 바인딩을 해제한다.

 

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;
}