본문 바로가기

데이터 바인딩과 HUD 시스템

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

캐릭터가 피해를 받아 체력이 80으로 줄었는데 화면의 체력 바는 여전히 100%를 보여 준다면, 플레이어는 게임을 믿을 수 없게 됩니다. 게임의 값과 화면의 표시를 맞추는 방법은 크게 두 가지입니다. 위젯이 주기적으로 게임 값을 읽어 오게 하거나, 게임 값이 바뀔 때마다 위젯에 알려 주는 것입니다. 그런데 알려 주는 방식에는 또 하나의 문제가 숨어 있습니다. 값이 바뀌었을 때 아직 HUD가 만들어지지 않았다면, 그 알림은 어디로 가야 할까요?

C++에서 위젯 생성 및 제어에서 위젯을 만들고 붙이는 법을 익혔으니, 이 글에서는 데이터 바인딩(data binding)의 개념을 정리하고, 체력과 탄약을 보여 주는 HUD(Head-Up Display)를 C++로 만듭니다. 값이 바뀔 때 직접 알리는 경로와, HUD가 준비되었을 때 현재 값을 한 번 읽어 오는 경로를 함께 연결해, 준비 순서가 어떻게 되든 표시가 맞도록 만드는 방법을 살펴봅니다.


데이터 바인딩의 두 가지 방식

데이터 바인딩은 텍스트, 이미지, 진행률 바의 퍼센트 같은 위젯 속성을 체력, 탄약 수, 점수 같은 게임 변수에 연결해, UI가 게임 값을 읽거나 변경 알림을 받아 표시하게 하는 연결입니다. 위젯이 직접 값을 읽거나 값이 바뀔 때 표시 함수가 불리게 하면, 같은 값을 여러 곳에서 따로 관리하는 일을 줄일 수 있습니다.

UMG에서 바인딩은 주로 두 가지로 설정합니다. 속성 바인딩(property binding)은 위젯의 속성을 게임 변수에 직접 연결하고, 함수 바인딩(function binding)은 속성 값을 돌려주는 함수를 연결합니다. 함수 바인딩의 함수는 위젯이 갱신될 때마다 불려 값을 가져옵니다. 여기서 알아 둘 점은 이 두 가지 일반 바인딩이 값이 바뀔 때만 계산되는 방식이 아니라는 것입니다. 값이 그대로여도 주기적으로 평가될 수 있으므로, 이 글의 C++ 예제는 대신 값이 바뀔 때 직접 알리고 처음 값은 따로 읽어 오는 방식을 씁니다.

바인딩을 쓰는 이유는 여러 가지입니다. 게임플레이 로직은 게임 변수를 바꾸는 데만 집중하고 UI는 그 값을 보여 주기만 하므로, 코드와 UI가 분리되어 읽고 고치기 쉬워집니다. 기획자가 블루프린트에서 바인딩을 설정하고 값을 조정할 수 있으니 프로그래머 없이도 UI를 빠르게 고칠 수 있습니다. 반면 갱신 비용은 고를 수 있어야 합니다. 일반 바인딩은 값이 그대로여도 평가될 수 있고, 값이 바뀔 때 직접 함수를 부르는 방식은 반복 조회를 줄이는 대신 알림과 처음 값 맞추기를 직접 구현해야 합니다.


HUD를 어떻게 구성할까

HUD는 플레이어의 화면에 늘 표시되어 게임플레이에 필요한 핵심 정보를 보여 주는 UI 요소의 묶음입니다. 체력 바, 탄약 수, 미니맵, 점수, 목표 표시가 대표적입니다. HUD는 보통 AHUD나 APlayerController에서 관리하며, UUserWidget 기반의 위젯 블루프린트를 화면에 띄워 구현합니다. 이 글의 예제는 로컬 플레이어의 입력과 UI 참조를 한곳에서 다루기 위해 APlayerController에서 관리합니다.

HUD는 다음 순서로 만듭니다. 먼저 HUD 역할을 할 위젯 블루프린트(예: WBP_PlayerHUD)를 만들고, 체력이나 스태미나를 보여 줄 ProgressBar, 탄약과 점수를 보여 줄 TextBlock, 조준점 Image 같은 위젯을 넣습니다. 그다음 각 위젯의 속성이 게임플레이 데이터와 이어지도록 연결 방식을 정하고, APlayerController나 AHUD를 상속한 클래스에서 이 위젯 블루프린트를 만들어 화면에 붙입니다. 마지막으로 캐릭터나 다른 게임 시스템에서 데이터가 바뀌면 그 값이 HUD에 반영되게 합니다.


C++로 HUD 만들기

앞 글들의 AMyPlayerController와 UMyUserWidget을 바탕으로 HUD를 만들되, 위젯 클래스의 이름은 UMyPlayerHUD로 바꿉니다. 코드는 세 클래스로 나뉩니다. HUD 위젯은 받은 값을 화면에 그리는 함수만 가지고, 플레이어 컨트롤러는 HUD를 만들어 두고 알림을 HUD로 전달하며, 캐릭터는 자기 값이 바뀔 때 컨트롤러에 알립니다. 캐릭터가 위젯을 직접 알지 않게 나눈 덕분에, 나중에 HUD를 바꾸거나 없애도 캐릭터 코드는 고칠 필요가 없습니다.

