본문 바로가기

개발 공부 기록/UnrealEngine5

이득우의 언리얼 C++ 게임 개발의 정석 UE5로 따라잡기 - 9

개요

'이득우의 언리얼 C++ 게임 개발의 정석'의 11장 게임 데이터와 UI 위젯 내용을 바탕으로 하고 있습니다.

알라딘 책 구매 링크

 

이득우의 언리얼 C++ 게임 개발의 정석

언리얼 엔진 학습에 목말라하는 게임 개발자에게 단비 같은 언리얼 엔진 프로그래밍 책이다. 에픽게임즈 본사의 개발자 프로그램 언리얼 데브 그랜트를 수상한 저자의 책으로, 언리얼 엔진의

www.aladin.co.kr

 

본문

엑셀로 만들어진 테이블 데이터를 언리얼 엔진으로 불러들이는 기능을 구현한다.

언리얼 엔진은 게임 앱을 관리하기 위한 용도로 게임 인스턴스라는 언리얼 오브젝트를 제공한다.
이 게임 인스턴스가 캐릭터의 스탯을 관리하도록 설계하면 게임 앱이 초기화될 때 캐릭터 스탯 데이터를 불러들이고,
게임 앱이 종료 될 때까지 캐릭터 스탯 데이터는 보존된다.

GameInstance를 부모 클래스로 하는 새로운 C++클래스를 만들고 캐릭터 스탯을 관리하는 기능을 추가한다.
또한 새롭게 생성된 게임 인스턴스가 게임 앱을 관리하기 위해서는 프로젝트 세팅의 맵&모드 탭에 있는
GameInstance 항목을 바꿔줘야 한다.

 

게임을 시작하는 것은 여러 과정을 거친다. 게임 앱 초기화, 월드 생성, 월드에 레벨을 로딩해 스테이지를 생성하고 나면,
여기에 플레이어가 로그인까지 마쳐야 비로소 게임이 시작된다. 각 단계 별 호출되는 함수는 아래와 같다.

  1. 게임 앱의 초기화 - UGameInstance::Init
  2. 레벨에 속한 액터의 초기화 - AActor::PostInitializeComponents
  3. 플레이어의 로그인 - AGameMode::PostLogin
  4. 게임의 시작 - AGameMode::StartPlay, AActor::BeginPlay

언리얼 엔진에서 엑셀 데이터를 사용하기 위해서는 CSV로 저장된 파일을 읽어올 수 있으며 테이블 데이터의 각 열의 이름이 유형과 동일한 구조체를 선언해야 한다.
언리얼 엔진에서 제공하는 FTableRowBase 구조체를 상속받는 새로운 구조체를 게임 인스턴스 헤더에 선언하도록 하자.

구조체를 생성할 때 언리얼이 지정한 규칙에 따라줘야 에디터 인터페이스에서 연동해 사용할 수 있다. 언리얼 오브젝트 선언과 유사하게 USTRUCT 매크로를 구조체 선언 윗 줄에 넣고 구조체 내부에는 GENERATED_USTRUCT_BODY() 매크로를 선언한다.

구조체의 이름은 F(Framework)로 시작하도록 언리얼 코딩 표준에 명시되어 있다.

각 열의 이름과 동일한 멤버 변수를 타입에 맞춰 선언한다. 이때 테이블의 첫 번째에 위치한 Name 열 데이터는 언리얼 엔진에서 자동으로 키 값으로 사용하기 때문에 Name 열은 선언에서 제외한다.

 

구조체 선언 후 컴파일을 하고 CSV 파일을 에디터에 임포트하면 옵션을 선택할 수 있고 선언해둔 ABCharacterData가 목록에 추가된다.

 

생성된 게임 데이터 애셋을 열어보면 다음과 같이 CSV 데이터 테이블이 구분되어 보여짐을 볼 수 있다.
언리얼 엔진은 해당 테이블 데이터를 관리하도록 DataTable이라는 언리얼 오브젝트를 제공한다.
DataTable을 게임 인스턴스의 멤버 변수로 선언하고 데이터 애셋의 레퍼런스를 복사한 후, 데이터를 불러들이는 기능을 구현한다.

 

// ABGameInstance.h

#pragma once

#include "ArenaBattle.h"
#include "Engine/DataTable.h"
#include "Engine/GameInstance.h"
#include "ABGameInstance.generated.h"

/**
 * 
 */
