
캐릭터의 체력을 나타내는 체력바와 실제 캐릭터 체력이 연동되도록 설정해보자.
UActorComponent를 상속받는 C++ 클래스를 만들어 캐릭터의 스탯정보와 변경을 맡도록 하겠다.
<Header>
DECLARE_MULTICAST_DELEGATE(FOnHpZeroDelegate);
DECLARE_MULTICAST_DELEGATE_OneParam(FOnHpChangedDelegate, float /*CurrentHp*/);
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class ARENABATTLE_API UABCharacterStatComponent : public UActorComponent
{
GENERATED_BODY()
/...
public:
FOnHpZeroDelegate OnHpZero;
FOnHpChangedDelegate OnHpChanged;
FORCEINLINE float GetMaxHp() { return MaxHp; }
FORCEINLINE float GetCurrentHp() { return CurrentHp; }
float ApplyDamage(float InDamage);
protected:
// Hp 변경
void SetHp(float NewHp);
UPROPERTY(VisibleInstanceOnly,Category = Stat)
float MaxHp;
// 값들은 디스크에 저장되는데 Transient라는 매크로를 통해서 불필요한 저장을 막을수 있다.
UPROPERTY(Transient, VisibleInstanceOnly, Category = Stat)
float CurrentHp;
};
Tick()이 호출되는 것은 부하가 크기 때문에 Event가 일어날 때만 변경되도록 Tick()은 제거해준다.
이름들에서 직관적으로 알 수 있지만
OnHpZero : Hp가 0이 되면 호출되는 델리게이트
OnHpChanged : Hp가 변경되면 호출되는 델리게이트
등을 추가했다.
여러 곳에 BroadCast를 진행하기 때문에 MULTICAST, 인자가 하나이기 때문에 OneParam이 Delegate를 생성할 때 붙게된다.
<cpp>
UABCharacterStatComponent::UABCharacterStatComponent()
{
// MaxHp, CurrentHp 초기화
MaxHp = 200.0f;
SetHp(CurrentHp = MaxHp);
}
// Called when the game starts
void UABCharacterStatComponent::BeginPlay()
{
Super::BeginPlay();
SetHp(MaxHp);
}
float UABCharacterStatComponent::ApplyDamage(float InDamage)
{
const float PrevHp = CurrentHp;
// Damage가 0보다 작지않도록 Clamp
const float ActualDamage = FMath::Clamp<float>(InDamage, 0, InDamage);
// 현재 체력에 데미지 입힘
SetHp(PrevHp - ActualDamage);
// float간의 연산으로 오차가 생길 수 있기 떄문에 0과 비교하지 않는다.
if (CurrentHp <= KINDA_SMALL_NUMBER)
{
// Hp가 0이 되었다는 것을 알리는 델리게이트 호출
OnHpZero.Broadcast();
}
return ActualDamage;
}
void UABCharacterStatComponent::SetHp(float NewHp)
{
// CurrentHp에 NewHp값을 넣는데 0~MaxHp 사이의 값이 되도록 Clamp
CurrentHp = FMath::Clamp<float>(NewHp, 0.0f, MaxHp);
// Hp가 변경되었다는 것을 알리는 델리게이트 호출
OnHpChanged.Broadcast(CurrentHp);
}
UserWidget을 상속받는 C++클래스를 하나 만들어서 체력바 역할을 하도록 하겠다.
<Header>
public:
UABHpBarWidget(const FObjectInitializer& ObjectInitializer);
protected:
// 위젯이 초기화될 때 HpProgressBar를 가져오도록 함.
// 이 함수가 불릴 때에는 UI에 관련된 모든 기능들이 거의 초기화가 완료된 시점이다.
virtual void NativeConstruct()override;
public:
FORCEINLINE void SetMaxHp(float NewMaxHp) { MaxHp = NewMaxHp; }
void UpdateHpBar(float NewCurrentHp);
protected:
UPROPERTY()
TObjectPtr<class UProgressBar> HpProgressBar;
UPROPERTY()
float MaxHp;
UABHpBarWidget의 생성자의 파라미터에 FObjectInitializer가 있는 것을 볼 수 있는데 UUserWidget의 생성자가 해당 인자를 가진 생성자만 지원하기 때문이다.
FObjectInitializer는 생성자가 호출된 이후 UObject 생성(프로퍼티 초기화)을 마무리하는 내부 클래스라고 한다.
초기화할 때 HpProgressBar가 지정되야하는데 이 작업이 NativeConstruct()에서 이루어진다.
<cpp>
#include "Components/ProgressBar.h"
// ...
UABHpBarWidget::UABHpBarWidget(const FObjectInitializer& ObjectInitializer)
:Super(ObjectInitializer)
{
MaxHp = -1.0f;
}
void UABHpBarWidget::NativeConstruct()
{
Super::NativeConstruct();
HpProgressBar = Cast<UProgressBar>(GetWidgetFromName(TEXT("PbHpBar")));
ensure(HpProgressBar);
}
void UABHpBarWidget::UpdateHpBar(float NewCurrentHp)
{
ensure(MaxHp > 0.0f);
if (HpProgressBar)
{
HpProgressBar->SetPercent(NewCurrentHp / MaxHp);
}
}
생성자에서 MaxHp를 음수로 지정하면 위젯에서 문제가 생기기 때문에 초기화할 때 올바른 값으로 지정하도록 해야한다.
GetWidgetFromName()은 함수 이름에서 알 수 있듯 이름으로 위젯을 찾아오는 함수이다.
Build.cs 파일에 가서 PublicDependencyModuleNames.AddRange() 에 UMG를 추가해줘야 정상적으로 빌드가 된다.