HUD 위젯 클래스

이 위젯은 체력 바와 탄약 텍스트를 가집니다. 헤더의 두 포인터에 붙인 meta = (BindWidget)은 위젯 블루프린트의 디자이너에 같은 이름과 타입의 위젯이 있어야 한다는 표시이며, 그 위젯이 이 포인터에 연결됩니다. 이름이 하나라도 다르면 위젯 블루프린트를 컴파일할 때 필요한 위젯을 찾지 못했다는 오류가 납니다. 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();

    // 임시 표시 설정 (캐릭터의 현재값은 컨트롤러에서 전달)
    if (HealthProgressBar)
    {
        HealthProgressBar->SetPercent(1.0f); // 현재값을 받기 전 임시 표시
    }
    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);
    }
}

두 위젯은 meta=(BindWidget)으로 위젯 블루프린트의 HealthProgressBar, AmmoCountText에 연결됩니다. NativeConstruct는 체력 100%와 Ammo: 0/0을 넣는데, 이것은 실제 캐릭터 값을 받기 전의 임시 표시일 뿐입니다. UpdateHealth는 앞 글과 같은 방식으로 비율을 0에서 1 사이로 제한해 바에 넣고, UpdateAmmo는 현재 탄약과 최대 탄약을 Ammo: 현재/최대 형식의 텍스트로 만듭니다.

HUD를 관리할 플레이어 컨트롤러

플레이어 컨트롤러는 HUD 위젯을 만들고, 캐릭터의 체력이나 탄약이 바뀌었다는 알림을 받으면 HUD의 갱신 함수를 부릅니다. 여기에 한 가지를 더합니다. HUD가 준비된 시점에 SyncHUDFromPawn이 이 컨트롤러가 조종하는 폰을 GetPawn()으로 가져와 현재 체력과 탄약을 읽게 합니다. 이렇게 특정 시점의 현재 값을 읽어 옮기는 것을 스냅샷(snapshot)이라고 부르겠습니다.

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;
    virtual void OnPossess(APawn* InPawn) override;

    // 현재 소유한 캐릭터의 값을 HUD에 복사
    void SyncHUDFromPawn();

    // 블루프린트에서 할당할 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++ 클래스 헤더 포함
#include "MyCharacter.h"

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

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

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

    // HUD 위젯 생성 및 화면에 추가
    if (PlayerHUDClass)
    {
        PlayerHUD = CreateWidget<UMyPlayerHUD>(this, PlayerHUDClass);
        if (PlayerHUD)
        {
            PlayerHUD->AddToViewport();
            SyncHUDFromPawn();
            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::OnPossess(APawn* InPawn)
{
    Super::OnPossess(InPawn);
    SyncHUDFromPawn();
}

void AMyPlayerController::SyncHUDFromPawn()
{
    if (!IsLocalController() || !IsValid(PlayerHUD)) return;

    const AMyCharacter* Character = Cast<AMyCharacter>(GetPawn());
    if (!IsValid(Character)) return;

    PlayerHUD->UpdateHealth(Character->GetCurrentHealth(), Character->GetMaxHealth());
    PlayerHUD->UpdateAmmo(Character->GetCurrentAmmo(), Character->GetMaxAmmo());
}

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

BeginPlay는 로컬 컨트롤러에서만 HUD를 만들어 화면에 붙인 뒤 바로 SyncHUDFromPawn을 부르고, OnPossess는 Super::OnPossess 다음에 같은 함수를 부릅니다. 이렇게 두 곳에서 부르는 이유는 HUD 부착과 폰 빙의 가운데 어느 쪽이 먼저 일어날지 정해져 있지 않기 때문입니다. HUD가 먼저 준비되면 AddToViewport 뒤의 조회는 폰이 없어 건너뛰지만, 나중에 폰을 빙의하면 OnPossess에서 현재 값을 읽습니다. 반대로 폰을 먼저 빙의했을 때 HUD가 없었다면, HUD를 붙인 뒤의 호출이 값을 읽습니다. 이 함수는 매번 현재 폰을 다시 조회할 뿐, 체력을 회복하거나 탄약을 채우지는 않습니다.

SyncHUDFromPawn 안의 IsValid(Character)는 캐릭터 객체가 유효하다는 뜻일 뿐, 캐릭터의 BeginPlay가 끝났다는 검사가 아닙니다. HUD와 빙의가 먼저 준비되면 캐릭터 생성자에서 정한 값을 읽을 수도 있습니다. 그래서 다음에 볼 캐릭터의 BeginPlay 초기화와 두 번의 초기 알림도 그대로 유지해야 합니다. 블루프린트에서 바꾼 최댓값이 반영된 초기화가 나중에 끝나면, 그 값으로 HUD가 다시 갱신됩니다. 정리하면 HUD 부착, 폰 빙의, 캐릭터 초기화라는 세 경로를 모두 연결해 두고, 서로의 BeginPlay가 정해진 순서로 실행된다고 가정하지 않는 것입니다.

폰이 없거나 AMyCharacter가 아니면 현재 값 복사를 건너뜁니다. 이때는 임시 표시나 이전 표시가 그대로 남을 수 있으며, 빙의가 풀렸을 때 표시를 비우거나 숨기는 처리는 따로 필요합니다.

값을 바꾸고 알리는 캐릭터

플레이어 캐릭터는 자신의 체력이나 탄약이 바뀌면 플레이어 컨트롤러의 함수를 불러 HUD에 알립니다. 헤더에서는 지정자의 차이를 봅니다. 최댓값은 EditAnywhere라서 블루프린트나 배치한 인스턴스에서 바꿀 수 있고, 현재 값은 VisibleAnywhere라서 에디터에서 보기만 합니다. cpp에서 값을 바꾸는 BeginPlay, TakeDamage, Fire는 모두 값을 먼저 바꾼 뒤 GetController()를 AMyPlayerController로 캐스팅해 알리는 같은 순서를 따릅니다.

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!"));
    }
}