USTRUCT(BlueprintType)
struct FABCharacterData : public FTableRowBase
{
	GENERATED_USTRUCT_BODY()

public:
	FABCharacterData() : Level(1), MaxHP(100.0f), Attack(10.0f), DropExp(10), NextExp(30) {}

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Data")
	int32 Level;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Data")
	float MaxHP;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Data")
	float Attack;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Data")
	int32 DropExp;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Data")
	int32 NextExp;
};

UCLASS()
class ARENABATTLE_API UABGameInstance : public UGameInstance
{
	GENERATED_BODY()
	
public:
	UABGameInstance();

	virtual void Init() override;
	FABCharacterData* GetABCharacterData(int32 Level);

private:
	UPROPERTY()
	class UDataTable* ABCharacterTable;
};
// ABGameInstance.cpp

#include "ABGameInstance.h"

UABGameInstance::UABGameInstance()
{
	FString CharacterDataPath = TEXT("/Script/Engine.DataTable'/Game/GameData/ABCharacterData.ABCharacterData'");
	static ConstructorHelpers::FObjectFinder<UDataTable> DT_ABCHARACTER(*CharacterDataPath);
	ABCHECK(DT_ABCHARACTER.Succeeded());
	ABCharacterTable = DT_ABCHARACTER.Object;
	ABCHECK(ABCharacterTable->GetRowMap().Num() > 0);
}

void UABGameInstance::Init()
{
	Super::Init();
	// ABLOG(Warning, TEXT("DropExp of Level 20 ABCharacter : %d"), GetABCharacterData(20)->DropExp);
}

FABCharacterData* UABGameInstance::GetABCharacterData(int32 Level)
{
	return ABCharacterTable->FindRow<FABCharacterData>(*FString::FromInt(Level), TEXT(""));
}

 

캐릭터 스탯에 대한 관리를 액터 컴포넌트가 담당할 수 있도록 새로운 액터 컴포넌트를 추가한다.
액터 클래스가 아닌 액터 컴포넌트 클래스를 추가하는 것을 유의하자.
ABCharacterStatComponent라고 액터 컴포넌트 클래스 이름을 지어 생성한 후 ABCharacter 클래스에
액터 컴포넌트를 멤버 변수로 추가해주었다.

// ABCharacter.h

public:
	UPROPERTY(VisibleAnywhere, Category = Stat)
	class UABCharacterStatComponent* CharacterStat;
// ABCharacter.cpp

CharacterStat = CreateDefaultSubobject<UABCharacterStatComponent>(TEXT("CHARACTERSTAT"));

 

컴파일을 하고 나면 ABCharacter에게 CharacterStat이라는 액터 컴포넌트가 추가된다.

 

게임 인스턴스에서 데이터를 가져와 초기화하고 레벨이 변경되면 해당 스탯이 바뀌도록 SetNewLevel 함수를 구현하고 로직을 추가한다.

// ABCharacterStatComponent.h

#pragma once

#include "ArenaBattle.h"
#include "Components/ActorComponent.h"
#include "ABCharacterStatComponent.generated.h"

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

public:	
	// Sets default values for this component's properties
	UABCharacterStatComponent();

protected:
	// Called when the game starts
	virtual void BeginPlay() override;
	// 액터의 PostInitializeComponents()에 대응하는 초기화 함수이다.
	// 액터의 PostInitializeComponents()가 호출되기 바로 전에 호출된다.
	virtual void InitializeComponent() override;

public:
	void SetNewLevel(int32 NewLevel);

private:
	struct FABCharacterData* CurrentStatData = nullptr;

	UPROPERTY(EditInstanceOnly, Category = Stat, meta = (AllowPrivateAccess = true))
	int32 Level;

	UPROPERTY(Transient, VisibleInstanceOnly, Category = Stat, meta = (AllowPrivateAccess = true))
	float CurrentHP;
};
// ABCharacterStatComponent.cpp

#include "ABCharacterStatComponent.h"
#include "ABGameInstance.h"

// Sets default values for this component's properties
UABCharacterStatComponent::UABCharacterStatComponent()
{
	// Set this component to be initialized when the game starts, and to be ticked every frame.  You can turn these features
	// off to improve performance if you don't need them.
	PrimaryComponentTick.bCanEverTick = false;
	// InitializeComponent()를 호출하기 위해서는 true로 설정해주어야 한다.
	bWantsInitializeComponent = true;
	// ...

	Level = 1;
}

