본문 바로가기

공부/이득우의 언리얼 프로그래밍

[Study] Part 2 - 헤드업디스플레이의 구현 (13/15)

728x90
반응형

 

 

  • 정리
    • 헤드업디스플레이의 구현
      1. 플레이어 컨트롤러에서 헤드업디스플레이의 생성과 표시
      2. 컴포넌트, 액터, 위젯의 초기화 프로세스의 이해
      3. 언리얼 리플렉션을 활용한 UI 데이터의 유연한 연동

 

  • 헤드업디스플레이의 생성 과정
    • 헤드업디스플레이(HUD)는 플레이어 컨트롤러(Playercontroller)에 의해 제작되고 관리되는 UI 객체 0
    • HUD의 구현은 위젯을 생성하고 이를 플레이어 뷰포트에 띄우는 과정으로 생성된다.
    • 이렇게 만들어진 위젯은 자신을 소유한 플레이어 컨트롤러에 접근할 수 있다.

헤드업디스플레이(=HUD)의 생성 과정 및 로직 구성

 

  • HUD로 사용할 유저위젯 블루프린트를 만들어준다.
  • 기존에 만들어 준 WBP_HpBarCanvas Panel에 셋팅해주고, 이 위젯을 관리할 C++ 클래스를 만들어 준다. (UserWidget 클래스 생성)
  • 이후, 방금 만든 유저위젯 블루프린트를 우리가 만든 C++ 클래스를 상속받을수 있도록 다음과 같이 설정해준다.

 

WBP_ABHUD>Graph>Class Settings>Class Options>Parent Class

 

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "ABPlayerController.generated.h"

UCLASS()
class ARENABATTLE_API AABPlayerController : public APlayerController
{
	GENERATED_BODY()
	
public:
	AABPlayerController();
	
protected:
	virtual void BeginPlay() override;

	// HUD Section
protected:
	// 클래스 정보 변수 선언, 위젯을 생성할 때 클래스 정보를 넘겨야한다.
	UPROPERTY(EditAnywhere, BlueprintReadWrite,Category = HUD)
	TSubclassOf<class UABHUDWidget> ABHUDWidgetClass;

	// 생성한 위젯의 포인터를 담을 변수 선언
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = HUD)
	TObjectPtr<class UABHUDWidget> ABHUDWidget;
};
  • ABPlayerController.h
#include "Player/ABPlayerController.h"
#include "UI/ABHUDWidget.h"

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();		// override의 경우 Super 붙이기

	FInputModeGameOnly GameOnlyInputMode;		// 구조체 선언
	SetInputMode(GameOnlyInputMode);			// 구조체 넘기기 (시작하자마자 포커스가 뷰 포트 안으로 들어가게 된다.)

	// 게임이 시작되면 위젯을 생성
	ABHUDWidget = CreateWidget<UABHUDWidget>(this, ABHUDWidgetClass);
	if (ABHUDWidget)
	{
		ABHUDWidget->AddToViewport();
	}
}
  • ABPlayerController.cpp

 

  • PlayerController에서 게임이 시작되면 위젯을 생성할 수 있도록 로직을 구성한다.
  • 위젯을 생성할 때 필요한 클래스 정보를 담기위한 변수를 선언하고, 생성한 위젯의 포인터를 담을 변수를 선언해준다.

 

플레이하면 설정했던대로 좌측 상단에 HP바가 생긴것을 확인할 수 있다.
WBP_CharacterStat
WBP_ABHUD

 

  • UserWidget을 통해 CharacterStat을 표시하는 위젯을 만들어 준다. (Vertical Box, Horizontal Box 사용)
  • WBP_ABHUDWBP_CharacterStat을 추가해준다.
  • CharacterStat에 만들어둔 CharacterStat에 대한 데이터를 연동하도록 기능을 추가
  • 액터와 컴포넌트의 초기화 프로세스 정리

 