캐릭터는 생성자에서 현재 체력과 탄약을 최댓값으로 정하고, BeginPlay에서 한 번 더 정합니다. 블루프린트나 배치한 인스턴스에서 바꾼 최댓값은 생성자가 끝난 뒤에 적용되므로, BeginPlay에서 다시 초기화해야 그 값이 반영됩니다. 그런 다음 자신을 조종하는 컨트롤러가 AMyPlayerController이면 OnHealthChanged와 OnAmmoChanged로 초기 값을 알립니다.

TakeDamage는 AActor에서 물려받은 피해 처리 함수를 재정의한 것으로, 부모 함수가 돌려준 실제 피해량만큼 체력을 줄이고 0과 최댓값 사이로 제한한 뒤 컨트롤러에 알립니다. Fire는 탄약이 있을 때만 하나를 줄이고 알립니다. 아래쪽의 getter 네 개는 BlueprintPure로 공개해, 컨트롤러의 스냅샷과 뒤에 볼 블루프린트 바인딩에서 모두 읽을 수 있게 했습니다.

기본 설정에서 캐릭터 초기화와 HUD 연결이 모두 끝나면 체력은 100%, 탄약은 Ammo: 30/30으로 표시됩니다. 현재 체력이 80/100, 탄약이 12/30인 캐릭터에 HUD를 새로 붙인다면, 현재 값 조회로 체력 80%와 Ammo: 12/30이 표시됩니다. 지나간 알림을 다시 보내는 것이 아니라, 그 순간의 값을 읽어 오는 경로입니다.

탄약이 계속 Ammo: 0/0으로 남아 있다면 컨트롤러 블루프린트의 PlayerHUDClass 지정, 위젯 블루프린트의 부모 클래스와 자식 위젯 이름, 컨트롤러가 지금 조종하는 폰이 AMyCharacter인지, 그리고 AddToViewport와 Super::OnPossess 뒤의 동기화 호출이 있는지를 확인합니다.


세 경로가 HUD 표시로 모이는 과정

HUD 준비 시 현재값을 읽고 변경을 전달합니다

HUD 부착·Pawn 소유·캐릭터 값 변경 때 조건이 맞아야 현재값 조회/직접 전달이 표시된다. 명시한 준비 사건의 가능한 순서 하나를 예로 들어 미준비 통지 생략과 준비 뒤 현재값 동기화를 보이고 네 사건 고정 순서를 보장하지 않는다.

UNREAL C++ · CH6-3

HUD 준비 시 현재값을 읽고 변경을 전달합니다

임시 표시 뒤에 HUD 부착·Pawn 소유의 현재값 조회와 캐릭터 초기화 통지를 연결합니다. 과거 통지를 재전송하거나 BeginPlay의 고정 순서를 가정하지 않습니다.

독립된 준비 시점과 값 변경이 HUD 표시로 이어진다HUD부착과Pawn소유는각각SyncHUDFromPawn으로현재값조회한다. 캐릭터초기화와값변경은직접함수호출로새값을전달한다. 조회에는유효HUD와현재AMyCharacter,전달에는HUD존재조건이필요하다. 네입력사건의고정순서를보장하거나실패한통지를큐에보관하는그림이아니다.HUD 부착AddToViewport 뒤Pawn 소유Super::OnPossess 뒤현재값 조회현재 Pawn의 getterHUD/Pawn 유효할 때캐릭터 초기화최대값 반영 뒤 통지체력 · 탄약 변경새 값으로 통지새값 직접 전달OnHealthChangedOnAmmoChangedHUD 있을 때 표시HUD 표시체력 Percent탄약 Text
독립된 준비 시점과 값 변경이 HUD 표시로 이어진다HUD부착과Pawn소유는각각SyncHUDFromPawn으로현재값조회한다. 캐릭터초기화와값변경은직접함수호출로새값을전달한다. 조회에는유효HUD와현재AMyCharacter,전달에는HUD존재조건이필요하다. 네입력사건의고정순서를보장하거나실패한통지를큐에보관하는그림이아니다.HUD 부착AddToViewport 뒤Pawn 소유Super::OnPossess 뒤현재값 조회현재 Pawn의 getterHUD/Pawn 유효할 때캐릭터 초기화최대값 반영 뒤 통지체력 · 탄약 변경새 값으로 통지새값 직접 전달OnHealthChangedOnAmmoChangedHUD 있을 때 표시HUD 표시체력 Percent탄약 Text

