안동민 개발노트

본문 시작

데이터 바인딩과 HUD 시스템

체력 같은 게임 상태를 C++ UserWidget과 PlayerController에 연결해 HUD가 값 변화에 반응하게 만듭니다.

이전 절에서 UMG 위젯을 C++에서 생성하고 제어하는 방법을 배웠습니다.

이제 UI 개발의 핵심이자 효율적인 워크플로우를 가능하게 하는 데이터 바인딩(Data Binding) 개념과, 이를 활용하여 게임의 필수적인 정보 표시 장치인 HUD(Head-Up Display) 시스템을 구현하는 방법에 대해 알아보겠습니다.


데이터 바인딩(Data Binding)이란?

데이터 바인딩은 UI 위젯의 특정 속성(예: 텍스트, 이미지, 진행률 바의 퍼센트)을 게임 내 변수(예: 플레이어의 체력, 탄약 수, 점수)에 연결하여, UI가 게임 값을 읽거나 변경 통지를 받아 표시하는 연결입니다. 일반 UMG 속성·함수 바인딩은 값을 주기적으로 평가하므로 변경 통지 방식과 구별합니다.

이는 UI를 수동으로 업데이트하는 코드를 반복적으로 작성할 필요를 없애고, 코드와 UI 디자인을 분리하여 개발 효율성을 크게 높여줍니다.

UMG에서 데이터 바인딩은 주로 두 가지 방식으로 이루어집니다.

속성 바인딩 (Property Binding): 가장 일반적인 방식으로, 위젯의 특정 속성을 게임 변수에 직접 연결합니다.

함수 바인딩 (Function Binding): 위젯의 속성 값을 반환하는 함수를 연결합니다.

이 함수는 위젯이 업데이트될 때마다 호출되어 값을 가져옵니다.

왜 데이터 바인딩이 중요할까요?

  • 자동 업데이트: 게임 변수가 변경되면 UI가 자동으로 반영되므로, 수동으로 UI를 업데이트하는 번거로움과 오류를 줄일 수 있습니다.
  • 코드와 UI 분리: 게임플레이 로직은 게임 변수를 업데이트하는 데 집중하고, UI는 해당 변수를 표시하는 역할만 합니다. 이는 코드의 가독성과 유지보수성을 높입니다.
  • 빠른 이터레이션: 디자이너가 블루프린트에서 데이터 바인딩을 설정하고 값을 조정함으로써, 프로그래머의 개입 없이 UI를 빠르게 반복 수정할 수 있습니다.
  • 갱신 비용 선택: 일반 속성·함수 바인딩은 값이 그대로여도 평가될 수 있습니다. 변경 시 직접 함수를 호출하는 방식은 통지와 초기 동기화를 구현하는 대신 반복 조회를 줄입니다.

HUD (Head-Up Display) 시스템

HUD는 플레이어의 화면에 항상 표시되어 게임플레이에 필요한 핵심 정보를 제공하는 UI 요소들의 집합입니다.

체력 바, 탄약 카운터, 미니맵, 점수, 목표 표시 등이 HUD의 대표적인 예시입니다.

HUD는 보통 AHUD 또는 APlayerController에서 관리하며, UUserWidget 기반 위젯 블루프린트를 화면에 띄우는 방식으로 구현합니다.

이 예제는 로컬 플레이어 입력과 UI 참조를 연결하기 위해 APlayerController에서 관리합니다.

HUD 시스템 구현 단계

HUD UMG 위젯 블루프린트 생성: 게임의 HUD 역할을 할 UMG 위젯 블루프린트를 만듭니다. (예: WBP_PlayerHUD).

필요한 위젯 추가: 이 위젯 블루프린트에 ProgressBar (체력/스테미너), TextBlock (탄약, 점수), Image (크로스헤어) 등을 추가합니다.

데이터 바인딩 설정: 각 위젯의 속성을 게임플레이 데이터에 바인딩합니다.

C++에서 HUD 위젯 생성 및 표시: APlayerController 또는 AMyHUD에서 이 위젯 블루프린트를 로드하고 화면에 추가합니다.

게임플레이 데이터 업데이트: 플레이어 캐릭터나 다른 게임 시스템에서 관련 데이터를 업데이트하고, 이 데이터가 HUD에 반영되도록 합니다.


C++에서 HUD 시스템 구현 예시

이전 절의 AMyPlayerController와 UMyUserWidget (이제 UMyPlayerHUD로 이름 변경)을 기반으로 HUD 시스템을 구현해 보겠습니다.

HUD 위젯 C++ 클래스 (MyPlayerHUD.h / .cpp)