우클릭->User Interface -> Widget Blueprint -> 방금 만든 클래스를 상속해서 Vertical Box와 Progress Bar(PbHpBar)를 다음과 같이 추가한다.

PbHpBar의 Size를 Full로 가득 채워주고 Percent는 0.5, 색을 빨간색(1,0,0)으로 설정하면 사진과 같이 된다.
위젯을 생성했지만 위젯 자체로는 캐릭터에 부착할 수 없기 때문에 위젯 컴포넌트를 이용한다.
<CharacterBase.h>
// Stat Section
protected:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Stat, Meta = (AllowPrivateAccess = "true"))
TObjectPtr<class UABCharacterStatComponent> Stat;
// UI Widget Section
protected:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Widget, Meta = (AllowPrivateAccess = "true"))
TObjectPtr<class UWidgetComponent> HpBar;
<CharacterBase.cpp>
AABCharacterBase::AABCharacterBase()
{
//...
// Stat Component
Stat = CreateDefaultSubobject<UABCharacterStatComponent>(TEXT("Stat"));
// Widget Component
HpBar = CreateDefaultSubobject<UABWidgetComponent>(TEXT("HpBar"));
HpBar->SetupAttachment(GetMesh());
HpBar->SetRelativeLocation(FVector(0.0f, 0.0f, 180.f));
static ConstructorHelpers::FClassFinder<UUserWidget> HpBarWidgetRef(TEXT("/Game/ArenaBattle/UI/WBP_HPBar.WBP_HPBar_C"));
if (HpBarWidgetRef.Class)
{
HpBar->SetWidgetClass(HpBarWidgetRef.Class);
// 위젯 공간 2D,
HpBar->SetWidgetSpace(EWidgetSpace::Screen);
// 위젯이 담길 캔버스의 작업공간 크기
HpBar->SetDrawSize(FVector2D(150.0f, 15.0f));
HpBar->SetCollisionEnabled(ECollisionEnabled::NoCollision);
}
}
아까 생성한 StatComponent와 위젯을 부착시킬 WidgetComponent를 추가한다.
그리고 생성자 내부에 컴포넌트 등록해준다.
Widget은 트랜스폼을 가진 컴포넌트이기 때문에 SetupAttackment로 위치를 지정해줘야한다.
위젯은 애니메이션 블루프린트 처럼 beginplay()가 시작되면 등록된 클래스 정보를 받아와서 인스턴스를 생성한다.
constructorhelpers를 이용해서 생성자에서 지정하면서 SetWidgetSpace()로 위젯의 공간(2D,3D), SetDrawSize()로 위젯 담길 캔버스의 크기, SetCollisionEnabled()로 충돌 방지 등의 작업을 해준다.