컴포넌트, 액터, UI위젯의 초기화 과정

 

  • 현재 스텟 데이터같은 경우 컴포넌트가 관리하고 있음
    • InitializeComponent 시점에서 스탯에 대한 데이터를 완벽하게 초기화 시켜준다.
  • 액터이러한 모든 것을 실행하는 주체
    • 컴포넌트의 InitializeComponent 이후 PostInitializeComponents가 실행되므로 InitializeComponent에서 확정된 데이터를 이후의 함수들이 사용할 수 있다.
  • 액터에 의해 만들어진 UI 위젯들은 적절한 초기화 시점에서 데이터를 공급받아야한다.
    • 액터는 CreateWidget을 통해 UI위젯을 초기화한다. 이때 UI위젯은 NativeOnInitialized라고하는 함수가 호출이 된다. 위젯이 생성될 뿐 눈에 보여지는 것은 아니다.
    • 눈에 보여지기 위해서는 AddToViewport라고 하는 함수가 호출되어야 한다. 호출이 되면 위젯에서는 NativeConstruct라는 함수가 호출되면서 UI위젯을 최종적으로 구축하게 된다.

 

  • 스탯 컴포넌트로가서 데이터를 일찍 초기화시키도록 구조 변경 필요
UABCharacterStatComponent::UABCharacterStatComponent()
{
	// ...

	// 해당 변수를 true로 설정해야만 InitializeComponent함수를 호출하도록 설계해뒀다. 모든 컴포넌트가 가상 함수를 호출하게 되면 성능상 이슈가 있을수도 있기 때문에
	bWantsInitializeComponent = true;
}

void UABCharacterStatComponent::InitializeComponent()
{
	Super::InitializeComponent();

	SetLevelStat(CurrentLevel);
	SetHp(BaseStat.MaxHp);
}
  • ABCharacterStatComponent.cpp
class ARENABATTLE_API UABCharacterStatComponent : public UActorComponent
{
	// ...

protected:
	virtual void InitializeComponent() override;
	
	// ...
}
  • ABCharacterStatComponent.h

 

  • BeginPlay()에 있던 내용을 InitializeComponent()로 옮겨준다.
  • 그러나, 언리얼 엔진에서 InitializeComponent()를 사용하기 위해선 bWantsInitializeComponent라는 변수를 true로 설정해야만 함수를 호출하도록 설계해뒀다. 모든 컴포넌트가 가상 함수를 호출하게 되면 성능상 이슈가 있을수도 있기 때문에
  • 다음과 같이 InitializeComponent()에서 스탯의 데이터가 확정이 되면 이후 플레이어 컨트롤러(ABPlayerController)BeginPlay에서 구성해둔 AddToViewport()가 실행된다.
  • AddToViewport()가 실행되고 나면 UI위젯인 ABHUDWidget에서 NativeConstruct()가 호출된다.

 

#pragma once

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "ABHUDWidget.generated.h"

UCLASS()
class ARENABATTLE_API UABHUDWidget : public UUserWidget
{
	GENERATED_BODY()
	
public:
	UABHUDWidget(const FObjectInitializer& ObjectInitializer);

protected:
	virtual void NativeConstruct() override;
};
  • ABHUDWidget.h
#include "UI/ABHUDWidget.h"

