내배캠/Unreal Engine

UI 위젯 설계와 실시간 데이터 연동

동그래님 2025. 2. 6. 17:12

 

📌 WBP 생성하기

부모 클래스인 User Widget을 선택해 WBP를 생성한다.

 

🔎 Widget Blueprint 란?

언리얼에서 UI를 시각적으로 설계할 수 있도록 제공되는 에디터용 블루프린트이다.
이 WBP에서 TextBlock, Button, Image 등 다양한 UI요소를 드래그 앤 드롭으로 간편하게 배치할 수 있다.
여기서 만든 WBP를 PlayerController에서 ViewPort에 표시하도록 할 수 있게한다.

 

 

🔎 UI 요소란?

WBP 내부에서 좌측을 살펴보면 Palette 창이 보인다.
여기서 다양한 UI 요소를 드래그해서 드롭하면 아주 간편하게 Viewport에 배치할 수 있다.
  • Text: TextBlock을 의미하고, 캐릭터 체력이나 점수, 남은시간, 킬로그 등 많은 곳에 사용된다.
  • Button: "게임 시작", "게임 종료", "옵션" 등 사용자가 클릭할 수 있는 이벤트 버튼으로 사용된다.
  • Progress Bar: 체력 게이지나 로딩 게이지 등 시각적 표현이 필요할 때 사용된다.

 

 

📌 WBP 에서 점수, 시간, 레벨 표시를 위한 UI Widget 디자인

  • 좌측 Palette 창에서 Canvers Panel를 배치한다.
  • 그 다음 Text block를 배치해, 우측 Details 창에서 위치와 크기 등을 조절하며 UI의 위치를 잡아준다.
  • 필요한 경우 폰트도 Import하여 사용할 수 있다.

 

  • 우측 Details 창의 바로 아래를 살펴보면 Text Block의 이름을 지정해줄 수 있다.
  • 추후 C++ 코드에서 UUserWidget::GetWidgetFormName(const FName& Name) 메서드를 사용해
    해당 Text Block의 이름으로 검색해 위젯을 가져오기 때문에 각 텍스트 기능에 맞춰 이름을 설정해두자.

 

 

  • 우측 상단의 Screen Size를 조절해 각 모니터 사이즈에서 어떻게 보이게 될지 확인할 수 있다.

 


 

📌 PlayerController에서  HUD 생성 로직 추가

✅ PlayerController.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "StrikeZonePlayerController.generated.h"

UCLASS()
class STRIKEZONE_API AStrikeZonePlayerController : public APlayerController
{
	GENERATED_BODY()
    
protected:
	virtual void BeginPlay() override;

public:
	AStrikeZonePlayerController();

	// 에디터에서 UMG 위젯 클래스를 할당 받을 변수
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HUD")
	TSubclassOf<UUserWidget> HUDWidgetClass;
    
    // UUserWidget 인스턴스 저장 변수
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HUD")
	UUserWidget* HUDWidgetInstance;
	
    // GameState에서 HUD Widget Instance에 접근하기 위한 getter 함수
	UFUNCTION(BlueprintCallable, Category = "HUD")
	UUserWidget* GetHUDWidget() const { return HUDWidgetInstance; }

};

 

 

✅ PlayerController.cpp

#include "StrikeZonePlayerController.h"
#include "SZ_GameState.h"
#include "Blueprint/UserWidget.h"

AStrikeZonePlayerController::AStrikeZonePlayerController() : HUDWidgetInstance(nullptr) {}

void AStrikeZonePlayerController::BeginPlay()
{
	Super::BeginPlay();

	if (HUDWidgetClass)
	{
    	// 위젯 생성
		HUDWidgetInstance = CreateWidget<UUserWidget>(this, HUDWidgetClass);
		if (HUDWidgetInstance)
		{	
        	// 위젯 viewport에 표시
			HUDWidgetInstance->AddToViewport();
		}
	}
    
    // 게임이 시작되고 월드가 있다면 GameState를 가져온다.
    // GameState의 UpdateHUD() 함수를 호출해 게임이 시작되면 바로 HUD 갱신
	ASZ_GameState* GameState = GetWorld() ? GetWorld()->GetGameState<ASZ_GameState>() : nullptr;
	if (GameState)
	{
		GameState->UpdateHUD();
	}
}

 

 

🚫 빌드 오류

PlayerController에서 코드를 작성하고 빌드하게 되면, 아래와 같은 오류가 발생하게 된다.

PlayerController에서 CreateWidget이 작동하려면, UMG 모듈이 빌드 설정에 추가 되어있어야한다.
ProjectName.Build.cs 라는 코드를 열어 아래와 같이 "UMG" 을 추가해주면 오류가 해결된다.

 


 

📌 HUD Widget과 Game State 데이터 연동

🔎WBP에서 바인딩 방식

  1. Text Block 클릭 후 우측 Details 창에서 체인 아이콘을 눌러 "Create Binding" 클릭
  2. Event Graph로 넘어오게되면 GameInstance 가져오기
  3. GameInstance에 있는 Total Score를 Text로 변환해서 Return Node에 값 전달

🔴 단점:

이 바인딩 방식은 초심자에게 조금 더 간편하게 데이터를 연동할 수 있게 한다는 장점이 있다.
하지만 Tick과 같이 매 프레임마다 데이터를 연동해 갱신하기 때문에, UI가 많고 복잡해질수록 성능에 이슈가 생길 수 있다.

따라서 C++에서 UTextBlock::SetText(FText Intext) 함수를 데이터 갱신이 필요한 시점에 직점 호출하는 방식으로 사용한다면,
필요할 때만 UI를 업데이트할 수 있어 퍼포먼스에 유리할 수 있다.

 

 

