
이번엔 위젯으로 HUD를 만들어서 체력과 스탯같은 캐릭터 정보를 화면에 띄우도록 했다.
우선 HUD로 사용할 위젯 블루프린트를 생성해서 우리가 전에 만들었던 HpBar 위젯을 넣도록한다.
화면 전체를 사용할 것이기 때문에 캔버스 패널을 넣고 그 밑에 HpBar위젯을 넣는다.

HUD를 우리가 원하는대로 조정하기위해 c++ 위젯 클래스를 생성하고 방금 만든 HUD 블루프린트의 부모 클래스로 설정한다.

PlayerController가 플레이어의 화면을 최종적으로 관리하는 역할을 하기 때문에 `PlayerController에서 위젯을 띄우도록 한다.
<ABPlayerController.h>
public:
AABPlayerController();
protected:
virtual void BeginPlay() override;
//HUD Section
protected:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = HUD)
TSubclassOf<class UABHUDWidget> ABHUDWidgetClass;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = HUD)
TObjectPtr<class UABHUDWidget> ABHUDWidget;
PlayerController에 위젯 클래스와 위젯을 저장할 변수를 추가해준다.
<ABPlayerController.cpp>
AABPlayerController::AABPlayerController()
{
static ConstructorHelpers::FClassFinder<UABHUDWidget> ABHUDWidgetRef(TEXT("/Game/ArenaBattle/UI/WBP_ABHUD.WBP_ABHUD_C"));
if (ABHUDWidgetRef.Class)
{
ABHUDWidgetClass = ABHUDWidgetRef.Class;
}
}
void AABPlayerController::BeginPlay()
{
Super::BeginPlay();
FInputModeGameOnly GameOnlyInputMode;
SetInputMode(GameOnlyInputMode);
ABHUDWidget = CreateWidget<UABHUDWidget>(this, ABHUDWidgetClass);
if (ABHUDWidget)
{
// 화면에 띄우기
ABHUDWidget->AddToViewport();
}
}
우리가 생성한 위젯 블루프린트를 적용해주고 AddToViewport()로 위젯을 화면에 띄우도록 한다.

화면에 캐릭터 체력바가 연동은 안되었지만 잘 보인다.
캐릭터 스탯정보를 화면에 추가하기위해서 새로운 위젯 블루프린트를 생성한다.
이 위젯 블루프린트를 위한 C++클래스 역시 생성해주고 부모 클래스로 설정해준다.
캐릭터 정보를 세로로 나타내기 위해 vertical box를 root로 지정하고 Horizontal box로 한칸한칸 정보를 나타내도록 한다.

그리고 텍스트를 추가해서 스탯 이름, 스탯 값 등을 나타내도록 한다.
우리가 캐릭터 스탯에 넣었던 5가지 MaxHp,Attack,AttackRange,AttackSpeed 그리고 MovementSpeed를 각각 만들어서 넣어주고 VerticalBox를 가득 채우도록 한다.

대략 이런 너무 빈공간이 많은듯한 모습이 나오지만

HUD에 추가해서 크기를 조정해주면 어느정도 알맞은 크기가 된다.

Stat데이터가 초기화되는 시점을 앞으로 당기기 위해서 CharacterStatComponent의 BeginPlay()에서 이루어지는 초기화를 InitializeComponent()로 옮긴다.
<CharacterStatComponent.h>
protected:
virtual void InitializeComponent() override;
<CharacterStatComponent.cpp>
UABCharacterStatComponent::UABCharacterStatComponent()
{
CurrentLevel = 1;
AttackRadius = 50.0f;
bWantsInitializeComponent = true;
}
void UABCharacterStatComponent::InitializeComponent()
{
Super::InitializeComponent();
SetLevelStat(CurrentLevel);
SetHp(BaseStat.MaxHp);
}
bInitializeComponent를 true로 해야 InitializeComponent()가 호출된다.
BeginPlay()의 역할이 사라졌기 때문에 지워주도록 한다.
PlayerController에서 호출한 AddToViewport를 하면 생성된 위젯에서 NativeConstruct가 호출된다. 이를 이용해서 데이터를 연동시킨다.
<ABCharacterStatHUDInterface.h>
public:
virtual void SetupHUDWidget(class UABHUDWidget* InHUDWidget) = 0;
이를 위해 인터페이스 클래스를 생성한다.
<ABHUDWidget.h>
protected:
virtual void NativeConstruct() override;
protected:
UPROPERTY()
TObjectPtr<class UABHpBarWidget> HpBar;
UPROPERTY()
TObjectPtr<class UABCharacterStatWidget> CharacterStat;
<ABHUDWidget.cpp>
void UABHUDWidget::NativeConstruct()
{
Super::NativeConstruct();
HpBar = Cast<UABHpBarWidget>(GetWidgetFromName(TEXT("WidgetHpBar")));
ensure(HpBar);
CharacterStat = Cast<UABCharacterStatWidget>(GetWidgetFromName(TEXT("WidgetCharacterStat")));
ensure(CharacterStat);
IABCharacterHUDInterface* HUDPawn = Cast<IABCharacterHUDInterface>(GetOwningPlayerPawn());
if (HUDPawn)
{
HUDPawn->SetupHUDWidget(this);
}
}
체력 위젯과 스탯 위젯을 저장할 변수를 추가해주고 GetWidgetFromName를 이용해서 위젯들을 가져온다. 그래서 위에 적힌 이름과 같은 이름을 가지도록 HUD에 설정한 위젯의 이름들을 각각 알맞게 수정해준다.
GetOwningPlayer를 통해 HUD를 소유하고있는 컨트롤러를 가져올 수 있고 GetOwningPlayerPawn을 이용해 Pawn을 가져올 수도 있다.
그리고 만들었던 인터페이스를 통해 함수를 호출시킨다.
class ARENABATTLE_API AABCharacterPlayer : public AABCharacterBase, public IABCharacterHUDInterface
{
//...
// UI Section
protected:
virtual void SetupHUDWidget(class UABHUDWidget* InHUDWidget) override;
}
CharacterPlayer에 인터페이스를 구현시킨다.
들어온 인자를 사용해서 스탯에 있는 데이터를 넘겨주고 스탯에 있는 델리게이트를 바인딩 시켜주는 로직을 구현해야하는데 이를 위해 위젯의 내용을 업데이트시켜준다.
<ABHUDWidget.h>
public:
void UpdateStat(const FABCharacterStat& BaseStat, const FABCharacterStat& ModifierStat);
void UpdateHpBar(float NewCurrentHp);
<ABHUDWidget.cpp>
void UABHUDWidget::UpdateStat(const FABCharacterStat& BaseStat, const FABCharacterStat& ModifierStat)
{
FABCharacterStat TotalStat = BaseStat + ModifierStat;
HpBar->SetMaxHp(TotalStat.MaxHp);
CharacterStat->UpdateStat(BaseStat, ModifierStat);
}
void UABHUDWidget::UpdateHpBar(float NewCurrentHp)
{
HpBar->UpdateHpBar(NewCurrentHp);
}
UpdateHpBar()는 전에 만든 HpBarWidget에 있는 함수를 그대로 호출시키는 함수이다.
UpdateStat()은 BaseStat과 ModifierStat을 각각 표시하기 때문에 각각 인자로 받는다.
<ABCharacterStatWidget.H>
public:
void UpdateStat(const FABCharacterStat& BaseStat, const FABCharacterStat& ModifierStat);
캐릭터 스탯에는 BaseStat과 ModifierStat 둘다 업데이트하는 UpdateStat()함수를 추가한다.
이어서 델리게이트
<ABCharacterStatComponent.h>
DECLARE_MULTICAST_DELEGATE_TwoParams(FOnStatChangedDelegate, const FABCharacterStat& /*BaseStat*/, const FABCharacterStat& /*ModifierStat*/);
FOnStatChangedDelegate OnStatChanged;
FORCEINLINE void SetBaseStat(const FABCharacterStat& InBaseStat) { BaseStat = InBaseStat; OnStatChanged.Broadcast(BaseStat,ModifierStat); }
FORCEINLINE void SetModifierStat(const FABCharacterStat& InModifierStat) { ModifierStat = InModifierStat; OnStatChanged.Broadcast(BaseStat, ModifierStat); }
FORCEINLINE const FABCharacterStat& GetBaseStat() const { return BaseStat; }
FORCEINLINE const FABCharacterStat& GetModifierStat() const { return ModifierStat; }
BaseStat과 ModifierStat을 받는 스탯 변경을 알리는 델리게이트를 선언하고 각 스탯을 받을 수 있는 Get함수를 추가한다. 그리고 Set함수에 델리게이트 Broadcast를 추가하여 스탯이 변경될 때마다 알림을 보내도록 한다.
<ABCharacterStatComponent.cpp>
void UABCharacterStatComponent::SetLevelStat(int32 InNewLevel)
{
CurrentLevel = FMath::Clamp(InNewLevel, 1, UABGameSingleton::Get().CharacterMaxLevel);
SetBaseStat(UABGameSingleton::Get().GetCharacterStat(CurrentLevel));
check(BaseStat.MaxHp > 0.0f);
스탯을 변경하는 내용을 SetBaseStat으로 바꿔준다.
void AABCharacterPlayer::SetupHUDWidget(UABHUDWidget* InHUDWidget)
{
if (InHUDWidget)
{
InHUDWidget->UpdateStat(Stat->GetBaseStat(), Stat->GetModifierStat());
InHUDWidget->UpdateHpBar(Stat->GetCurrentHp());
Stat->OnStatChanged.AddUObject(InHUDWidget, &UABHUDWidget::UpdateStat);
Stat->OnHpChanged.AddUObject(InHUDWidget, &UABHUDWidget::UpdateHpBar);
}
}
스탯을 변경하는 내용과 델리게이트를 추가했으니 CharacterPlayer의 SetupHUDWidget의 구현을 마무리한다.
각 위젯의 Update함수를 호출해주고 델리게이트에 각 함수들을 추가해준다.

HpBar가 연동되었고 이제는 스탯 변경역시 연동시키기위해 CharacterStatWidget을 수정해주도록 한다.
void UABCharacterStatWidget::NativeConstruct()
{
Super::NativeConstruct();
// 이 위젯이 초기화될 때 FABCharacterStat이라는 구조체가 가지는 속성들의 값들을 모두 읽어가지고 여기에 매칭되는 텍스트블록에 포인터를 가져오도록 한다.
for (TFieldIterator<FNumericProperty> PropIt(FABCharacterStat::StaticStruct()); PropIt; ++PropIt)
{
const FName PropKey(PropIt->GetName());
const FName TextBaseControlName = *FString::Printf(TEXT("Txt%sBase"), *PropIt->GetName());
const FName TextModifierControlName = *FString::Printf(TEXT("Txt%sModifier"), *PropIt->GetName());
// 가져온 텍스트를 룩업 테이블에 추가
UTextBlock* BaseTextBlock = Cast<UTextBlock>(GetWidgetFromName(TextBaseControlName));
if (BaseTextBlock)
{
BaseLookup.Add(PropKey, BaseTextBlock);
}
UTextBlock* ModifierTextBlock = Cast<UTextBlock>(GetWidgetFromName(TextModifierControlName));
if (ModifierTextBlock)
{
ModifierLookup.Add(PropKey, ModifierTextBlock);
}
}
}
void UABCharacterStatWidget::UpdateStat(const FABCharacterStat& BaseStat, const FABCharacterStat& ModifierStat)
{
for (TFieldIterator<FNumericProperty> PropIt(FABCharacterStat::StaticStruct()); PropIt; ++PropIt)
{
const FName PropKey(PropIt->GetName());
// 데이터가 들어있는 것의 포인터를 넘겨주면 GetValue_InContainer를 사용해서 FNumericProperty가 가지고있는 값을 얻어올 수 있다.
float BaseData = 0.0f;
PropIt->GetValue_InContainer((const void*)&BaseStat, &BaseData);
float ModifierData = 0.0f;
PropIt->GetValue_InContainer((const void*)&ModifierStat, &ModifierData);
UTextBlock** BaseTextBlockPtr = BaseLookup.Find(PropKey);
if (BaseTextBlockPtr)
{
// SanitizeFloat로 float값을 정렬한 뒤에 FText로 변환해서 컨트롤의 텍스트에 할당
(*BaseTextBlockPtr)->SetText(FText::FromString(FString::SanitizeFloat(BaseData)));
}
UTextBlock** ModifierTextBlockPtr = ModifierLookup.Find(PropKey);
if (ModifierTextBlockPtr)
{
(*ModifierTextBlockPtr)->SetText(FText::FromString(FString::SanitizeFloat(ModifierData)));
}
}
}
NativeConstruct()를 추가해주고 StaticStruct()로 구조체 정보를 가져오고 TFieldIterator로 구조체의 속성들을 순회하여 속성 이름을 키로 가지는 맵에 텍스트 위젯들을 저장한다.
UpdateStat에서는 가져온 값을 TextBlock의 SetText를 이용해서 변경된 스탯 값을 위젯에 적용한다.
TFieldIterator와TextBlock은 처음 보는 내용인데TFieldIterator는 구조체의 속성들을 순회하는 Iterator이고TextBlock은 Text위젯이고SetText를 통해 해당 Text위젯이 나타내는 Text를 지정하는 것같다.

BaseStat과 ModifierStat이 잘 적용된 것을 볼 수 있다.