조건이 맞지 않으면 그 표시 호출을 생략합니다. 조회는 그때의 현재 Pawn 값을 읽고, 직접 전달은 그 호출의 새 값을 받습니다. 준비·초기화 순서가 달라도 원 코드의 세 경로를 유지하며, 실제 게임 상태의 변화와 임시 표시를 구별합니다.

HUD 현재값 조회와 변경 통지
시점실제 코드확인할 조건
위젯 구성NativeConstruct: 체력 100%, Ammo: 0/0 표시임시 표시이며 캐릭터의 실제 현재값은 아님
HUD 부착AddToViewport 뒤 SyncHUDFromPawn현재 소유한 AMyCharacter가 유효하면 체력·탄약 getter를 읽음
Pawn 소유Super::OnPossess 뒤 같은 함수 호출HUD가 있으면 현재값을 읽고, 없으면 나중에 HUD 부착 경로에서 읽음
캐릭터 초기화BeginPlay에서 최대값 반영 후 두 초기 통지IsValid는 초기화 완료 검사가 아님. 나중에 초기화되면 이 통지가 표시를 갱신함
이후 값 변경TakeDamage·Fire → OnHealthChanged·OnAmmoChangedHUD가 없으면 표시 함수 호출을 생략하며 통지를 큐에 저장하지 않음
위젯 구성
실제 코드: NativeConstruct: 체력 100%, Ammo: 0/0 표시
확인할 조건: 임시 표시이며 캐릭터의 실제 현재값은 아님
HUD 부착
실제 코드: AddToViewport 뒤 SyncHUDFromPawn
확인할 조건: 현재 소유한 AMyCharacter가 유효하면 체력·탄약 getter를 읽음
Pawn 소유
실제 코드: Super::OnPossess 뒤 같은 함수 호출
확인할 조건: HUD가 있으면 현재값을 읽고, 없으면 나중에 HUD 부착 경로에서 읽음
캐릭터 초기화
실제 코드: BeginPlay에서 최대값 반영 후 두 초기 통지
확인할 조건: IsValid는 초기화 완료 검사가 아님. 나중에 초기화되면 이 통지가 표시를 갱신함
이후 값 변경
실제 코드: TakeDamage·Fire → OnHealthChanged·OnAmmoChanged
확인할 조건: HUD가 없으면 표시 함수 호출을 생략하며 통지를 큐에 저장하지 않음

준비 시 현재값 읽기

SyncHUDFromPawn은 현재 Pawn의 getter를 읽어 표시합니다. HUD 또는 현재 AMyCharacter가 없으면 생략하며, 나중에 다른 준비 경로에서 최신값을 읽습니다. 체력을 회복하거나 탄약을 충전하지 않습니다.

값 변경 시 새값 전달

캐릭터 초기화·TakeDamage·Fire는 컨트롤러가 있으면 값을 전달합니다. PlayerHUD가 있을 때 표시 함수를 호출하며, 없으면 값을 보관하거나 자동 재전송하지 않습니다.

단일 로컬 Standalone 예제이며 세 준비 경로의 순서를 보장하지 않습니다. Pawn이 없거나 다른 타입이면 임시값·이전 표시가 남을 수 있습니다. HUD 재부착 기능을 추가하면 붙인 직후 현재값을 다시 읽어야 합니다. 소유 해제 시 HUD 정리·네트워크 클라이언트 동기화·전체 재소유 처리는 구현하지 않았습니다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
원 도표의 제목·노드·관계·조건·주석
범위원문
관계·조건 설명HUD 부착과 Pawn 소유는 각각 SyncHUDFromPawn으로 현재값을 조회한다. 캐릭터 초기화와 값 변경은 직접 함수 호출로 새 값을 전달한다. 조회에는 유효한 HUD와 현재 AMyCharacter가 필요하며, 전달에는 HUD가 존재해야 한다. 이 그림은 네 입력 사건의 고정 순서를 보장하거나 실패한 통지를 큐에 보관하는 구조를 나타내지 않는다.
노드·연결선 라벨최댓값 반영 뒤 통지
노드·연결선 라벨새 값 직접 전달
원 표의 셀BeginPlay에서 최댓값 반영 후 두 초기 통지
본문·주석값 변경 시 새 값 전달
예의 값과 변화
관찰·상황값·보충 정보변화·이유
미준비 알림

가능한 사건 순서 하나에서는 값 변경이 HUD 준비보다 먼저 온다.

  • HUD: 아직 미준비
  • 알림: 생략 가능
  • 모든 게임 실행의 이벤트 순서를 고정하는 사례가 아니다.
준비 완료

HUD 부착과 현재 Pawn 소유 조건을 확인한다.

  • HUD: 부착됨
  • Pawn: 현재 소유 대상
  • 유효한 현재 대상과 화면이 함께 있어야 전달할 수 있다.
현재 값 동기화

현재 캐릭터 값을 조회·전달해 화면을 맞춘다.

  • 원천: 현재 캐릭터 값
  • 대상: 준비된 HUD
  • 현재 값을 동기화해 과거에 누락한 알림을 보완한다.