이 위젯은 체력 바와 탄약 텍스트를 가집니다.

MyPlayerHUD.h
#pragma once

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "MyPlayerHUD.generated.h"

// 필요한 UMG 위젯 클래스 포워드 선언
class UProgressBar;
class UTextBlock;

UCLASS()
class MYPROJECT_API UMyPlayerHUD : public UUserWidget
{
    GENERATED_BODY()

public:
    // UMG 디자이너에서 위젯 이름과 정확히 일치하는 이름으로 IsVariable 체크된 위젯을 만드세요.
    UPROPERTY(meta = (BindWidget))
    UProgressBar* HealthProgressBar; // 체력 바

    UPROPERTY(meta = (BindWidget))
    UTextBlock* AmmoCountText; // 탄약 수 텍스트

    // HUD를 업데이트할 C++ 함수 (플레이어 컨트롤러에서 호출)
    UFUNCTION(BlueprintCallable, Category = "HUD")
    void UpdateHealth(float CurrentHealth, float MaxHealth);

    UFUNCTION(BlueprintCallable, Category = "HUD")
    void UpdateAmmo(int32 CurrentAmmo, int32 MaxAmmo);

protected:
    virtual void NativeConstruct() override; // 위젯 생성 시 초기화 로직
};
MyPlayerHUD.cpp
#include "MyPlayerHUD.h"
#include "Components/ProgressBar.h" // UProgressBar 헤더
#include "Components/TextBlock.h"   // UTextBlock 헤더

void UMyPlayerHUD::NativeConstruct()
{
    Super::NativeConstruct();

    // 초기값 설정 (UI가 화면에 나타날 때)
    if (HealthProgressBar)
    {
        HealthProgressBar->SetPercent(1.0f); // 100%로 시작
    }
    if (AmmoCountText)
    {
        AmmoCountText->SetText(FText::FromString(TEXT("Ammo: 0/0"))); // 초기 텍스트
    }
}

void UMyPlayerHUD::UpdateHealth(float CurrentHealth, float MaxHealth)
{
    if (HealthProgressBar)
    {
        // 체력 비율 계산 및 설정 (0.0 ~ 1.0)
        const float Percent = MaxHealth > 0.0f ? FMath::Clamp(CurrentHealth / MaxHealth, 0.0f, 1.0f) : 0.0f;
        HealthProgressBar->SetPercent(Percent);
    }
}

void UMyPlayerHUD::UpdateAmmo(int32 CurrentAmmo, int32 MaxAmmo)
{
    if (AmmoCountText)
    {
        // 탄약 텍스트 업데이트
        FText AmmoDisplay = FText::Format(FText::FromString(TEXT("Ammo: {0}/{1}")),
                                        FText::AsNumber(CurrentAmmo),
                                        FText::AsNumber(MaxAmmo));
        AmmoCountText->SetText(AmmoDisplay);
    }
}

HUD를 관리할 플레이어 컨트롤러 (MyPlayerController.h / .cpp)

플레이어 컨트롤러는 HUD 위젯을 생성하고, 플레이어의 체력/탄약 정보가 변경될 때 HUD 위젯의 업데이트 함수를 호출합니다.

MyPlayerController.h
#pragma once

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

class UMyPlayerHUD; // 우리가 만든 HUD 위젯 C++ 클래스 선언

UCLASS()
class MYPROJECT_API AMyPlayerController : public APlayerController
{
    GENERATED_BODY()

public:
    AMyPlayerController();

protected:
    virtual void BeginPlay() override;

    // 블루프린트에서 할당할 HUD 위젯 블루프린트 클래스
    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI")
    TSubclassOf<UMyPlayerHUD> PlayerHUDClass; // UMyPlayerHUD를 상속받는 블루프린트만 할당 가능

    // 생성된 HUD 위젯 인스턴스
    UPROPERTY()
    UMyPlayerHUD* PlayerHUD; // 캐스팅이 필요 없는 UMyPlayerHUD 타입으로 저장

public:
    // 캐릭터의 체력 업데이트를 HUD에 알리는 함수 (캐릭터에서 호출)
    UFUNCTION(BlueprintCallable, Category = "UI")
    void OnHealthChanged(float CurrentHealth, float MaxHealth);

    // 캐릭터의 탄약 업데이트를 HUD에 알리는 함수 (캐릭터에서 호출)
    UFUNCTION(BlueprintCallable, Category = "UI")
    void OnAmmoChanged(int32 CurrentAmmo, int32 MaxAmmo);
};
MyPlayerController.cpp
#include "MyPlayerController.h"
#include "MyPlayerHUD.h" // 우리가 만든 HUD 위젯 C++ 클래스 헤더 포함