// 위젯의 경우에는 인자(ObjectInitializer)가 하나 들어간다. 상위 클래스에 패스해주기
UABHUDWidget::UABHUDWidget(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
{

}

// 스탯 컴포넌트안에 있는 스탯 데이터들이 HUD 위젯 안의 두개의 위젯과 연동하도록 하여 스탯 데이터가 업데이트되면 자동으로 반영되도록
void UABHUDWidget::NativeConstruct()
{
	Super::NativeConstruct();
}
  • ABHUDWidget.cpp

 

  • ABHUDWidget에서 생성자 NativeConstruct()를 구성해준다.
  • 다음으론 스탯 컴포넌트안에 있는 스탯 데이터들이 HUD 위젯 안의 두개의 위젯과 연동하도록 하여 스탯 데이터가 업데이트되면 자동으로 반영되도록 로직을 구성해준다.
  • 먼저, 캐릭터 스탯 위젯을 위한 클래스(UserWidget을 상속받은 ABCharacterStatWidget)를 새로 추가하고, WBP_CharacterStat위젯의 parent classABCharacterStatWidget 클래스로 지정해준다.

 

class ARENABATTLE_API IABCharacterHUDInterface
{
	GENERATED_BODY()

public:
	virtual void SetupHUDWidget(class UABHUDWidget* InHUDWidget) = 0;
};
  • ABCharacterHUDInterface.h

 

  • 명령을 보낼 인터페이스 클래스를 생성하고, SetupHUDWidget의 이름과 함께 UABHUDWidget의 포인터를 넘겨주는 함수를 생성해준다.

 

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "GameData/ABCharacterStat.h"
#include "ABCharacterStatComponent.generated.h"

// 스텟이 수정될 때마다 알림을 보내는 델리게이트
DECLARE_MULTICAST_DELEGATE_TwoParams(FOnStatChangedDelegate, const FABCharacterStat& /*BaseStat*/, const FABCharacterStat& /*ModifierStat*/);


UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class ARENABATTLE_API UABCharacterStatComponent : public UActorComponent
{
	// ...

public:
	// 델리게이트 변수 선언
	FOnStatChangedDelegate OnStatChanged;

	// BaseStat용 Setter 함수 추가
	FORCEINLINE void SetBaseStat(const FABCharacterStat& InBaseStat) { BaseStat = InBaseStat;  OnStatChanged.Broadcast(GetBaseStat(), GetModifierStat()); }
	FORCEINLINE void SetModifierStat(const FABCharacterStat& InModifierStat) { ModifierStat = InModifierStat;  OnStatChanged.Broadcast(GetBaseStat(), GetModifierStat()); }

	// 스텟을 가져올 수 있는 Getter 함수 추가
	// BaseStat, ModifierStat Getter
	FORCEINLINE const FABCharacterStat& GetBaseStat() const { return BaseStat; }
	FORCEINLINE const FABCharacterStat& GetModifierStat() const { return ModifierStat; }
  • ABCharacterStatComponent.h
// 주어진 레벨에서 스텟 값을 변경하도록 설정
void UABCharacterStatComponent::SetLevelStat(int32 InNewLevel)
{
	// GameSingleton에서 데이터를 가져오기
	CurrentLevel = FMath::Clamp(InNewLevel, 1, UABGameSingleton::Get().CharacterMaxLevel);

	// BaseStat값을 변경하는건 SetBaseStat을 활용해 변경해준다. -> 델리게이트를 통해 브로드캐스팅
	SetBaseStat(UABGameSingleton::Get().GetCharacterStat(CurrentLevel));
	check(BaseStat.MaxHp > 0.0f);
}
  • ABCharacterStatComponent.cpp

 

  • ABCharacterStatComponent 클래스에서 스텟이 수정될 때마다 알림을 보내는 델리게이트를 추가해주고, BaseStat, ModifierStatSetter함수와 Getter함수를 만들어준다.
  • 여기서 Setter함수들을 통해 Stat이 변경될 때마다 OnStatChanged의 델리게이트에 의해 Broadcast된다.

 

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "GameData/ABCharacterStat.h"
#include "ABHUDWidget.generated.h"

UCLASS()
class ARENABATTLE_API UABHUDWidget : public UUserWidget
{
	GENERATED_BODY()
	
public:
	UABHUDWidget(const FObjectInitializer& ObjectInitializer);

// 업데이트 하는 함수들
public:
	void UpdateStat(const FABCharacterStat& BaseStat, const FABCharacterStat& ModifierStat);
	void UpdateHpBar(float NewCurrentHp);

protected:
	virtual void NativeConstruct() override;

// 제작한 컨트롤 위젯 클래스 멤버변수 선언
protected:
	UPROPERTY()
	TObjectPtr<class UABHpBarWidget> HpBar;

	UPROPERTY()
	TObjectPtr<class UABCharacterStatWidget> CharacterStat;
};
  • ABHUDWidget.h

 

  • ABHUDWidget에서는 StatHpBar업데이트할 수 있는 함수들을 만들어주고, 제작한 컨트롤 위젯 클래스 멤버변수들을 선언해준다.
    • ABHpBarWidget
    • ABCharacterStatWidget

 

#include "UI/ABHUDWidget.h"
#include "Interface/ABCharacterHUDInterface.h"
#include "ABHpBarWidget.h"
#include "ABCharacterStatWidget.h"

// 위젯의 경우에는 인자(ObjectInitializer)가 하나 들어간다. 상위 클래스에 패스해주기
UABHUDWidget::UABHUDWidget(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
{

}

// 스텟 업데이트
void UABHUDWidget::UpdateStat(const FABCharacterStat& BaseStat, const FABCharacterStat& ModifierStat)
{
	FABCharacterStat TotalStat = BaseStat + ModifierStat;
	HpBar->SetMaxHp(TotalStat.MaxHp);

	CharacterStat->UpdateStat(BaseStat, ModifierStat);
}

// HpBar 업데이트
void UABHUDWidget::UpdateHpBar(float NewCurrentHp)
{
	HpBar->UpdateHpBar(NewCurrentHp);
}

// 스탯 컴포넌트안에 있는 스탯 데이터들이 HUD 위젯 안의 두개의 위젯과 연동하도록 하여 스탯 데이터가 업데이트되면 자동으로 반영되도록
// 인터페이스를 통해 위젯을 사용하고 있는 캐릭터에 명령을 보낸다.
void UABHUDWidget::NativeConstruct()
{
	Super::NativeConstruct();

	HpBar = Cast<UABHpBarWidget>(GetWidgetFromName(TEXT("WidgetHpBar")));
	ensure(HpBar);

	CharacterStat = Cast<UABCharacterStatWidget>(GetWidgetFromName(TEXT("WidgetCharacterStat")));
	ensure(CharacterStat);

	// Pawn 얻어오기. HUD의 경우 GetOwningPlayer 함수를 사용해서 HUD를 소유하는 컨트롤러를 가져올 수 있고, GetOwningPlayerPawn을 통해 컨트롤러가 빙의하고 있는 Pawn을 바로 가져올 수 있다.
	// 해당 위젯에 전달해서 폰으로 하여금 셋업하라고 명령을 내려주면 된다.
	IABCharacterHUDInterface* HUDPawn = Cast<IABCharacterHUDInterface>(GetOwningPlayerPawn());
	if (HUDPawn)
	{
		// 자기 자신을 전달
		HUDPawn->SetupHUDWidget(this);
	}
}
  • ABHUDWidget.cpp

 

  • Pawn 얻어오기. HUD의 경우 GetOwningPlayer 함수를 사용해서 HUD소유하는 컨트롤러를 가져올 수 있고, GetOwningPlayerPawn을 통해 컨트롤러가 빙의하고 있는 Pawn을 바로 가져올 수 있다. (지금의 경우 Player가 된다.)
  • 해당 위젯에 전달해서 폰으로 하여금 셋업하라고 명령을 내려주면 된다.
  • ABCharacterHUDInterface에 자기 자신을 넘겨줌으로써 인터페이스를 통해 셋업하라고 명령을 내려준다.

 

UCLASS()
class ARENABATTLE_API AABCharacterPlayer : public AABCharacterBase, public IABCharacterHUDInterface
{
	// ...
	// UI Section
protected:
	virtual void SetupHUDWidget(class UABHUDWidget* InHUDWidget) override;
}
  • ABCharacterPlayer.h
// ...

#include "UI/ABHUDWidget.h"
#include "CharacterStat/ABCharacterStatComponent.h"

// ...

// SetupHUDWidget
void AABCharacterPlayer::SetupHUDWidget(UABHUDWidget* InHUDWidget)
{
	if (InHUDWidget)
	{
		// Getter함수를 통해 업데이트
		InHUDWidget->UpdateStat(Stat->GetBaseStat(), Stat->GetModifierStat());
		InHUDWidget->UpdateHpBar(Stat->GetCurrentHp());

		// ABHUDWidget의 업데이트함수들을 바인드
		Stat->OnStatChanged.AddUObject(InHUDWidget, &UABHUDWidget::UpdateStat);
		Stat->OnHpChanged.AddUObject(InHUDWidget, &UABHUDWidget::UpdateHpBar);
	}
}
  • ABCharacterPlayer.cpp

 

  • ABCharacterPlayer에서는 ABCharacterHUDInterface를 상속받았기 때문에 SetupHUDWidget을 구현해준다.
  • void UABHUDWidget::NativeConstruct()에서 SetupHUDWidget()을 호출하기 때문에 매개변수로 전달된 InHUDWidget을 통해 업데이트해주고, UABCharacterStatComponent클래스인 Stat델리게이트에 등록해서 바인드 해준다.

 

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "GameData/ABCharacterStat.h"
#include "ABCharacterStatWidget.generated.h"

UCLASS()
class ARENABATTLE_API UABCharacterStatWidget : public UUserWidget
{
	GENERATED_BODY()
	
public:
	virtual void NativeConstruct() override;

public:
	void UpdateStat(const FABCharacterStat& BaseStat, const FABCharacterStat& ModifierStat);

private:
	UPROPERTY()
	TMap<FName, class UTextBlock*> BaseLookup;

	UPROPERTY()
	TMap<FName, class UTextBlock*> ModifierLookup;
};
  • ABCharacterStatWidget.h
#include "UI/ABCharacterStatWidget.h"
#include "Components/TextBlock.h"		// TextBlock 접근

void UABCharacterStatWidget::NativeConstruct()
{
	Super::NativeConstruct();

	// FABCharacterStat 구조체에 있는 속성 값들을 모두 읽어서 매칭되는 위젯의 텍스트 블록에 포인터를 가져오도록
	// 구조체 정보는 StaticStruct()로 가져오기
	for (TFieldIterator<FNumericProperty> PropIt(FABCharacterStat::StaticStruct()); PropIt; ++PropIt)
	{
		// 속성의 키값
		const FName PropKey(PropIt->GetName());
		// Base에 들어갈 값을 FString으로 합성
		const FName TextBaseControlName = *FString::Printf(TEXT("Txt%sBase"), *PropIt->GetName());
		// Modifier에 들어갈 값을 FString으로 합성
		const FName TextModifierControlName = *FString::Printf(TEXT("Txt%sModifier"), *PropIt->GetName());

		// 이름을 통해 TextBlock의 포인터를 불러온다.
		// 이를 각각의 TMap자료 구조 테이블에 넣어 관리하도록 한다.
		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());

		// 주어진 구조체 포인터에서 현재 프로퍼티의 값을 가져와서 BaseData와 ModifierData에 저장한다.
		float BaseData = 0.0f;
		PropIt->GetValue_InContainer((const void*)&BaseStat, &BaseData);
		float ModifierData = 0.0f;
		PropIt->GetValue_InContainer((const void*)&ModifierStat, &ModifierData);

		// PropKey를 통해 UTextBlock 포인터를 찾는다. 만약 찾았다면 BaseData값으로 업데이트 시켜준다.
		UTextBlock** BaseTextBlockPtr = BaseLookup.Find(PropKey);
		if (BaseTextBlockPtr)
		{
			// BaseData를 문자열로 변환하고이를 FText로 변환해서 넣어준다.
			(*BaseTextBlockPtr)->SetText(FText::FromString(FString::SanitizeFloat(BaseData)));
		}

		UTextBlock** ModifierTextBlockPtr = ModifierLookup.Find(PropKey);
		if (ModifierTextBlockPtr)
		{
			(*ModifierTextBlockPtr)->SetText(FText::FromString(FString::SanitizeFloat(ModifierData)));
		}
	}
}
  • ABCharacterStatWidget.cpp

 

  • void UABCharacterStatWidget::NativeConstruct()
    • FABCharacterStat 구조체에 있는 속성 값들을 모두 읽어서 매칭되는 위젯의 텍스트 블록에 포인터를 가져오도록 순회해준다.
    • BaseModifier에 들어갈 값을 FString으로 합성해주고, 이름을 통해 TextBlock의 포인터를 불러온다.
    • 포인터를 불러왔다면 이를 각각 TMap 자료 구조 테이블에 넣어 관리하도록 한다.
  • void UABCharacterStatWidget::UpdateStat()
    • FABCharacterStat 구조체의 숫자 프로퍼티들을 순회하며, 각각의 프로퍼티 값들을 두 개의 다른 구조체(BaseStat, ModifierStat)에서 읽어온다.
    • 그런 다음, 해당 프로퍼티 이름을 키로 하는 텍스트 블록을 찾아서 UI에 값을 업데이트해준다.
    • 이를 통해 구조체의 데이터를 UI에 동적으로 반영하게 된다.

 

BaseStat과 ModifierStat이 정상적으로 보여지는 모습

 

 

 

 

 

 

해당 포스트는 인프런의 <이득우의 언리얼 프로그래밍 Part2 -언리얼 게임 프레임웍의 이해>
강의를 수강하고 정리한 내용입니다.
 

이득우의 언리얼 프로그래밍 Part2 - 언리얼 게임 프레임웍의 이해 | 이득우 - 인프런

청강문화산업대학교에서 언리얼 엔진, 게임 수학, UEFN 게임제작을 가르치고 있습니다. - 이득우의 언리얼 C++ 프로그래밍, 넥슨 코리아 공식 교육 교재 선정 2023 - 스마일게이트 언리얼 프로그래

www.inflearn.com

 

 

 

 

 

 

728x90