몬스터 피격 시 머리 위에 체력 바가 나타나는 간단한 기능을 구현하려다 C++과 블루프린트 통신의 온갖 함정에 빠지며 좌절했다. 하지만 이 과정을 통해 이름으로 함수를 찾는 불안정한 방식의 한계와, C++ 부모 클래스를 만들어 상속하는 방식이 왜 가장 확실하고 안정적인 '계약' 관계인지 온몸으로 깨닫게 되었다. 이 포스트는 나의 처절한 실패와 최종적인 성공 기록이다. 😅
C++과 블루프린트가 소통하는 방법은 여러 가지가 있지만, 크게 두 가지 접근법으로 나눌 수 있다.
'추측'에 기반한 통신 (내가 실패했던 방식들 💩)
FindFunctionByName 이나 FindField 같은 함수를 사용해 블루프린트 내부에 특정 이름의 함수나 변수가 있을 것이라고 '추측'하고 접근하는 방식이다.'계약'에 기반한 통신 (최종 성공 방식 👍)
UFUNCTION(BlueprintImplementableEvent) 같은 매크로로 함수를 선언하는 것은, "나를 상속받는 자식은 반드시 이 함수를 가지고 있다"는 '계약'을 맺는 것과 같다.Cast를 통해 위젯이 우리와 '계약'된 놈인지 확인만 하면, 컴파일러가 함수의 존재를 보장해주므로 안전하게 직접 호출할 수 있다. 절대 실패하지 않는다.#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "MonsterHealthBar.generated.h"
UCLASS()
class SPARTA_TPROJECT_02_API UMonsterHealthBar : public UUserWidget
{
GENERATED_BODY()
public:
// C++에서는 호출만, 실제 구현은 블루프린트에서 할 함수
UFUNCTION(BlueprintImplementableEvent, Category = "Health")
void UpdateHealthBar(float HealthRatio);
};```
#### AIMonsterBase.h
몬스터 헤더 파일. `UWidgetComponent`와 타이머 관련 멤버들을 선언했다.
```cpp
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "Engine/TimerHandle.h" // FTimerHandle을 위해 추가
#include "AIMonsterBase.generated.h"
UCLASS()
class SPARTA_TPROJECT_02_API AAIMonsterBase : public ACharacter
{
GENERATED_BODY()
public:
AAIMonsterBase();
protected:
virtual void BeginPlay() override;
public:
virtual float TakeDamage(float DamageAmount, struct FDamageEvent const& DamageEvent, class AController* EventInstigator, AActor* DamageCauser) override;
// ... (기타 멤버들) ...
protected:
// 월드에 위젯을 표시할 컴포넌트
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "UI")
class UWidgetComponent* HealthBarWidgetComponent;
// 체력 바를 숨기기 위한 타이머 핸들
FTimerHandle HealthBarTimerHandle;
// 체력 바를 숨기는 함수
void HideHealthBar();
};
#include "AIMonsterBase.h"
#include "Components/WidgetComponent.h"
#include "MonsterHealthBar.h" // 우리가 만든 위젯 헤더
AAIMonsterBase::AAIMonsterBase()
{
// ...
HealthBarWidgetComponent = CreateDefaultSubobject<UWidgetComponent>(TEXT("HealthBarWidget"));
HealthBarWidgetComponent->SetupAttachment(GetMesh());
HealthBarWidgetComponent->SetWidgetSpace(EWidgetSpace::Screen);
HealthBarWidgetComponent->SetDrawAtDesiredSize(true);
HealthBarWidgetComponent->SetRelativeLocation(FVector(0.f, 0.f, 200.f));
}
void AAIMonsterBase::BeginPlay()
{
Super::BeginPlay();
if (HealthBarWidgetComponent)
{
HealthBarWidgetComponent->SetVisibility(false); // 시작 시 숨김
}
}
void AAIMonsterBase::HideHealthBar()
{
if (HealthBarWidgetComponent)
{
HealthBarWidgetComponent->SetVisibility(false); // 타이머 만료 시 숨김
}
}
float AAIMonsterBase::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
const float ActualDamage = Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);
if (ActualDamage > 0.0f)
{
CurrentHealth -= ActualDamage;
if (HealthBarWidgetComponent)
{
HealthBarWidgetComponent->SetVisibility(true); // 피격 시 표시
// 위젯을 C++ 클래스로 형변환(Cast)
UMonsterHealthBar* HealthBar = Cast<UMonsterHealthBar>(HealthBarWidgetComponent->GetUserWidgetObject());
// 성공했다면 C++ 함수를 직접 호출 (가장 안전한 방법)
if (HealthBar)
{
float HealthRatio = FMath::Clamp(CurrentHealth / MaxHealth, 0.0f, 1.0f);
HealthBar->UpdateHealthBar(HealthRatio);
}
// 3초 후 숨김 타이머 설정
GetWorldTimerManager().SetTimer(HealthBarTimerHandle, this, &AAIMonsterBase::HideHealthBar, 3.0f, false);
}
// ... (죽음 처리 등) ...
}
return ActualDamage;
}
ProcessEvent의 오해: C++에서 블루프린트의 '커스텀 이벤트'에 파라미터를 넘기려고 ProcessEvent를 사용했으나 계속 실패했다. ProcessEvent는 '함수'를 호출할 때 사용하는 것이었고, 파라미터 전달 방식도 내가 생각했던 것보다 훨씬 까다로웠다. 이 잘못된 접근 때문에 며칠을 헤맸다.FTimerHandle을 헤더 파일에 선언해놓고, 정작 Engine/TimerHandle.h 헤더를 include하지 않아 컴파일 오류가 발생했다. C++의 기본이지만, 정신없이 코딩하다 보면 놓치기 쉬운 실수다.MonsterHealthBarWidget.h처럼 클래스 종류를 이름에 또 붙여서 혼란을 자초했다. MonsterHealthBar.h (C++ 뼈대)와 WBP_MonsterHealthBar (블루프린트 결과물)처럼 명확하게 구분하는 것이 훨씬 좋았다.| 개념 | 설명 | 비고 |
|---|---|---|
| C++ 위젯 부모 클래스 | C++로 위젯의 기능적 뼈대를 만들고, 블루프린트는 상속받아 디자인에만 집중하게 하는 방식. | 가장 안정적이고 확장 가능한, 업계 표준 방식. |
BlueprintImplementableEvent | C++ 헤더에 함수를 선언하고, 실제 구현은 블루프린트의 이벤트 그래프에서 하도록 만드는 UFUNCTION 매크로. | C++에서 블루프린트로 단방향 함수 호출이 필요할 때 이상적이다. |
| 리페어런팅(Reparenting) | 기존 블루프린트의 부모 클래스를 다른 클래스(주로 우리가 만든 C++ 클래스)로 변경하는 작업. | 위젯 에디터의 그래프 탭 > 클래스 세팅에서 변경할 수 있다. (UE5 기준) |
Cast를 통한 안전한 함수 호출 | GetUserWidgetObject()로 얻은 위젯을 우리가 만든 C++ 클래스 타입으로 Cast한다. 성공하면, 컴파일러가 존재를 보장하는 함수를 직접 호출한다. | FindFunctionByName 같은 '이름으로 찾기' 방식보다 100배는 안전하다. |