// Called when the game starts
void UABCharacterStatComponent::BeginPlay()
{
	Super::BeginPlay();

	// ...
	
}

void UABCharacterStatComponent::InitializeComponent()
{
	Super::InitializeComponent();
	SetNewLevel(Level);
}

void UABCharacterStatComponent::SetNewLevel(int32 NewLevel)
{
	auto ABGameInstance = Cast<UABGameInstance>(UGameplayStatics::GetGameInstance(GetWorld()));

	ABCHECK(nullptr != ABGameInstance);
	CurrentStatData = ABGameInstance->GetABCharacterData(NewLevel);
	if (nullptr != CurrentStatData)
	{
		Level = NewLevel;
		CurrentHP = CurrentStatData->MaxHP;
	}
	else
	{
		ABLOG(Error, TEXT("Level (%d) data doesn't exist"), NewLevel);
	}
}

 

언리얼 오브젝트에는 직렬화 기능이 있어서 오브젝트의 UPROPERTY 속성을 저장하고 로딩할 수 있다.
하지만 컴포넌트 스탯 중 CurrentHP 값은 게임을 시작할 때마다 변경되므로 보관하는 것보다는
Transient 키워드를 추가해 해당 속성을 직렬화에서 제거하는 것이 좋다.

배치한 캐릭터 액터의 CharacterStat의 Level을 변경하고 플레이해보면 테이블에 존재하는 HP값으로 바뀌어 로딩됨을 확인할 수 있다.

 

다음으로는 캐릭터가 대미지를 받으면 캐릭터의 TakeDamage 함수에서 직접 처리하는 것이 아닌 액터 컴포넌트가 처리하도록 구성을 변경한다. 액터 컴포넌트가 캐릭터에 의존성을 가지지 않도록, 액터 컴포넌트에 델리게이트를 선언하고 캐릭터에서 이를 바인딩시키는 형태로 구조를 설계해본다.

// ABCharacterStatComponent.h

#pragma once

#include "ArenaBattle.h"
#include "Components/ActorComponent.h"
#include "ABCharacterStatComponent.generated.h"

// 델리게이트 선언
DECLARE_MULTICAST_DELEGATE(FOnHPIsZeroDelegate);

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

public:	
	// Sets default values for this component's properties
	UABCharacterStatComponent();

protected:
	// Called when the game starts
	virtual void BeginPlay() override;
	// 액터의 PostInitializeComponents()에 대응하는 초기화 함수이다.
	// 액터의 PostInitializeComponents()가 호출되기 바로 전에 호출된다.
	virtual void InitializeComponent() override;

public:
	void SetNewLevel(int32 NewLevel);
	// 대미지처리 계산을 할 SetDamage 함수
	void SetDamage(float NewDamage);
	// 공격력 스탯만큼의 대미지를 반환하는 함수
	float GetAttack();
	// CurrentHP가 0 이하가 되었을 때 알려줄 델리게이트
	FOnHPIsZeroDelegate OnHPIsZero;

private:
	struct FABCharacterData* CurrentStatData = nullptr;

	UPROPERTY(EditInstanceOnly, Category = Stat, meta = (AllowPrivateAccess = true))
	int32 Level;

	UPROPERTY(Transient, VisibleInstanceOnly, Category = Stat, meta = (AllowPrivateAccess = true))
	float CurrentHP;
};

// ABCharacterStatComponent.cpp

#include "ABCharacterStatComponent.h"
#include "ABGameInstance.h"

// Sets default values for this component's properties
UABCharacterStatComponent::UABCharacterStatComponent()
{
	// Set this component to be initialized when the game starts, and to be ticked every frame.  You can turn these features
	// off to improve performance if you don't need them.
	PrimaryComponentTick.bCanEverTick = false;
	// InitializeComponent()를 호출하기 위해서는 true로 설정해주어야 한다.
	bWantsInitializeComponent = true;
	// ...

	Level = 1;
}

// Called when the game starts
void UABCharacterStatComponent::BeginPlay()
{
	Super::BeginPlay();

	// ...
	
}

void UABCharacterStatComponent::InitializeComponent()
{
	Super::InitializeComponent();
	SetNewLevel(Level);
}

