728x90
반응형
- 정리
- 헤드업디스플레이의 구현
- 플레이어 컨트롤러에서 헤드업디스플레이의 생성과 표시
- 컴포넌트, 액터, 위젯의 초기화 프로세스의 이해
- 언리얼 리플렉션을 활용한 UI 데이터의 유연한 연동
- 헤드업디스플레이의 구현
- 헤드업디스플레이의 생성 과정
- 헤드업디스플레이(HUD)는 플레이어 컨트롤러(Playercontroller)에 의해 제작되고 관리되는 UI 객체 0
- HUD의 구현은 위젯을 생성하고 이를 플레이어 뷰포트에 띄우는 과정으로 생성된다.
- 이렇게 만들어진 위젯은 자신을 소유한 플레이어 컨트롤러에 접근할 수 있다.
- HUD로 사용할 유저위젯 블루프린트를 만들어준다.
- 기존에 만들어 준 WBP_HpBar를 Canvas Panel에 셋팅해주고, 이 위젯을 관리할 C++ 클래스를 만들어 준다. (UserWidget 클래스 생성)
- 이후, 방금 만든 유저위젯 블루프린트를 우리가 만든 C++ 클래스를 상속받을수 있도록 다음과 같이 설정해준다.
#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에서 게임이 시작되면 위젯을 생성할 수 있도록 로직을 구성한다.
- 위젯을 생성할 때 필요한 클래스 정보를 담기위한 변수를 선언하고, 생성한 위젯의 포인터를 담을 변수를 선언해준다.
- UserWidget을 통해 CharacterStat을 표시하는 위젯을 만들어 준다. (Vertical Box, Horizontal Box 사용)
- WBP_ABHUD에 WBP_CharacterStat을 추가해준다.
- CharacterStat에 만들어둔 CharacterStat에 대한 데이터를 연동하도록 기능을 추가
- 액터와 컴포넌트의 초기화 프로세스 정리
- 현재 스텟 데이터같은 경우 컴포넌트가 관리하고 있음
- 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 class를 ABCharacterStatWidget 클래스로 지정해준다.
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, ModifierStat의 Setter함수와 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에서는 Stat과 HpBar를 업데이트할 수 있는 함수들을 만들어주고, 제작한 컨트롤 위젯 클래스 멤버변수들을 선언해준다.
- 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 구조체에 있는 속성 값들을 모두 읽어서 매칭되는 위젯의 텍스트 블록에 포인터를 가져오도록 순회해준다.
- Base와 Modifier에 들어갈 값을 FString으로 합성해주고, 이름을 통해 TextBlock의 포인터를 불러온다.
- 포인터를 불러왔다면 이를 각각 TMap 자료 구조 테이블에 넣어 관리하도록 한다.
- void UABCharacterStatWidget::UpdateStat()
- FABCharacterStat 구조체의 숫자 프로퍼티들을 순회하며, 각각의 프로퍼티 값들을 두 개의 다른 구조체(BaseStat, ModifierStat)에서 읽어온다.
- 그런 다음, 해당 프로퍼티 이름을 키로 하는 텍스트 블록을 찾아서 UI에 값을 업데이트해준다.
- 이를 통해 구조체의 데이터를 UI에 동적으로 반영하게 된다.
해당 포스트는 인프런의 <이득우의 언리얼 프로그래밍 Part2 -언리얼 게임 프레임웍의 이해>
강의를 수강하고 정리한 내용입니다.
이득우의 언리얼 프로그래밍 Part2 - 언리얼 게임 프레임웍의 이해 | 이득우 - 인프런
청강문화산업대학교에서 언리얼 엔진, 게임 수학, UEFN 게임제작을 가르치고 있습니다. - 이득우의 언리얼 C++ 프로그래밍, 넥슨 코리아 공식 교육 교재 선정 2023 - 스마일게이트 언리얼 프로그래
www.inflearn.com
728x90
'공부 > 이득우의 언리얼 프로그래밍' 카테고리의 다른 글
[Study] Part 2 - 게임의 완성 (15/15) (0) | 2024.08.07 |
---|---|
[Study] Part 2 - 게임플로우 다듬기 (14/15) (0) | 2024.06.24 |
[Study] Part 2 - 행동 트리 모델의 구현 (12/15) (0) | 2024.06.20 |
[Study] Part 2 - 행동 트리 모델의 이해 (11/15) (0) | 2024.06.19 |
[Study] Part 2 - 게임데이터 관리 (10/15) (0) | 2024.06.18 |