다음과 같이 잘 설정된 것을 볼 수 있다.
WidgetComponent와 UserWidget을 각각 상속받는 C++클래스들을 생성해서 기능을 확장시킨다.
<ABUserWidget.h>
public:
FORCEINLINE void SetOwningActor(AActor* NewOwner) { OwningActor = NewOwner; }
protected:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Actor")
TObjectPtr<AActor> OwningActor;
Actor정보를 저장하도록 OwningActor를 추가해준다.
<ABWidgetComponent.h>
protected:
virtual void InitWidget() override;
<ABWidgetComponent.cpp>
void UABWidgetComponent::InitWidget()
{
Super::InitWidget();
UABUserWidget* ABUserWidget = Cast<UABUserWidget>(GetWidget());
if (ABUserWidget)
{
ABUserWidget->SetOwningActor(GetOwner());
}
}
UWidgetComponent에서 지원하는 InitWidget()함수를 override해준다.
WidgetComponent에서 InitWidget()이 호출된 시점에는 위젯에 대한 인스턴스가 생성이 된 직후이다.
Widget에 대한 인스턴스가 생성이 되었을 때 액터정보를 세팅한다.
위에서 사용했던 위젯클래스가 UABUserWidget을 상속받게 하여 이 액터 정보를 사용할 수 있도록 한다.
그런데 위젯클래스에서 액터클래스를 직접적으로 참고하면 의존성이 발생하기 때문에 Interface를 이용한다.
<ABCharacterWidgetInterface.h>
public:
virtual void SetupCharacterWidget(class UABUserWidget* InUserWidget) = 0;
이 인터페이스를 캐릭터가 상속받아 위의 등록한 함수를 구현하도록 한다.
<ABCharacterBase.h>
class ARENABATTLE_API AABCharacterBase : public ACharacter, public IABAnimationAttackInterface, public IABCharacterWidgetInterface
{
// ....
virtual void SetupCharacterWidget(class UABUserWidget* InUserWidget);
};
<ABCharacterBase.cpp>
void AABCharacterBase::SetupCharacterWidget(UABUserWidget* InUserWidget)
{
UABHpBarWidget* HpBarWidget = Cast<UABHpBarWidget>(InUserWidget);
if (HpBarWidget)
{
HpBarWidget->SetMaxHp(Stat->GetMaxHp());
HpBarWidget->UpdateHpBar(Stat->GetCurrentHp());
Stat->OnHpChanged.AddUObject(HpBarWidget, &UABHpBarWidget::UpdateHpBar);
}
}
위젯에 캐릭터 스탯에 들어있는 MaxHp를 설정, CurrentHp로 세팅해주고 Stat에 등록했던 Delegate OnHpChanged에 위젯 내부의 함수 UpdateHpBar를 묶어서 Hp값이 변경될 때마다 호출시키도록 한다.
<ABCharacterBase.h>
virtual void BeginPlay() override;
<ABCharacterBase.cpp>
void AABCharacterBase::BeginPlay()
{
Super::PostInitializeComponents();
Stat->OnHpZero.AddUObject(this, &AABCharacterBase::SetDead);
}
void AABCharacterBase::SetDead()
{
GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_None);
PlayDeadAnimation();
SetActorEnableCollision(false);
// HpBar 안보이게 설정
HpBar->SetHiddenInGame(true);
}
float AABCharacterBase::TakeDamage(float Damage, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
Super::TakeDamage(Damage, DamageEvent, EventInstigator, DamageCauser);
Stat->ApplyDamage(Damage);
return Damage;
}
BeginPlay() 내부에 Stat에 등록한 OnHpZero 델리게이트와 SetDead()를 묶어 Hp가 0이 되면 SetDead()를 호출하도록 한다.
SetDead() 에 수정을 가해서 죽으면 HpBar가 보이지 않게 하도록 한다.
데미지를 받으면 바로 죽게하는 대신에 데미지를 적용시키도록 수정해준다.
<ABHpBarWidget.cpp>
void UABHpBarWidget::NativeConstruct()
{
Super::NativeConstruct();
HpProgressBar = Cast<UProgressBar>(GetWidgetFromName(TEXT("PbHpBar")));
ensure(HpProgressBar);
IABCharacterWidgetInterface* CharacterWidget = Cast< IABCharacterWidgetInterface>(OwningActor);
if (CharacterWidget)
{
CharacterWidget->SetupCharacterWidget(this);
}
}
위젯의 NativeConstruct()에서 SetupCharacterWidget()을 호출해서 위젯을 세팅하도록 한다.
