2-13강 헤드업 디스플레이의 구현

Ryan Ham·2024년 7월 11일
0

이득우 Unreal

목록 보기
19/23
post-thumbnail

강의 목표

  • HUD의 UI 생성 방법의 이해
  • Component, Actor, UI 위젯 초기화 process의 학습
  • 언리얼 reflection을 활용한 유연한 데이터 연동 시스템의 구현

HUD의 생성 과정

HUD란?

HUD(Head Up Display)는 위 그림에서 보는 것처럼 기본 게임화면 위에 캐릭터 Stat, Hp, 총알의 개수 등등에 해당하는 정보를 1개의 layer를 더 입혀서 나타내 준다.

HUD의 특성

  • HUD는 Player Controller에 의해 제작되고 관리되는 UI 객체
  • HUD의 구현은 위젯을 생성하고 이를 player viewport에 띄우는 과정으로 생성.
  • 이렇게 만들어진 위젯은 자신을 소유한 player controller에 접근 가능.

Player와 1:1로 매칭되는 것이 Player Controller이고, Player 화면을 최종적으로 관리하는 역할을 Player Controller가 가지고 있다.

Player Controller는 게임이 시작될 때 CreateWidget()으로 HUD Widget을 생성하고, HUD Widget은 GetOwingPlayer()을 통해서 자신을 소유하고 있는 Player Controller에 대한 정보를 얻어올 수 있다.

이전에 만들었던 HpBar는 캐릭터에 부착해서 이동해야 했기 때문에 Widget과 Widget Component로 만들었던 반면, 이번에는 Transform 정보 없이 그냥 화면 위에다가 띄우면 된다.

Player Controller에서 HUD 추가

// PlayerController.cpp

void ARyanPlayerController::BeginPlay()
{
	...

	// 생성자에서 HUD 래퍼런스를 가져오고 BeinPlay에서 CreateWidget으로 HUD를 Viewport에 붙여준다. 
	RyanHUDWidget = CreateWidget<URyanHUDWidget>(this, RyanHUDWidgetClass);
	if (RyanHUDWidget)
	{
		RyanHUDWidget->AddToViewport();
	}
}

에디터에서 HUD Widget을 만들고 Controller를 통해 아주 쉽게 HUD를 게임에서 띄울 수 있다!


HUD 만들기

우리가 만들 HUD의 구조

우리는 총 2개의 Widget Blueprint와 각각의 BP들이 부모로 가지게 될 UserWidget을 상속한 C++ 클래스를 만들 것이다.

1개의 WBP는 전체 HP, 캐릭터 Stat을 볼 수 있고 나머지 1개의 WBP는 캐릭터 Stat에 해당한다. 이렇게 만드는 이유는 Widget을 component 형태로 만들어서 modularity하게 설계하기 위함이다.

기본적인 HUD/C++ 클래스 세팅구조

  1. BP 생성 창에서 User Widget을 상속해 Widget Blueprint를 만든다.
  1. 위에서 만든 Widget BP를 관리할 C++ 클래스를 만든다. 이 또한, User Widget을 상속해서 만든다. 앞선 강의에서 Widget Component는 자신이 소유한 Actor 정보를 가지고 올 수 없었지만, HUD 같은 경우에는 GetOwingPlayer로 자신이 속해있는 Controller 정보에 바로 접근할 수 있기 때문에 바닐라 User Widget을 상속해서 만들어도 상관 없다.

  2. 1에서 만든 Widget Blueprint의 부모를 2에서 만든 C++ 클래스로 설정
    BP->Graph->Details->Class Options에서 지정 가능하다. (아래 사진 참고)

캐릭터 Stat 정보를 표시하는 HUD 만들기

전체적인 레이아웃의 형태는 Vertical Box 밑에 Horizontal Box를 배치함으로서 다음과 같은 형태를 만들어준다. HUD의 일부를 담당하는 위젯이므로 Canvas panel은 사용하지 않는다(해당 그림을 첨부하지 못했지만 Stat HUD가 포함될 전체 HUD는 전체 화면에 입혀질 것이기 때문에 처음에 Canvas panel을 기본 베이스로 깔아준다).

WBP의 TEXT에서는 Padding, Vertical alignment, 폰트 색상, 폰트 크기 등등을 다양하게 설정할 수 있다. 하지만, 일단은 값들을 하드코딩하고 이게 잘 적용되는지를 알아보자.


Component, Actor, UI Widget의 초기화 과정

앞서 기억을 되짚어 보면, 현재 Stat 데이터들은 ActorComponent를 상속한 Stat Component가 관리하고 있다. Actor의 ControllerUI Widget을 생성하는데, UI Widget 입장에서는 적절한 초기화 시점에서 데이터를 공급받아야 한다.

BeginPlay시점으로 전 단계에서는, ComponentInitialize Component가 제일 먼저 실행이 된다. 이 부분에서 Stat에 대한 데이터가 완벽하게 초기화된다. 그 다음 ActorPostInitialize Components가 실행이 된다.

BeginPlay 이후 단계에서는, Player ControllerCreateWidget을 통해서 UI Widget을 생성하게 된다. Controller에서 CreateWidget을 실행하면 UI Widget 입장에서는 NativeOnInitialized 함수를 실행하게 된다. 이 단계에서 Widget은 그냥 생성되는 것이지 아직 보여지지는 않는다.

그 다음, UI Widget을 Viewport에 보여주기 위해 Controller에서 AddToViewport 함수를 실행하면, UI Widget 내부에서는 NativeConstruct 함수를 호출하게 된다. 이 시점이 되서야 최종적으로 화면에 HUD가 보여지게 된다.

Step 1 : Stat Component 시점

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에 대한 설정을 완료하도록 코드를 작성한다.

Step 2 : Player Controller 시점

void ARyanPlayerController::BeginPlay()
{

	Super::BeginPlay();
	...
	// Controller에서는 CreateWidget -> AddToViewport 실행!
	// CreateWidget으로는 뒤에 붙는 template 타입에 대한 pointer가 생성.
	RyanHUDWidget = CreateWidget<URyanHUDWidget>(this, RyanHUDWidgetClass);
	if (RyanHUDWidget)
	{
		RyanHUDWidget->AddToViewport();
	}

}

Controller에서는 CreateWidgetAddToViewport를 통해서 HUD Widget을 생성, 화면에 출력하게 한다.

Step 3 : 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


HUD에 데이터 연동

이 부분 다시 듣기

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으로 초기화된 모습이다.

profile
🏦KAIST EE | 🏦SNU AI(빅데이터 핀테크 전문가 과정) | 📙CryptoHipsters 저자

0개의 댓글