🔎 SetText 방식으로 데이터 반영해 위젯 갱신하기

✅ GameState.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/GameState.h"
#include "SZ_GameState.generated.h"

UCLASS()
class STRIKEZONE_API ASZ_GameState : public AGameState
{
	GENERATED_BODY()
	
public:
	ASZ_GameState();

	virtual void BeginPlay() override;

	FTimerHandle LevelTimerHandle;
	FTimerHandle HUDUpdateTimerHandle;

	void StartLevel();
	void EndLevel();
	void UpdateHUD();
};

 

✅ GameState.cpp

#include "SZ_GameState.h"
#include "SZ_GameInstance.h"
#include "StrikeZonePlayerController.h"
#include "PlayerCharacter.h"
#include "Kismet/GameplayStatics.h"
#include "ItemSpawnVolume.h"
#include "CoinItem.h"
#include "Components/TextBlock.h"
#include "Components/ProgressBar.h"
#include "Blueprint/UserWidget.h"

ASZ_GameState::ASZ_GameState()
{
	Score = 0;
	SpawnedCoinCount = 0;
	CollectedCoinCount = 0;
	LevelDuration = 30.0f;
	CurrentLevelIndex = 0;
	MaxLevels = 3;
}

void ASZ_GameState::BeginPlay()
{
	Super::BeginPlay();

	StartLevel();

	GetWorldTimerManager().SetTimer(
		HUDUpdateTimerHandle,
		this,
		&ASZ_GameState::UpdateHUD,
		0.1f,
		true
	);
}

void ASZ_GameState::UpdateHUD()
{
	if (APlayerController* PlayerController = GetWorld()->GetFirstPlayerController())
	{
		if (AStrikeZonePlayerController* SZ_PlayerController = Cast<AStrikeZonePlayerController>(PlayerController))
		{
			if (UUserWidget* HUDWidget = SZ_PlayerController->GetHUDWidget())
			{
				// Time TextBlock을 가져와서 남은 시간 데이터를 연동해 SetText로 문자열 설정
				if (UTextBlock* TimeText = Cast<UTextBlock>(HUDWidget->GetWidgetFromName(TEXT("Time"))))
				{
					float RemainingTime = GetWorldTimerManager().GetTimerRemaining(LevelTimerHandle);
					TimeText->SetText(FText::FromString(FString::Printf(TEXT("Time: %.1f"), RemainingTime)));
				}
				// Score TextBlock을 가져와서 남은 시간 데이터를 연동해 SetText로 문자열 설정
				if (UTextBlock* ScoreText = Cast<UTextBlock>(HUDWidget->GetWidgetFromName(TEXT("Score"))))
				{
					if (UGameInstance* GameInstance = GetGameInstance())
					{
						USZ_GameInstance* SZ_GameInstance = Cast<USZ_GameInstance>(GameInstance);
						if (SZ_GameInstance)
						{
							ScoreText->SetText(FText::FromString(FString::Printf(TEXT("Score: %d"), SZ_GameInstance->TotalScore)));
						}
					}
				}
				// Level TextBlock을 가져와서 남은 시간 데이터를 연동해 SetText로 문자열 설정
				if (UTextBlock* LevelIndexText = Cast<UTextBlock>(HUDWidget->GetWidgetFromName(TEXT("Level"))))
				{
					LevelIndexText->SetText(FText::FromString(FString::Printf(TEXT("Level: %d"), CurrentLevelIndex + 1)));
				}
				// HP_Bar ProgressBar를 가져와서 체력 게이지 연동
				if (UProgressBar* HP_Bar = Cast<UProgressBar>(HUDWidget->GetWidgetFromName(TEXT("HP_Bar"))))
				{
					if (APlayerCharacter* PlayerCharacter = Cast<APlayerCharacter>(SZ_PlayerController->GetPawn()))
					{
						float CurrentHP = PlayerCharacter->GetHealth();
						float MaxHP = PlayerCharacter->GetMaxHealth();
						float HP_Percent = FMath::Clamp(CurrentHP / MaxHP, 0.0f, 1.0f);

						HP_Bar->SetPercent(HP_Percent);
					}
				}
			}
		}
	}
}

 

📍UpdateHUD( ):

  • 이전에 PlayerController에 UUserWidget Instance를 반환하는 Getter 함수인 GetHUDWidget( ) 를 호출해 위젯 인스턴스를 가져온다.
  • GetWidgetFormName( ) 함수를 통해 에디터에서 설정했던 TextBlock의 이름으로 해당 TextBlock을 가져온다.
  • UTextBlock::SetText( ) 함수로 데이터의 값을 연동해 텍스트를 설정한다.
  • UProgressBar::SetPercent( ) 함수로 게이지를 연동

📍BeginPlay( ) 에서 UpdateHUD 함수 타이머로 반복 호출:

  • HUDUpdateTimerHandle을 헤더 파일에 선언
  • 0.1초 마다 UpdateHUD 함수를 반복적으로 호출하도록 SetTimer 설정
BeginPlay 함수 이외에도 레벨이 시작될 때, 캐릭터의 데미지를 입었을 때, 레벨이 끝날 때 등의 상황에 
추가적으로 UpdateHUD 함수를 호출해서 데이터의 변동사항을 UI에 갱신되도록 할 수 있다.

 

 

 

🔔 데이터 반영한 UI 업데이트

코인을 획득할 때마다 Score가 갱신되고, Time도 실시간으로 갱신되는 모습을 볼 수 있고,
체력 게이지도 포션을 먹었을 때 정상적으로 회복되는 것을 확인할 수 있다.