HUD 준비 시 현재값을 읽고 변경을 전달합니다

HUD 부착·Pawn 소유·캐릭터 값 변경 때 조건이 맞아야 현재값 조회/직접 전달이 표시된다. 명시한 준비 사건의 가능한 순서 하나를 예로 들어 미준비 통지 생략과 준비 뒤 현재값 동기화를 보이고 네 사건 고정 순서를 보장하지 않는다.

UNREAL C++ · CH6-3

HUD 준비 시 현재값을 읽고 변경을 전달합니다

임시 표시 뒤에 HUD 부착·Pawn 소유의 현재값 조회와 캐릭터 초기화 통지를 연결합니다. 과거 통지를 재전송하거나 BeginPlay의 고정 순서를 가정하지 않습니다.

독립된 준비 시점과 값 변경이 HUD 표시로 이어진다HUD부착과Pawn소유는각각SyncHUDFromPawn으로현재값조회한다. 캐릭터초기화와값변경은직접함수호출로새값을전달한다. 조회에는유효HUD와현재AMyCharacter,전달에는HUD존재조건이필요하다. 네입력사건의고정순서를보장하거나실패한통지를큐에보관하는그림이아니다.HUD 부착AddToViewport 뒤Pawn 소유Super::OnPossess 뒤현재값 조회현재 Pawn의 getterHUD/Pawn 유효할 때캐릭터 초기화최대값 반영 뒤 통지체력 · 탄약 변경새 값으로 통지새값 직접 전달OnHealthChangedOnAmmoChangedHUD 있을 때 표시HUD 표시체력 Percent탄약 Text
독립된 준비 시점과 값 변경이 HUD 표시로 이어진다HUD부착과Pawn소유는각각SyncHUDFromPawn으로현재값조회한다. 캐릭터초기화와값변경은직접함수호출로새값을전달한다. 조회에는유효HUD와현재AMyCharacter,전달에는HUD존재조건이필요하다. 네입력사건의고정순서를보장하거나실패한통지를큐에보관하는그림이아니다.HUD 부착AddToViewport 뒤Pawn 소유Super::OnPossess 뒤현재값 조회현재 Pawn의 getterHUD/Pawn 유효할 때캐릭터 초기화최대값 반영 뒤 통지체력 · 탄약 변경새 값으로 통지새값 직접 전달OnHealthChangedOnAmmoChangedHUD 있을 때 표시HUD 표시체력 Percent탄약 Text

조건이 맞지 않으면 그 표시 호출을 생략합니다. 조회는 그때의 현재 Pawn 값을 읽고, 직접 전달은 그 호출의 새 값을 받습니다. 준비·초기화 순서가 달라도 원 코드의 세 경로를 유지하며, 실제 게임 상태의 변화와 임시 표시를 구별합니다.

HUD 현재값 조회와 변경 통지
시점실제 코드확인할 조건
위젯 구성NativeConstruct: 체력 100%, Ammo: 0/0 표시임시 표시이며 캐릭터의 실제 현재값은 아님
HUD 부착AddToViewport 뒤 SyncHUDFromPawn현재 소유한 AMyCharacter가 유효하면 체력·탄약 getter를 읽음
Pawn 소유Super::OnPossess 뒤 같은 함수 호출HUD가 있으면 현재값을 읽고, 없으면 나중에 HUD 부착 경로에서 읽음
캐릭터 초기화BeginPlay에서 최대값 반영 후 두 초기 통지IsValid는 초기화 완료 검사가 아님. 나중에 초기화되면 이 통지가 표시를 갱신함
이후 값 변경TakeDamage·Fire → OnHealthChanged·OnAmmoChangedHUD가 없으면 표시 함수 호출을 생략하며 통지를 큐에 저장하지 않음
위젯 구성
실제 코드: NativeConstruct: 체력 100%, Ammo: 0/0 표시
확인할 조건: 임시 표시이며 캐릭터의 실제 현재값은 아님
HUD 부착
실제 코드: AddToViewport 뒤 SyncHUDFromPawn
확인할 조건: 현재 소유한 AMyCharacter가 유효하면 체력·탄약 getter를 읽음
Pawn 소유
실제 코드: Super::OnPossess 뒤 같은 함수 호출
확인할 조건: HUD가 있으면 현재값을 읽고, 없으면 나중에 HUD 부착 경로에서 읽음
캐릭터 초기화
실제 코드: BeginPlay에서 최대값 반영 후 두 초기 통지
확인할 조건: IsValid는 초기화 완료 검사가 아님. 나중에 초기화되면 이 통지가 표시를 갱신함
이후 값 변경
실제 코드: TakeDamage·Fire → OnHealthChanged·OnAmmoChanged
확인할 조건: HUD가 없으면 표시 함수 호출을 생략하며 통지를 큐에 저장하지 않음

준비 시 현재값 읽기

SyncHUDFromPawn은 현재 Pawn의 getter를 읽어 표시합니다. HUD 또는 현재 AMyCharacter가 없으면 생략하며, 나중에 다른 준비 경로에서 최신값을 읽습니다. 체력을 회복하거나 탄약을 충전하지 않습니다.

값 변경 시 새값 전달

