데이터 바인딩과 HUD 시스템
체력 같은 게임 상태를 C++ UserWidget과 PlayerController에 연결해 HUD가 값 변화에 반응하게 만듭니다.
이전 절에서 UMG 위젯을 C++에서 생성하고 제어하는 방법을 배웠습니다.
이제 UI 개발의 핵심이자 효율적인 워크플로우를 가능하게 하는 데이터 바인딩(Data Binding) 개념과, 이를 활용하여 게임의 필수적인 정보 표시 장치인 HUD(Head-Up Display) 시스템을 구현하는 방법에 대해 알아보겠습니다.
데이터 바인딩(Data Binding)이란?
데이터 바인딩은 UI 위젯의 특정 속성(예: 텍스트, 이미지, 진행률 바의 퍼센트)을 게임 내 변수(예: 플레이어의 체력, 탄약 수, 점수)에 연결하여, UI가 게임 값을 읽거나 변경 통지를 받아 표시하는 연결입니다. 일반 UMG 속성·함수 바인딩은 값을 주기적으로 평가하므로 변경 통지 방식과 구별합니다.
위젯이 값을 읽게 하거나 변경 시 표시 함수를 호출하면, 같은 값을 여러 곳에서 따로 관리하는 일을 줄일 수 있습니다. 아래 C++ 예제는 변경 통지와 초깃값 조회를 직접 연결하는 방식을 사용합니다.
UMG에서 데이터 바인딩은 주로 두 가지 방식으로 이루어집니다.
속성 바인딩 (Property Binding): 가장 일반적인 방식으로, 위젯의 특정 속성을 게임 변수에 직접 연결합니다.
함수 바인딩 (Function Binding): 위젯의 속성 값을 반환하는 함수를 연결합니다.
이 함수는 위젯이 업데이트될 때마다 호출되어 값을 가져옵니다.
왜 데이터 바인딩이 중요할까요?
- 표시 갱신: 바인딩은 게임 값을 읽어 표시합니다. 직접 갱신 방식에서는 값이 바뀔 때 알려 주는 코드와, HUD가 준비됐을 때 현재값을 읽는 코드가 필요합니다.
- 코드와 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)
이 위젯은 체력 바와 탄약 텍스트를 가집니다.
#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; // 화면 구성 시 임시 표시 설정
};#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);
}
}HUD를 관리할 플레이어 컨트롤러 (MyPlayerController.h / .cpp)
플레이어 컨트롤러는 HUD 위젯을 생성하고, 플레이어의 체력/탄약 정보가 변경될 때 HUD 위젯의 업데이트 함수를 호출합니다.
NativeConstruct의 체력 100%와 Ammo: 0/0은 실제 캐릭터 값이 아닌 임시 표시입니다. HUD가 준비됐을 때 SyncHUDFromPawn에서 이 컨트롤러가 소유한 Pawn을 GetPawn()으로 가져와 현재 체력과 탄약을 읽겠습니다. 이렇게 특정 시점의 현재값을 읽어 옮기는 것을 스냅샷이라고 합니다.
#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);
};#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);
}
}HUD가 먼저 준비되면 AddToViewport 뒤의 조회는 Pawn이 없어 생략될 수 있습니다. 나중에 Pawn을 소유하면 Super::OnPossess 뒤 같은 함수가 현재값을 읽습니다. 반대로 Pawn을 먼저 소유했을 때 HUD가 없다면, HUD를 붙인 뒤의 호출이 값을 읽습니다. 이 함수는 매번 현재 Pawn을 조회하며 체력 회복이나 탄약 충전을 하지 않습니다.
여기서 IsValid(Character)는 캐릭터 객체가 유효하다는 뜻이며, 캐릭터의 BeginPlay가 끝났다는 검사는 아닙니다. HUD와 Pawn 소유가 먼저 준비되면 생성자 당시의 현재값을 읽을 수도 있습니다. 따라서 다음 코드의 캐릭터 BeginPlay 초기화와 두 초기 통지를 유지해야 합니다. 최댓값 설정을 반영한 초기화가 나중에 끝나면 그 값으로 HUD를 다시 갱신합니다. HUD 부착·Pawn 소유·캐릭터 초기화의 세 경로를 연결하며, 서로의 BeginPlay가 고정된 순서로 실행된다고 가정하지 않습니다.
Pawn이 없거나 AMyCharacter가 아니면 현재값 복사를 생략합니다. 이때 임시값이나 이전 표시가 남을 수 있으며, 소유 해제 시 표시를 비우거나 숨기는 처리는 별도로 필요합니다.
플레이어 캐릭터에서 데이터 변경 및 알림 (MyCharacter.h / .cpp)
플레이어 캐릭터는 자신의 체력이나 탄약이 변경될 때 플레이어 컨트롤러의 함수를 호출하여 HUD에 알립니다.
#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; }
};#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!"));
}
}기본 설정의 캐릭터 초기화와 HUD 연결이 끝나면 체력은 100%, 탄약은 Ammo: 30/30으로 표시됩니다. 예시: 현재 체력이 80/100, 탄약이 12/30인 캐릭터의 HUD를 준비한다면 현재값 조회는 체력 80%와 Ammo: 12/30을 표시합니다. 과거 통지를 재전송하는 것이 아니라 그 순간의 값을 읽는 경로입니다.
현재값 조회로 놓친 표시를 복구하기
캐릭터 BeginPlay 초기화가 끝났고 현재 체력은 80/100, 탄약은 12/30이라고 가정합니다. HUD가 아직 없는 동안 ActualDamage가 20인 데미지를 한 번 받고 Fire를 세 번 호출했습니다. 나중에 같은 캐릭터를 소유한 컨트롤러가 HUD를 생성·부착하고 SyncHUDFromPawn을 호출하면 무엇이 표시될까요? 그 결과를 얻으려면 과거 네 번의 변경 통지를 큐에 보관해야 할까요?
현재 상태와 통지 이력을 나누어 풀이하기
게임 상태는 체력 60/100, 탄약 9/30이 되었으므로, 현재값 조회는 체력 60%와 Ammo: 9/30을 표시합니다. 각 변경 시 HUD가 없으면 표시 호출을 생략했지만 게임 상태가 사라진 것은 아닙니다. 이 예제는 과거 통지를 큐로 재생하지 않고 getter에서 최신값을 읽습니다. 다른 Pawn을 소유하거나 유효한 AMyCharacter가 없다면 이 계산의 대상 조건이 달라져 같은 표시를 보장할 수 없습니다.
탄약이 계속 0/0이면 PlayerHUDClass 할당, 아래 위젯의 부모·자식 이름, 컨트롤러가 현재 소유한 Pawn의 AMyCharacter 타입, AddToViewport와 Super::OnPossess 뒤의 동기화 호출을 확인하세요.
UMG 디자이너와 갱신 방식 연결
이 예제의 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를 반환하는 함수를 연결합니다. 같은 Pawn의 getter를 읽고Format Text로 현재/최대 탄약을 반환합니다. - 같은 속성에 직접 Set 함수와 바인딩을 함께 사용하지 않습니다. 어느 경로가 표시값을 책임지는지 하나로 정합니다.
HUD 준비 시 현재값을 읽고 변경을 전달합니다
HUD 부착·Pawn 소유·캐릭터 값 변경 때 조건이 맞아야 현재값 조회/직접 전달이 표시된다. 명시한 준비 사건의 가능한 순서 하나를 예로 들어 미준비 통지 생략과 준비 뒤 현재값 동기화를 보이고 네 사건 고정 순서를 보장하지 않는다.
UNREAL C++ · CH6-3
HUD 준비 시 현재값을 읽고 변경을 전달합니다
임시 표시 뒤에 HUD 부착·Pawn 소유의 현재값 조회와 캐릭터 초기화 통지를 연결합니다. 과거 통지를 재전송하거나 BeginPlay의 고정 순서를 가정하지 않습니다.
조건이 맞지 않으면 그 표시 호출을 생략합니다. 조회는 그때의 현재 Pawn 값을 읽고, 직접 전달은 그 호출의 새 값을 받습니다. 준비·초기화 순서가 달라도 원 코드의 세 경로를 유지하며, 실제 게임 상태의 변화와 임시 표시를 구별합니다.
| 시점 | 실제 코드 | 확인할 조건 |
|---|---|---|
| 위젯 구성 | NativeConstruct: 체력 100%, Ammo: 0/0 표시 | 임시 표시이며 캐릭터의 실제 현재값은 아님 |
| HUD 부착 | AddToViewport 뒤 SyncHUDFromPawn | 현재 소유한 AMyCharacter가 유효하면 체력·탄약 getter를 읽음 |
| Pawn 소유 | Super::OnPossess 뒤 같은 함수 호출 | HUD가 있으면 현재값을 읽고, 없으면 나중에 HUD 부착 경로에서 읽음 |
| 캐릭터 초기화 | BeginPlay에서 최대값 반영 후 두 초기 통지 | IsValid는 초기화 완료 검사가 아님. 나중에 초기화되면 이 통지가 표시를 갱신함 |
| 이후 값 변경 | TakeDamage·Fire → OnHealthChanged·OnAmmoChanged | HUD가 없으면 표시 함수 호출을 생략하며 통지를 큐에 저장하지 않음 |
- 위젯 구성
- 실제 코드: 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 부착과 현재 Pawn 소유 조건을 확인한다. |
|
|
| 현재 값 동기화 현재 캐릭터 값을 조회·전달해 화면을 맞춘다. |
|
|
HUD 준비 시 현재값을 읽고 변경을 전달합니다
HUD 부착·Pawn 소유·캐릭터 값 변경 때 조건이 맞아야 현재값 조회/직접 전달이 표시된다. 명시한 준비 사건의 가능한 순서 하나를 예로 들어 미준비 통지 생략과 준비 뒤 현재값 동기화를 보이고 네 사건 고정 순서를 보장하지 않는다.
UNREAL C++ · CH6-3
HUD 준비 시 현재값을 읽고 변경을 전달합니다
임시 표시 뒤에 HUD 부착·Pawn 소유의 현재값 조회와 캐릭터 초기화 통지를 연결합니다. 과거 통지를 재전송하거나 BeginPlay의 고정 순서를 가정하지 않습니다.
조건이 맞지 않으면 그 표시 호출을 생략합니다. 조회는 그때의 현재 Pawn 값을 읽고, 직접 전달은 그 호출의 새 값을 받습니다. 준비·초기화 순서가 달라도 원 코드의 세 경로를 유지하며, 실제 게임 상태의 변화와 임시 표시를 구별합니다.
| 시점 | 실제 코드 | 확인할 조건 |
|---|---|---|
| 위젯 구성 | NativeConstruct: 체력 100%, Ammo: 0/0 표시 | 임시 표시이며 캐릭터의 실제 현재값은 아님 |
| HUD 부착 | AddToViewport 뒤 SyncHUDFromPawn | 현재 소유한 AMyCharacter가 유효하면 체력·탄약 getter를 읽음 |
| Pawn 소유 | Super::OnPossess 뒤 같은 함수 호출 | HUD가 있으면 현재값을 읽고, 없으면 나중에 HUD 부착 경로에서 읽음 |
| 캐릭터 초기화 | BeginPlay에서 최대값 반영 후 두 초기 통지 | IsValid는 초기화 완료 검사가 아님. 나중에 초기화되면 이 통지가 표시를 갱신함 |
| 이후 값 변경 | TakeDamage·Fire → OnHealthChanged·OnAmmoChanged | HUD가 없으면 표시 함수 호출을 생략하며 통지를 큐에 저장하지 않음 |
- 위젯 구성
- 실제 코드: 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 부착과 현재 Pawn 소유 조건을 확인한다. |
|
|
| 현재 값 동기화 현재 캐릭터 값을 조회·전달해 화면을 맞춘다. |
|
|
UPROPERTY로 저장한 PlayerHUD는 위젯 인스턴스를 유지합니다. RemoveFromParent로 화면에서 분리하는 것과 참조를 비워 인스턴스를 더 이상 유지하지 않는 것은 다릅니다. 이 차이는 이전 절의 위젯 수명 설명에서 확인할 수 있습니다.
이 예제에는 HUD 숨기기·재생성 함수를 구현하지 않았습니다. 그런 함수를 컨트롤러에 추가한다면, 새 위젯을 생성해 붙이거나 같은 인스턴스를 다시 AddToViewport한 직후에도 SyncHUDFromPawn()을 호출하세요. 다시 화면을 구성할 때 NativeConstruct가 임시 표시를 대입할 수 있으므로 현재값을 다시 옮겨야 합니다.
현재값을 다시 표시하는 것은 이벤트 구독을 추가하는 일과 다릅니다. 이 예제는 체력·탄약의 변경을 직접 함수 호출로 전달하며 새 델리게이트를 연결하지 않습니다. 버튼 이벤트를 반복 연결하지 않는 방법은 다음 절의 NativeOnInitialized 설명에서 다룹니다.
최대 체력은 양수이고 최대 탄약은 0 이상인 유효한 설정을 전제로 합니다. 이 절은 단일 로컬 Standalone(독립 실행)의 HUD 연결 예제입니다. OnPossess에서 현재값을 읽는 처리를 추가했지만, 소유 해제 시 HUD 정리와 사망·리스폰을 포함한 전체 재소유 처리는 구현하지 않았습니다. OnPossess는 소유 권한을 가진 쪽에서 호출되므로 네트워크 클라이언트의 Pawn 동기화까지 해결하는 예제로 읽지 않습니다. 네트워크 복제와 실제 발사체·사망 처리도 별도 구현이 필요합니다.
데이터 바인딩은 UMG 기반 동적 UI 개발의 핵심 개념입니다.
게임플레이 변수가 바뀔 때 UI를 자동 갱신하도록 구성하면, UI 갱신 코드를 매 프레임 수동 처리할 필요가 줄어듭니다.
C++에서 위젯 생성/관리와 데이터 전달 흐름을 체계화하면 HUD 품질과 유지보수성이 함께 올라갑니다.
정적인 UI 설정 데이터에는 Data Asset/Data Table을 사용할 수 있습니다. 현재 체력 같은 런타임 상태와 구별하며, 관련 내용은 7장 3절에서 이어집니다.