void UABCharacterStatComponent::SetNewLevel(int32 NewLevel)
{
	auto ABGameInstance = Cast<UABGameInstance>(UGameplayStatics::GetGameInstance(GetWorld()));

	ABCHECK(nullptr != ABGameInstance);
	CurrentStatData = ABGameInstance->GetABCharacterData(NewLevel);
	if (nullptr != CurrentStatData)
	{
		Level = NewLevel;
		CurrentHP = CurrentStatData->MaxHP;
	}
	else
	{
		ABLOG(Error, TEXT("Level (%d) data doesn't exist"), NewLevel);
	}
}

void UABCharacterStatComponent::SetDamage(float NewDamage)
{
	ABCHECK(nullptr != CurrentStatData);
	// 0 ~ 최대 HP까지 범위에서 CurrentHP - NewDamage를 계산하여 현재 체력을 반환함
	CurrentHP = FMath::Clamp<float>(CurrentHP - NewDamage, 0.0f, CurrentStatData->MaxHP);
	if (CurrentHP <= 0.0f)
	{
		// 현재 체력이 0이라면 델리게이트 call
		OnHPIsZero.Broadcast();
	}
}

float UABCharacterStatComponent::GetAttack()
{
	ABCHECK(nullptr != CurrentStatData, 0.0f);
	return CurrentStatData->Attack;
}
// ABCharacter.cpp

AABCharacter::AABCharacter()
{
	// ... 생략
	CharacterStat = CreateDefaultSubobject<UABCharacterStatComponent>(TEXT("CHARACTERSTAT"));
}

void AABCharacter::PostInitializeComponents()
{
	// ... 생략
	CharacterStat->OnHPIsZero.AddLambda([this]() -> void {
		ABLOG(Warning, TEXT("OnHPIsZero"));
		ABAnim->SetDeadAnim();
		SetActorEnableCollision(false);
	});
}

float AABCharacter::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
	// ... 생략
	CharacterStat->SetDamage(FinalDamage);
}

void AABCharacter::AttackCheck()
{
	// ... 생략
	if (bResult)
	{
		if (IsValid(HitResult.GetActor()))
		{
			ABLOG(Warning, TEXT("Hit Actor Name : %s"), *HitResult.GetActor()->GetName());

			FDamageEvent DamageEvent;
			// 캐릭터 스탯의 GetAttack 함수값으로 공격력 스탯 전달하도록 수정
			HitResult.GetActor()->TakeDamage(CharacterStat->GetAttack(), DamageEvent, GetController(), this);
		}
	}
}

 

다음은 캐릭터의 HP 값이 시각적으로 보이도록 UI 위젯을 제작하고 캐릭터에 부착해본다.

콘텐츠 브라우저 우클릭 → 유저 인터페이스 → 위젯 블루프린트를 선택하여 UI 위젯을 생성한다.

현재 작업할 UI는 캐릭터 위에 작게 띄울 용도이기 때문에 우상단 Fill Screen으로 되어있는 버튼을 클릭 후 Custom으로 변경하여
150x50 크기로 변경해준다.

 

그 후 좌하단의 계층구조에서 캔버스 패널을 제거하고 팔레트에서 프로그레스 바를 찾아 드래그하여 추가한다.

 

그 후 프로그레스 바를 우클릭하여 다음으로 래핑(감싸기) → 세로 박스(Vertical Box)를 한 후,
팔레트의 프리미티브 목록에서 스페이서를 찾아 프로그레스 바의 위 아래로 추가해준다.

 

그 후 상단 스페이서, 프로그레스 바, 하단 스페이서가 세로 박스에서 차지하는 영역을 나누기 위해 우측 디테일 창에서 슬롯 크기를
자동에서 채우기로 변환하고 40, 20, 40으로 설정한다. 또한 체력 게이지를 빨갛게 표시하기 위해 프로그레스 바의 외형 탭에서
컬러 및 오파시티 채우기를 빨간색으로 변경해준다.

 

이제 만들어준 UI를 캐릭터에 부착하는 기능을 구현한다.
언리얼 엔진은 액터에 UI 위젯을 부착할 수 있도록 UWidgetComponent라는 클래스를 제공한다.

언리얼 엔진은 수많은 모듈이 뭉쳐진 집합으로 구성돼 있는데, 위젯 컴포넌트 기능은 에디터에서
자동으로 생성하는 개발 환경으로 추가되지 않기에 직접 추가를 해주어야 한다.