캐릭터 초기화·TakeDamage·Fire는 컨트롤러가 있으면 값을 전달합니다. PlayerHUD가 있을 때 표시 함수를 호출하며, 없으면 값을 보관하거나 자동 재전송하지 않습니다.

단일 로컬 Standalone 예제이며 세 준비 경로의 순서를 보장하지 않습니다. Pawn이 없거나 다른 타입이면 임시값·이전 표시가 남을 수 있습니다. HUD 재부착 기능을 추가하면 붙인 직후 현재값을 다시 읽어야 합니다. 소유 해제 시 HUD 정리·네트워크 클라이언트 동기화·전체 재소유 처리는 구현하지 않았습니다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.

관계도의 보충 설명

원 도표의 제목·노드·관계·조건·주석
범위원문
관계·조건 설명HUD 부착과 Pawn 소유는 각각 SyncHUDFromPawn으로 현재값을 조회한다. 캐릭터 초기화와 값 변경은 직접 함수 호출로 새 값을 전달한다. 조회에는 유효한 HUD와 현재 AMyCharacter가 필요하며, 전달에는 HUD가 존재해야 한다. 이 그림은 네 입력 사건의 고정 순서를 보장하거나 실패한 통지를 큐에 보관하는 구조를 나타내지 않는다.
노드·연결선 라벨최댓값 반영 뒤 통지
노드·연결선 라벨새 값 직접 전달
원 표의 셀BeginPlay에서 최댓값 반영 후 두 초기 통지
본문·주석값 변경 시 새 값 전달
예의 값과 변화
관찰·상황값·보충 정보변화·이유
미준비 알림

가능한 사건 순서 하나에서는 값 변경이 HUD 준비보다 먼저 온다.

  • HUD: 아직 미준비
  • 알림: 생략 가능
  • 모든 게임 실행의 이벤트 순서를 고정하는 사례가 아니다.
준비 완료

HUD 부착과 현재 Pawn 소유 조건을 확인한다.

  • HUD: 부착됨
  • Pawn: 현재 소유 대상
  • 유효한 현재 대상과 화면이 함께 있어야 전달할 수 있다.
현재 값 동기화

현재 캐릭터 값을 조회·전달해 화면을 맞춘다.

  • 원천: 현재 캐릭터 값
  • 대상: 준비된 HUD
  • 현재 값을 동기화해 과거에 누락한 알림을 보완한다.

위 그림의 위쪽은 서로 독립된 준비 시점과 값 변경이 HUD 표시로 이어지는 두 갈래를 보여 줍니다. 한 갈래는 HUD 부착(AddToViewport 뒤)과 폰 빙의(Super::OnPossess 뒤)가 각각 SyncHUDFromPawn을 불러, 현재 폰의 getter로 현재 값을 조회하는 경로입니다. 이 조회는 HUD와 폰이 모두 유효할 때만 일어납니다. 다른 갈래는 캐릭터 초기화(최댓값 반영 뒤의 알림)와 체력, 탄약 변경이 OnHealthChanged, OnAmmoChanged로 새 값을 직접 전달하는 경로로, HUD가 있을 때만 표시됩니다. 두 갈래가 모두 체력 퍼센트와 탄약 텍스트로 모입니다.

그림 아래의 설명처럼 조건이 맞지 않으면 그 표시 호출을 건너뜁니다. 조회는 그때의 현재 폰 값을 읽고, 직접 전달은 그 호출이 가진 새 값을 받습니다. 준비와 초기화의 순서가 달라져도 세 경로를 모두 유지하며, 실제 게임 상태의 변화와 임시 표시를 구별해야 합니다.

그림의 표는 시점별로 실제 코드와 확인할 조건을 정리합니다. 위젯이 구성될 때 NativeConstruct는 체력 100%와 Ammo: 0/0을 표시하지만 이것은 임시 표시입니다. AddToViewport 뒤의 SyncHUDFromPawn은 지금 조종하는 AMyCharacter가 유효하면 체력과 탄약 getter를 읽고, Super::OnPossess 뒤의 같은 호출은 HUD가 있으면 현재 값을 읽으며 없으면 나중에 HUD 부착 경로에서 읽습니다. 캐릭터의 BeginPlay는 최댓값을 반영한 뒤 두 번 알리는데, IsValid는 초기화 완료를 검사하지 않으므로 나중에 초기화가 끝나면 이 알림이 표시를 갱신합니다. 이후의 값 변경은 TakeDamage와 Fire가 OnHealthChanged, OnAmmoChanged를 부르는 경로로, HUD가 없으면 표시 호출을 건너뛸 뿐 알림을 큐에 저장하지 않습니다.

그림 아래쪽의 두 칸은 이를 다시 요약합니다. 준비될 때 현재 값을 읽는 SyncHUDFromPawn은 HUD나 현재 AMyCharacter가 없으면 건너뛰고, 나중에 다른 준비 경로에서 최신 값을 읽습니다. 값이 바뀔 때 새 값을 전달하는 경로는 컨트롤러가 있으면 값을 넘기고, PlayerHUD가 있을 때만 표시 함수를 부르며, 없으면 값을 보관하거나 자동으로 다시 보내지 않습니다. 단계 그림은 가능한 사건 순서 하나를 예로 들어, 값 변경이 HUD 준비보다 먼저 와서 알림이 표시되지 못하고, 나중에 HUD 부착과 폰 빙의가 확인되면, 현재 값을 읽어 화면을 맞추는 과정을 보여 줍니다. 이것이 모든 실행에서 사건 순서가 정해져 있다는 뜻은 아닙니다.


