HUD(Head Up Display)는 위 그림에서 보는 것처럼 기본 게임화면 위에 캐릭터 Stat, Hp, 총알의 개수 등등에 해당하는 정보를 1개의 layer를 더 입혀서 나타내 준다.
Player와 1:1로 매칭되는 것이 Player Controller이고, Player 화면을 최종적으로 관리하는 역할을 Player Controller가 가지고 있다.
Player Controller는 게임이 시작될 때 CreateWidget()
으로 HUD Widget을 생성하고, HUD Widget은 GetOwingPlayer()
을 통해서 자신을 소유하고 있는 Player Controller에 대한 정보를 얻어올 수 있다.
이전에 만들었던 HpBar는 캐릭터에 부착해서 이동해야 했기 때문에 Widget과 Widget Component로 만들었던 반면, 이번에는 Transform 정보 없이 그냥 화면 위에다가 띄우면 된다.
// PlayerController.cpp
void ARyanPlayerController::BeginPlay()
{
...
// 생성자에서 HUD 래퍼런스를 가져오고 BeinPlay에서 CreateWidget으로 HUD를 Viewport에 붙여준다.
RyanHUDWidget = CreateWidget<URyanHUDWidget>(this, RyanHUDWidgetClass);
if (RyanHUDWidget)
{
RyanHUDWidget->AddToViewport();
}
}
에디터에서 HUD Widget을 만들고 Controller를 통해 아주 쉽게 HUD를 게임에서 띄울 수 있다!
우리는 총 2개의 Widget Blueprint와 각각의 BP들이 부모로 가지게 될 UserWidget을 상속한 C++ 클래스를 만들 것이다.
1개의 WBP는 전체 HP, 캐릭터 Stat을 볼 수 있고 나머지 1개의 WBP는 캐릭터 Stat에 해당한다. 이렇게 만드는 이유는 Widget을 component 형태로 만들어서 modularity하게 설계하기 위함이다.
User Widget
을 상속해 Widget Blueprint
를 만든다. 위에서 만든 Widget BP를 관리할 C++ 클래스를 만든다. 이 또한, User Widget을 상속해서 만든다. 앞선 강의에서 Widget Component는 자신이 소유한 Actor 정보를 가지고 올 수 없었지만, HUD 같은 경우에는 GetOwingPlayer
로 자신이 속해있는 Controller 정보에 바로 접근할 수 있기 때문에 바닐라 User Widget을 상속해서 만들어도 상관 없다.
1에서 만든 Widget Blueprint의 부모를 2에서 만든 C++ 클래스로 설정
BP->Graph->Details->Class Options에서 지정 가능하다. (아래 사진 참고)
전체적인 레이아웃의 형태는 Vertical Box
밑에 Horizontal Box
를 배치함으로서 다음과 같은 형태를 만들어준다. HUD의 일부를 담당하는 위젯이므로 Canvas panel
은 사용하지 않는다(해당 그림을 첨부하지 못했지만 Stat HUD가 포함될 전체 HUD는 전체 화면에 입혀질 것이기 때문에 처음에 Canvas panel을 기본 베이스로 깔아준다).
WBP의 TEXT에서는 Padding, Vertical alignment, 폰트 색상, 폰트 크기 등등을 다양하게 설정할 수 있다. 하지만, 일단은 값들을 하드코딩하고 이게 잘 적용되는지를 알아보자.
앞서 기억을 되짚어 보면, 현재 Stat 데이터들은 ActorComponent를 상속한 Stat Component가 관리하고 있다. Actor의 Controller가 UI Widget을 생성하는데, UI Widget 입장에서는 적절한 초기화 시점에서 데이터를 공급받아야 한다.
BeginPlay
시점으로 전 단계에서는, Component의 Initialize Component
가 제일 먼저 실행이 된다. 이 부분에서 Stat에 대한 데이터가 완벽하게 초기화된다. 그 다음 Actor의 PostInitialize Components
가 실행이 된다.
BeginPlay
이후 단계에서는, Player Controller가 CreateWidget
을 통해서 UI Widget을 생성하게 된다. Controller에서 CreateWidget
을 실행하면 UI Widget 입장에서는 NativeOnInitialized
함수를 실행하게 된다. 이 단계에서 Widget은 그냥 생성되는 것이지 아직 보여지지는 않는다.
그 다음, UI Widget을 Viewport에 보여주기 위해 Controller에서 AddToViewport
함수를 실행하면, UI Widget 내부에서는 NativeConstruct
함수를 호출하게 된다. 이 시점이 되서야 최종적으로 화면에 HUD가 보여지게 된다.
URyanStatComponent::URyanStatComponent()
{
CurrentLevel = 1;
AttackRadius = 100.0f;
// 이 변수를 true로 설정해주어야 InitializeComponent가 호출이 된다.
bWantsInitializeComponent = true;
}
void URyanStatComponent::InitializeComponent()
{
Super::InitializeComponent();
SetLevelStat(CurrentLevel);
SetHp(BaseStat.MaxHp);
}
기존 BeginPlay()
에서 데이터의 초기화를 시켜주었는데, BeginPlay
를 삭제하고 InitializeComponent()
를 override해서 이 안에 데이터들을 초기화 시켜준다. 생성자 부분에서 bWantsInitializeComponent
의 값을 true로 설정해주어야 InitializeComponent
함수를 사용 가능. 모든 가상 함수들을 다 실행시키면 엔진에서 부하가 걸려 이렇게 한 것으로 추정한다.
InitializeComponent
내부 로직에서 이때 캐릭터 Stat에 대한 설정을 완료하도록 코드를 작성한다.
void ARyanPlayerController::BeginPlay()
{
Super::BeginPlay();
...
// Controller에서는 CreateWidget -> AddToViewport 실행!
// CreateWidget으로는 뒤에 붙는 template 타입에 대한 pointer가 생성.
RyanHUDWidget = CreateWidget<URyanHUDWidget>(this, RyanHUDWidgetClass);
if (RyanHUDWidget)
{
RyanHUDWidget->AddToViewport();
}
}
Controller에서는 CreateWidget
과 AddToViewport
를 통해서 HUD Widget을 생성, 화면에 출력하게 한다.
// HUDWidget.cpp
void URyanHUDWidget::NativeConstruct()
{
Super::NativeConstruct();
HpBar = Cast<URyanHpBarWidget>(GetWidgetFromName(TEXT("WidgetHpBar")));
ensure(HpBar);
CharacterStat = Cast<URyanCharacterStatWidget>(GetWidgetFromName(TEXT("WidgetCharacterStat")));
ensure(CharacterStat);
IRyanCharacterHUDInterface* HUDPawn = Cast<IRyanCharacterHUDInterface>(GetOwningPlayerPawn());
if (HUDPawn)
{
HUDPawn->SetupHUDWidget(this);
}
}
// CharacterPlayer.cpp
// SetupHUDWidget 함수는 Stat을 업데이트하는 역할
void ARyanCharacterPlayer::SetupHUDWidget(URyanHUDWidget* InHUDWidget)
{
if (InHUDWidget)
{
InHUDWidget->UpdateStat(Stat->GetBaseStat(), Stat->GetModifierStat());
InHUDWidget->UpdateHpBar(Stat->GetCurrentHp());
Stat->OnStatChanged.AddUObject(InHUDWidget, &URyanHUDWidget::UpdateStat);
Stat->OnHpChanged.AddUObject(InHUDWidget, &URyanHUDWidget::UpdateHpBar);
}
}
HUD Widget의 NativeConstruct
에서 HpBar와 CharacterStat
24:29
이 부분 다시 듣기
Stat Component 안에 있는 Stat 데이터들이 HUD Widget 안에 있는 2개의 Widget과 연동. Stat 정보가 update괴면 자동으로 반영되도록 설계해보자.
// Player.cpp
void AMyRyanCharacter::SetupHUDWidget(URyanHUDWidget* InHUDWidget)
{
if (InHUDWidget)
{
InHUDWidget->UpdateStat(Stat->GetBaseStat(), Stat->GetModifierStat());
InHUDWidget->UpdateHpBar(Stat->GetCurrentHp());
Stat->OnStatChanged.AddUObject(InHUDWidget, &URyanHUDWidget::UpdateStat);
Stat->OnHpChanged.AddUObject(InHUDWidget, &URyanHUDWidget::UpdateHpBar);
}
}
위젯에 있는 UpdateStat
함수와 UpdateHpBar
함수를 호출해주고 이를 Delegate로 연결해준다.
// HUD_Widget.cpp
void URyanHUDWidget::UpdateStat(const FRyanCharacterStat& BaseStat, const FRyanCharacterStat& ModifierStat)
{
FRyanCharacterStat TotalStat = BaseStat + ModifierStat;
HpBar->SetMaxHp(TotalStat.MaxHp);
CharacterStat->UpdateStat(BaseStat, ModifierStat);
}
void URyanHUDWidget::UpdateHpBar(float NewCurrentHp)
{
HpBar->UpdateHpBar(NewCurrentHp);
}
UpdateStat
이라는 함수는 HUDWidget에도 존재하고, StatWidget의 부모 클래스에도 존재한다.
void URyanCharacterStatWidget::UpdateStat(const FRyanCharacterStat& BaseStat, const FRyanCharacterStat& ModifierStat)
{
for (TFieldIterator<FNumericProperty> PropIt(FRyanCharacterStat::StaticStruct()); PropIt; ++PropIt)
{
const FName PropKey(PropIt->GetName());
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)
{
(*BaseTextBlockPtr)->SetText(FText::FromString(FString::SanitizeFloat(BaseData)));
}
UTextBlock** ModifierTextBlockPtr = ModifierLookup.Find(PropKey);
if (ModifierTextBlockPtr)
{
(*ModifierTextBlockPtr)->SetText(FText::FromString(FString::SanitizeFloat(ModifierData)));
}
}
}
StatWidget의 부모 클래스에서의 UpdateStat
는
StatComponent.h에서는 아래와 같이 Stat 정보들이 수정될때마다 알림을 주는 FOnStatChangedDelegate
라는 Delegate를 하나 추가로 선언해보자.
// StatComponent.h
DECLARE_MULTICAST_DELEGATE_TwoParams(FOnStatChangedDelegate, const FRyanCharacterStat& /*BaseStat*/, const FRyanCharacterStat& /*ModifierStat*/);
Stat HUD에 색깔까지 입혀보았다. 초록색은 Base Stat, 파란색은 Modifier Stat이다. Base Stat은 우리가 만든 custom Singleton 파일에서 캐릭터 BaseStat에 해당하는 DataTable 값을 참조해 level 1에 해당되는 값을 효과적으로 들고 오는 모습이다(아래 그림 참조).
Modifier Stat은 캐릭터가 상자를 아직 먹기 전이여서 0으로 초기화된 모습이다.