AMyPlayerController::AMyPlayerController()
{
    // ... 기존 생성자 코드 ...
}

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

    // 로컬 컨트롤러에서만 HUD 생성
    if (!IsLocalController()) return;

    // HUD 위젯 생성 및 화면에 추가
    if (PlayerHUDClass)
    {
        PlayerHUD = CreateWidget<UMyPlayerHUD>(this, PlayerHUDClass);
        if (PlayerHUD)
        {
            PlayerHUD->AddToViewport();
            UE_LOG(LogTemp, Warning, TEXT("Player HUD created and added to viewport."));
        }
        else
        {
            UE_LOG(LogTemp, Error, TEXT("Failed to create Player HUD."));
        }
    }
    else
    {
        UE_LOG(LogTemp, Warning, TEXT("PlayerHUDClass is not set in PlayerController."));
    }
}

void AMyPlayerController::OnHealthChanged(float CurrentHealth, float MaxHealth)
{
    if (PlayerHUD)
    {
        PlayerHUD->UpdateHealth(CurrentHealth, MaxHealth);
    }
}

void AMyPlayerController::OnAmmoChanged(int32 CurrentAmmo, int32 MaxAmmo)
{
    if (PlayerHUD)
    {
        PlayerHUD->UpdateAmmo(CurrentAmmo, MaxAmmo);
    }
}

플레이어 캐릭터에서 데이터 변경 및 알림 (MyCharacter.h / .cpp)

플레이어 캐릭터는 자신의 체력이나 탄약이 변경될 때 플레이어 컨트롤러의 함수를 호출하여 HUD에 알립니다.

MyCharacter.h
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "MyCharacter.generated.h"

UCLASS()
class MYPROJECT_API AMyCharacter : public ACharacter
{
    GENERATED_BODY()

public:
    AMyCharacter();

protected:
    virtual void BeginPlay() override;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
    float MaxHealth = 100.0f;

    UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Stats")
    float CurrentHealth; // 현재 체력

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat")
    int32 MaxAmmo = 30;

    UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Combat")
    int32 CurrentAmmo; // 현재 탄약

    // AActor에서 상속받은 데미지 처리 함수를 캐릭터에서 오버라이드
    virtual float TakeDamage(float DamageAmount, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser) override;

    // 탄약을 발사하는 예시 함수
    UFUNCTION(BlueprintCallable, Category = "Combat")
    void Fire();

public:
    // 아래 대안의 블루프린트 바인딩에서도 읽을 수 있는 getter
    UFUNCTION(BlueprintPure, Category = "Stats")
    float GetCurrentHealth() const { return CurrentHealth; }
    UFUNCTION(BlueprintPure, Category = "Stats")
    float GetMaxHealth() const { return MaxHealth; }
    UFUNCTION(BlueprintPure, Category = "Combat")
    int32 GetCurrentAmmo() const { return CurrentAmmo; }
    UFUNCTION(BlueprintPure, Category = "Combat")
    int32 GetMaxAmmo() const { return MaxAmmo; }
};
MyCharacter.cpp
#include "MyCharacter.h"
#include "MyPlayerController.h" // 플레이어 컨트롤러 헤더
#include "GameFramework/PlayerController.h" // APlayerController 기본 헤더

AMyCharacter::AMyCharacter()
{
    PrimaryActorTick.bCanEverTick = true;
    CurrentHealth = MaxHealth; // 초기 체력 설정
    CurrentAmmo = MaxAmmo;     // 초기 탄약 설정
}

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

    // 블루프린트/인스턴스의 최대값이 적용된 뒤 현재값 초기화
    CurrentHealth = MaxHealth;
    CurrentAmmo = MaxAmmo;

    // 게임 시작 시 HUD에 초기 체력/탄약 정보 전달
    AMyPlayerController* PC = Cast<AMyPlayerController>(GetController());
    if (PC)
    {
        PC->OnHealthChanged(CurrentHealth, MaxHealth);
        PC->OnAmmoChanged(CurrentAmmo, MaxAmmo);
    }
}

float AMyCharacter::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
    float ActualDamage = Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);

    CurrentHealth = FMath::Clamp(CurrentHealth - ActualDamage, 0.0f, MaxHealth);
    UE_LOG(LogTemp, Warning, TEXT("Character took %f damage. Current Health: %f"), ActualDamage, CurrentHealth);

    // 체력 변경 시 HUD에 알림
    AMyPlayerController* PC = Cast<AMyPlayerController>(GetController());
    if (PC)
    {
        PC->OnHealthChanged(CurrentHealth, MaxHealth);
    }

    if (CurrentHealth <= 0)
    {
        UE_LOG(LogTemp, Warning, TEXT("Character Died!"));
        // 사망 처리 로직
    }
    return ActualDamage;
}