디자이너에서 갱신 방식 고르기

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

C++에서 직접 갱신하는 경로를 쓸 때는 디자이너에서 Percent와 Text의 Bind를 비워 둡니다. 인자를 받고 void를 돌려주는 UpdateHealth와 UpdateAmmo는 속성 값을 돌려주는 바인딩 함수로 고를 수도 없습니다.

블루프린트의 함수 바인딩은 이와 별개의 대안입니다. 체력 Percent에는 인자 없이 실수를 돌려주는 함수를 연결하는데, 그 안에서 Get Owning Player Pawn을 MyCharacter로 캐스팅해 유효한지 확인하고, 최댓값이 양수일 때만 현재 값을 나눠 0에서 1 사이로 제한해 돌려줍니다. 탄약 Text에는 인자 없이 FText를 돌려주는 함수를 연결하고, 같은 폰의 getter를 읽어 Format Text로 현재와 최대 탄약을 만들어 돌려줍니다. 다만 바인딩을 연결한 속성에 C++에서 Set 함수를 직접 부르면 그 바인딩이 끊어지므로, 표시 값을 정하는 경로를 하나로 정합니다.

더 깊이: 바뀌지 않은 위젯은 다시 그리지 않는 무효화

값을 언제 읽을지와 별개로, 화면을 다시 그리는 데에도 CPU 비용이 듭니다. 슬레이트와 UMG의 무효화(invalidation)는 바뀐 것으로 표시된 위젯과 그 자식만 다시 그려 이 비용을 줄이는 기능입니다. 위젯을 무효화 상자(Invalidation Box)로 감싸면 상자는 자식 위젯의 지오메트리를 캐시하고, 자식이 바뀌지 않는 동안 그 캐시를 다시 씁니다. 창 전체에 같은 방식을 적용하는 전역 무효화(Global Invalidation)도 있는데, 콘솔 변수 Slate.EnableGlobalInvalidation을 켜면 창이 UI 전체를 하나로 캐시하고 그 안의 무효화 상자들은 꺼집니다.

캐시는 위젯이 자주 바뀌지 않을 때만 이득입니다. 프레임마다 바뀌는 위젯은 바뀔 때마다 무효화되므로 캐시하지 않을 때와 비슷한 CPU를 쓰면서 메모리만 더 차지합니다. 이런 위젯은 디테일 패널의 Performance 항목에서 Is Volatile을 켜 휘발성(Volatile) 위젯으로 표시하는데, 그러면 무효화 시스템이 그 위젯과 자식의 그리기 데이터를 캐시하지 않고 매 프레임 다시 그립니다. 자식 위젯을 텍스처 하나로 합쳐 그리는 리테이너 패널(Retainer Panel)도 있지만 메모리와 다시 그리는 비용이 더 커서, 공식 문서는 무효화 상자를 먼저 써 보라고 권합니다.


HUD를 다시 붙이거나 바꿀 때

UPROPERTY로 보관한 PlayerHUD는 위젯 인스턴스를 계속 유지합니다. RemoveFromParent로 화면에서 떼어 내는 것과 참조를 비워 인스턴스를 더 이상 유지하지 않는 것이 다르다는 점은 C++에서 위젯 생성 및 제어의 위젯 수명 설명에서 보았습니다.

이 예제에는 HUD를 숨기거나 다시 만드는 함수를 구현하지 않았습니다. 그런 함수를 컨트롤러에 더한다면, 새 위젯을 만들어 붙이거나 같은 인스턴스를 다시 AddToViewport한 직후에도 SyncHUDFromPawn()을 불러야 합니다. 화면을 다시 구성할 때 NativeConstruct가 임시 표시를 다시 넣을 수 있기 때문입니다.

현재 값을 다시 표시하는 것은 이벤트 구독을 더하는 일과 다릅니다. 이 예제는 체력과 탄약의 변경을 직접 함수 호출로 전달하며 새 델리게이트를 연결하지 않습니다. 버튼 이벤트를 여러 번 연결하지 않는 방법은 다음 글 UI 입력 처리 및 애니메이션의 NativeOnInitialized 설명에서 다룹니다.

이 예제는 최대 체력이 양수이고 최대 탄약이 0 이상인 유효한 설정을 전제로 하며, 단일 로컬 독립 실행(Standalone)의 HUD 연결만 다룹니다. OnPossess에서 현재 값을 읽는 처리는 넣었지만, 빙의가 풀릴 때의 HUD 정리와 사망, 리스폰을 포함한 전체 재빙의 처리는 구현하지 않았습니다. OnPossess는 빙의 권한을 가진 쪽에서 불리므로, 네트워크 클라이언트의 폰 동기화까지 해결하는 예제로 읽으면 안 됩니다. 네트워크 복제와 실제 발사체, 사망 처리도 따로 구현해야 합니다.