ArenaBattle.Build.cs 파일로 가서 “UMG” 모듈을 추가해야 현재 프로젝트에서 위젯 컴포넌트를 사용할 수 있게 된다.
그 후 캐릭터에게 위젯 컴포넌트를 추가하도록 하자.

생성된 위젯 컴포넌트는 캐릭터 머리 위로 오도록 위치를 조정하고 앞서 제작한 위젯 블루프린트의 레퍼런스를 사용해
애셋의 클래스 정보를 컴포넌트의 WidgetClass로 등록한다.

그리고 UI 위젯은 항상 플레이어를 향해 보도록 Screen 모드로 지정한다.

// ABCharacter.h

public:
	UPROPERTY(VisibleAnywhere, Category = UI)
	class UWidgetComponent* HPBarWidget;

 

이제 캐릭터의 스탯이 변동되면 프로그레스 바가 변경되도록 UI와 데이터를 연동하도록 한다.

UI 작업은 디자이너라는 공간에서 진행하지만 UI의 로직은 C++ 클래스에서 미리 만들어 제공할 수 있다.
위젯 블루프린트가 사용하는 기반 C++ 클래스는 UserWidget이므로 이를 상속받는 새로운 C++ 클래스를 만들고 ABCharacterStatComponent와 연동하도록 한다.

이번에도 상호의존성을 가지지 않게 ABCharacterStatComponent에 델리게이트를 하나 선언하고 컴포넌트의 HP 값이 바뀔 때마다
UI 위젯의 값이 자동으로 변경되도록 한다.

float 값을 0과 비교할 때는 미세한 오차 범위 내에 있는지를 보고 판단하는 것이 좋다. 언리얼 엔진은 무시 가능한 오차를 측정할 때
사용하도록 KINDA_SMALL_NUMBER라는 매크로를 제공한다.

// ABCharacterStatComponent.h

DECLARE_MULTICAST_DELEGATE(FOnHPChangedDelegate);

public:
	void SetHP(float NewHP);
	float GetHPRatio();
	
	FOnHPChangedDelegate OnHPChanged;
// ABCharacterStatComponent.cpp

void UABCharacterStatComponent::SetNewLevel(int32 NewLevel)
{
	auto ABGameInstance = Cast<UABGameInstance>(UGameplayStatics::GetGameInstance(GetWorld()));

	ABCHECK(nullptr != ABGameInstance);
	CurrentStatData = ABGameInstance->GetABCharacterData(NewLevel);
	if (nullptr != CurrentStatData)
	{
		Level = NewLevel;
		// SetHP 함수로 변경
		SetHP(CurrentStatData->MaxHP);
		// CurrentHP = CurrentStatData->MaxHP;
	}
	else
	{
		ABLOG(Error, TEXT("Level (%d) data doesn't exist"), NewLevel);
	}
}

void UABCharacterStatComponent::SetDamage(float NewDamage)
{
	ABCHECK(nullptr != CurrentStatData);
	// CurrentHP = FMath::Clamp<float>(CurrentHP - NewDamage, 0.0f, CurrentStatData->MaxHP);
	// SetHP 함수로 변경
	SetHP(FMath::Clamp<float>(CurrentHP - NewDamage, 0.0f, CurrentStatData->MaxHP));
	/*if (CurrentHP <= 0.0f)
	{
		OnHPIsZero.Broadcast();
	}*/
}

void UABCharacterStatComponent::SetHP(float NewHP)
{
	CurrentHP = NewHP;
	// HP가 변경될 때마다 Call
	OnHPChanged.Broadcast();
	if (CurrentHP < KINDA_SMALL_NUMBER)
	{
		CurrentHP = 0.0f;
		OnHPIsZero.Broadcast();
	}
}

float UABCharacterStatComponent::GetHPRatio()
{
	ABCHECK(nullptr != CurrentStatData, 0.0f);

	return (CurrentStatData->MaxHP < KINDA_SMALL_NUMBER) ? 0.0f : (CurrentHP / CurrentStatData->MaxHP);
}

 

캐릭터 컴포넌트의 델리게이트 로직을 완성하면 UI에서 캐릭터 컴포넌트에 연결해 HP가 변할 때마다
프로그레스 바를 업데이트하도록 기능을 추가한다.
예제에서는 학습을 위해서 약 포인터를 사용하여 구현하도록 했다.
만약 UI와 캐릭터가 서로 다른 액터라면 약 포인터를 사용하는 것이 바람직하다.