void AMyCharacter::Fire()
{
    if (CurrentAmmo > 0)
    {
        CurrentAmmo--;
        UE_LOG(LogTemp, Warning, TEXT("Fired! Current Ammo: %d"), CurrentAmmo);

        // 탄약 변경 시 HUD에 알림
        AMyPlayerController* PC = Cast<AMyPlayerController>(GetController());
        if (PC)
        {
            PC->OnAmmoChanged(CurrentAmmo, MaxAmmo);
        }
        // ... 총알 발사 로직 ...
    }
    else
    {
        UE_LOG(LogTemp, Warning, TEXT("No Ammo!"));
    }
}

UMG 디자이너와 갱신 방식 연결

이 예제의 WBP_PlayerHUD는 UMyPlayerHUD를 부모로 지정하고, HealthProgressBar와 AmmoCountText를 같은 이름·타입으로 만듭니다. 두 위젯의 Is Variable을 켜고 모듈에는 UMG 의존성을 설정합니다.

C++ 직접 갱신 경로를 사용할 때는 Percent/Text의 Bind를 비워 둡니다. 인자를 받고 void를 반환하는 UpdateHealth·UpdateAmmo는 속성값을 반환하는 바인딩 함수로 선택할 수 없습니다.

블루프린트 함수 바인딩은 별도의 대안입니다.

  1. 체력 Percent에는 인자 없이 실수 값을 반환하는 함수를 연결합니다. Get Owning Player Pawn을 MyCharacter로 캐스팅하고 유효성을 확인한 뒤, 최대값이 양수일 때만 현재값을 나누어 0~1로 제한합니다.
  2. 탄약 Text에는 인자 없이 FText를 반환하는 함수를 연결합니다. 같은 Pawn의 getter를 읽고 Format Text로 현재/최대 탄약을 반환합니다.
  3. 같은 속성에 직접 Set 함수와 바인딩을 함께 사용하지 않습니다. 어느 경로가 표시값을 책임지는지 하나로 정합니다.
HUD 통지와 준비 시점

현재 코드에서 초기값과 변경값이 전달되는 조건을 비교합니다.

HUD 통지와 준비 시점
시점실제 코드놓칠 수 있는 조건
위젯 구성기본 체력 100%와 Ammo 0/0 표시실제 캐릭터 값의 스냅샷은 아님
캐릭터 BeginPlay컨트롤러가 있으면 초기값 통지HUD가 아직 없으면 통지를 저장하거나 재전송하지 않음
TakeDamage·Fire값을 바꾼 뒤 컨트롤러를 통해 표시 함수 호출HUD가 생기기 전 변경이나 재표시는 별도 초기 동기화가 필요
위젯 구성
실제 코드: 기본 체력 100%와 Ammo 0/0 표시
놓칠 수 있는 조건: 실제 캐릭터 값의 스냅샷은 아님
캐릭터 BeginPlay
실제 코드: 컨트롤러가 있으면 초기값 통지
놓칠 수 있는 조건: HUD가 아직 없으면 통지를 저장하거나 재전송하지 않음
TakeDamage·Fire
실제 코드: 값을 바꾼 뒤 컨트롤러를 통해 표시 함수 호출
놓칠 수 있는 조건: HUD가 생기기 전 변경이나 재표시는 별도 초기 동기화가 필요

컨트롤러와 캐릭터의 BeginPlay 순서를 보장한다고 읽지 않습니다. 완성된 HUD 관리자는 생성·소유 확정 후 현재값을 다시 읽는 경로를 마련해야 합니다.

최대 체력은 양수이고 최대 탄약은 0 이상인 유효한 설정을 전제로 합니다. 이 절은 단일 로컬 플레이어 예제이며 네트워크 복제, 재소유 시 동기화, 실제 발사체·사망 처리는 구현하지 않았습니다.


데이터 바인딩은 UMG 기반 동적 UI 개발의 핵심 개념입니다.

게임플레이 변수가 바뀔 때 UI를 자동 갱신하도록 구성하면, UI 갱신 코드를 매 프레임 수동 처리할 필요가 줄어듭니다.

C++에서 위젯 생성/관리와 데이터 전달 흐름을 체계화하면 HUD 품질과 유지보수성이 함께 올라갑니다.

정적인 UI 설정 데이터에는 Data Asset/Data Table을 사용할 수 있습니다. 현재 체력 같은 런타임 상태와 구별하며, 관련 내용은 7장 3절에서 이어집니다.