체력처럼 실행 중에 바뀌는 상태와 달리, UI 설정처럼 바뀌지 않는 데이터에는 Data Asset이나 Data Table을 쓸 수 있습니다. 이 내용은 파일 입출력 및 JSON 처리에서 이어집니다.


정리

  • 데이터 바인딩은 위젯 속성을 게임 값에 연결하는 방법이며, UMG의 일반 속성, 함수 바인딩은 값이 그대로여도 평가될 수 있습니다.
  • 값이 바뀔 때 직접 알리는 방식은 반복 조회를 줄이지만, 알림을 받을 HUD가 아직 없을 때를 대비해 처음 값을 맞추는 경로가 필요합니다.
  • 이 예제는 HUD 부착과 폰 빙의 때 SyncHUDFromPawn으로 현재 값을 읽고, 캐릭터 초기화와 값 변경 때 OnHealthChanged, OnAmmoChanged로 새 값을 전달합니다.
  • 세 경로는 실행 순서가 정해져 있지 않으므로 모두 유지하며, HUD가 없을 때의 알림은 큐에 저장되지 않고 나중의 현재 값 조회가 표시를 맞춥니다.
  • 블루프린트에서 바꾼 최댓값은 생성자 뒤에 적용되므로 캐릭터의 BeginPlay에서 현재 값을 다시 초기화합니다.
  • C++ 직접 갱신과 블루프린트 바인딩 가운데 하나만 골라 같은 속성을 두 경로가 덮어쓰지 않게 합니다.

연습 문제

1. HUD 없이 지나간 변화

캐릭터의 BeginPlay 초기화가 끝났고, 현재 체력은 80/100, 탄약은 12/30입니다. HUD가 아직 없는 동안 실제 피해량이 20인 피해를 한 번 받고 Fire를 세 번 불렀습니다. 나중에 같은 캐릭터를 조종하는 컨트롤러가 HUD를 만들어 붙이고 SyncHUDFromPawn을 부르면 무엇이 표시될까요? 이 결과를 얻으려면 지나간 네 번의 알림을 큐에 보관해야 할까요?

해설 보기

게임 상태는 체력 60/100, 탄약 9/30이 되었으므로, 현재 값 조회는 체력 60%와 Ammo: 9/30을 표시합니다. 각 변경 때 HUD가 없어 표시 호출은 건너뛰었지만, 게임 상태 자체가 사라진 것은 아닙니다.

이 예제는 지나간 알림을 큐로 다시 재생하지 않고, getter에서 최신 값을 읽어 옵니다. 그래서 알림을 보관할 필요가 없습니다. 다만 컨트롤러가 다른 폰을 조종하거나 유효한 AMyCharacter가 없다면 조회 대상이 달라지므로 같은 표시를 기대할 수 없습니다.

2. 바인딩과 직접 갱신을 함께 쓰면

HealthProgressBar의 Percent에 블루프린트 함수 바인딩을 연결해 두고, C++의 UpdateHealth도 그대로 불렀습니다. 어떤 문제가 생길 수 있을까요?

해설 보기

두 경로가 같은 속성의 값을 각자 정하게 됩니다. 바인딩을 연결한 위젯 속성에 Set 함수를 직접 부르면 그 바인딩이 끊어지므로, C++ 코드가 SetPercent를 처음 부른 뒤부터는 바인딩 함수가 더 이상 표시를 정하지 않습니다. 디자이너에는 Bind가 그대로 남아 있어서, 바인딩 함수를 고쳐도 화면이 따라오지 않는 이유를 찾기 어려워집니다.

C++ 직접 갱신을 쓴다면 디자이너의 Bind를 비우고, 바인딩을 쓴다면 C++의 Set 호출을 빼서 한 경로만 남깁니다.

3. 블루프린트에서 바꾼 최대 체력

캐릭터 블루프린트의 기본값에서 MaxHealth를 150으로 바꿨습니다. 그런데 캐릭터 생성자에서만 CurrentHealth = MaxHealth를 하고, BeginPlay의 초기화를 지웠다고 합시다. 게임을 시작했을 때 현재 체력과 HUD는 어떻게 될까요?

해설 보기

생성자가 실행될 때는 C++의 기본값 100이 쓰이므로 CurrentHealth는 100이 되고, 블루프린트의 150은 그 뒤에 MaxHealth에만 적용됩니다. 그래서 현재 체력 100, 최대 체력 150인 상태로 시작하고, HUD는 약 66.7%를 표시합니다.

블루프린트에서 바꾼 값은 생성자 뒤에 적용되므로, 그 값을 바탕으로 한 초기화는 BeginPlay에서 다시 해야 합니다. 이 글의 코드가 BeginPlay에서 한 번 더 초기화하고 알리는 이유가 이것입니다.

위젯 속성 바인딩을 설정하는 방법은 Epic의 Property Binding 문서에서, 바인딩이나 Tick 대신 이벤트로 UI를 갱신하라는 권장은 UMG Best Practices 문서에서 확인할 수 있습니다.

다음 글 UI 입력 처리 및 애니메이션에서는 버튼 클릭 같은 UI 입력을 C++에서 받고, 위젯 애니메이션을 재생하는 방법을 살펴봅니다.