// ABCharacterWidget.h

#pragma once

#include "ArenaBattle.h"
#include "Blueprint/UserWidget.h"
#include "ABCharcterWidget.generated.h"

/**
 * 
 */
UCLASS()
class ARENABATTLE_API UABCharcterWidget : public UUserWidget
{
	GENERATED_BODY()
	
public:
	void BindCharacterStat(class UABCharacterStatComponent* NewCharacterStat);

private:
	// 언리얼 오브젝트의 약 포인터 선언시에는 TWeakObjectPtr을 사용
	TWeakObjectPtr<class UABCharacterStatComponent> CurrentCharacterStat;
};
// ABCharacterWidget.cpp

#include "ABCharcterWidget.h"
#include "ABCharacterStatComponent.h"

void UABCharcterWidget::BindCharacterStat(UABCharacterStatComponent* NewCharacterStat)
{
	ABCHECK(nullptr != NewCharacterStat);

	CurrentCharacterStat = NewCharacterStat;
	NewCharacterStat->OnHPChanged.AddLambda([this]() -> void {
		if (CurrentCharacterStat.IsValid())
		{
			ABLOG(Warning, TEXT("HPRatio : %f"), CurrentCharacterStat->GetHPRatio());
		}
	});
}

 

이후 캐릭터 컴포넌트와 위젯을 연결한다. 언리얼 엔진 4.21 버전부터는 위젯의 초기화 시점이 PostInitializeComponents에서
BeginPlay로 변경되었기 때문에 BeginPlay에 구현을 한다.

// ABCharacter.cpp

// Called when the game starts or when spawned
void AABCharacter::BeginPlay()
{
	Super::BeginPlay();
	
	if (APlayerController* PlayerController = Cast<APlayerController>(GetController()))
	{
		if (UEnhancedInputLocalPlayerSubsystem* SubSystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
		{
			SubSystem->AddMappingContext(DefaultContext, 0);
		}
	}

	// 위젯의 초기화 시점
	auto CharacterWidget = Cast<UABCharcterWidget>(HPBarWidget->GetUserWidgetObject());
	if (nullptr != CharacterWidget)
	{
		CharacterWidget->BindCharacterStat(CharacterStat);
	}

	// bUseControllerRotationYaw = true;
}

코드를 완성하면 위젯 블루프린트로 이동해 생성한 위젯 블루프린트를 제작한 위젯 클래스로부터 상속받도록 지정해준다.

UI 툴 우측 상단 그래프를 눌러서 그래프 창으로 이동하고, 툴바의 클래스 세팅 버튼을 누른 후 좌측 하단 부모 클래스
ABCharacterWidget으로 변경한다.

 

공격을 받으면 HPRatio 비율이 0.1씩 내려가는 것을 확인할 수 있다.

값 비율이 잘 전달되면 이제 위젯의 값을 업데이트하여 실제 UI의 체력 비율이 비주얼적으로도 업데이트 되는 것을 적용한다.

// ABCharacterWidget.cpp

#include "ABCharcterWidget.h"
#include "ABCharacterStatComponent.h"
#include "Components/ProgressBar.h"

void UABCharcterWidget::BindCharacterStat(UABCharacterStatComponent* NewCharacterStat)
{
	ABCHECK(nullptr != NewCharacterStat);

	CurrentCharacterStat = NewCharacterStat;
	/*NewCharacterStat->OnHPChanged.AddLambda([this]() -> void {
		if (CurrentCharacterStat.IsValid())
		{
			ABLOG(Warning, TEXT("HPRatio : %f"), CurrentCharacterStat->GetHPRatio());
		}
	});*/
	NewCharacterStat->OnHPChanged.AddUObject(this, &UABCharcterWidget::UpdateHPWidget);
}

void UABCharcterWidget::NativeConstruct()
{
	Super::NativeConstruct();
	HPProgressBar = Cast<UProgressBar>(GetWidgetFromName(TEXT("PB_HPBar")));
	ABCHECK(nullptr != HPProgressBar);
	UpdateHPWidget();
}

void UABCharcterWidget::UpdateHPWidget()
{
	if (CurrentCharacterStat.IsValid())
	{
		if (nullptr != HPProgressBar)
		{
			HPProgressBar->SetPercent(CurrentCharacterStat->GetHPRatio());
		}
	}
}