다양한 기록

[UE Game Framework] #7 Character Stat & Widget 본문

언리얼 엔진/Unreal Game Framework

[UE Game Framework] #7 Character Stat & Widget

라구넹 2025. 1. 5. 18:40

액터 컴포넌트를 활용한 스탯의 설계

- 액터 컴포넌트 = 액터에 부착할 수 있는 컴포넌트 중 트랜스폼이 없는 컴포넌트

- 액터의 기능을 확장할 때 컴포넌트로 분리해 모듈화할 수 있음

- 스탯 데이터를 담당하는 스탯 컴포넌트와 UI 위젯을 담당하는 UI 위젯 컴포넌트로 분리

- 액터는 두 컴포넌트가 서로 통신하도록 중개하는 역할로 지정

 

언리얼 델리게이트를 활용한 발행 구독 모델의 구현

- 푸시 형태의 알림을 구현하는데 적합한 디자인 패턴

- 스탯 변경 시 델리게이트에 연결된 컴포넌트에 알림을 보내 데이터를 갱신

- 스탯 컴포넌트와 UI 컴포넌트 사이에 느슨한 결합 생성

=> 위젯 컴포넌트가 구독하고 델리케이트가 발행

액터 컴포넌트 만들기

 

HP 바 위젯 생성

 

일단 패널에서 버티컬 박스 추가

자식으로 들어간 요소들이 세로로 차곡차곡 쌓임

 

내부 설정

전체 크기는 나중에 설정할 거라 대충

이제 위젯을 담는 클래스 필요

UserWidget을 상속받는 클래스 하나 만들어줌

생성 후 만들어뒀던 위젯 블루프린트의 그래프 -> 클래스 세팅

 

부모 클래스 만든 걸로 바꿔줌

연결되서 자동으로 업데이트 될 거임 이제

 

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

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

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:	
	// Sets default values for this component's properties
	UABCharacterStatComponent();

protected:
	// Called when the game starts
	virtual void BeginPlay() override;

public:
	FOnHpZeroDelegate OnHpZero;
	FOnHpChangedDelegate OnHpChanged;

	FORCEINLINE float GetMaxHp() { return MaxHp; }
	FORCEINLINE float GetCurrentHp() { return CurrentHp; }
	float ApplyDamage(float InDamage);

protected:
	void SetHp(float NewHp);

	UPROPERTY(VisibleInstanceOnly, Category = Stat)
	float MaxHp;

	UPROPERTY(Transient, VisibleInstanceOnly, Category = Stat)
	float CurrentHp;
		
};

스탯 헤더

UPROPERTY에서 VisibleInstanceOnly는 인스턴스마다 값이 다를 수 있는데 저렇게 해주면 됨

Transient는 각 속성값이 디스크에 저장될 필요없다는 뜻

 

// Fill out your copyright notice in the Description page of Project Settings.


#include "CharacterStat/ABCharacterStatComponent.h"

// Sets default values for this component's properties
UABCharacterStatComponent::UABCharacterStatComponent()
{
	MaxHp = 200.0f;
	CurrentHp = MaxHp;
}


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

	SetHp(MaxHp);
}

float UABCharacterStatComponent::ApplyDamage(float InDamage)
{
	const float PrevHp = CurrentHp;
	const float ActualDamage = FMath::Clamp(InDamage, 0.0f, InDamage);

	SetHp(PrevHp - ActualDamage);

	if ( CurrentHp < KINDA_SMALL_NUMBER )
	{
		OnHpZero.Broadcast();
	}

	return ActualDamage;
}

void UABCharacterStatComponent::SetHp(float NewHp)
{
	CurrentHp = FMath::Clamp(NewHp, 0.0f, MaxHp);
	OnHpChanged.Broadcast(CurrentHp);
}

스탯 구현 

체력에 변동이 생기거나, 체력이 0이 되면 델리게이트 브로드 캐스팅

 

Components/ProgressBar.h는 UMG 모듈에 있음 빌드 설정에 포함시키기

 

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

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

/**
 * 
 */
UCLASS()
class ARENABATTLE_API UABHpBarWidget : public UUserWidget
{
	GENERATED_BODY()
	
public:
	UABHpBarWidget(const FObjectInitializer& ObjectInitializer);

protected:
	virtual void NativeConstruct() override;

public:
	FORCEINLINE void SetMaxHp(float NewMaxHp) { MaxHp = NewMaxHp; }
	void UpdateHpBar(float NewCurrentHp);

protected:
	UPROPERTY()
	TObjectPtr<class UProgressBar> HpProgessBar;

	UPROPERTY()
	float MaxHp;
};

생성자 특별한 거라 저거 써줘야 함

// Fill out your copyright notice in the Description page of Project Settings.


#include "UI/ABHpBarWidget.h"
#include "Components/ProgressBar.h"

UABHpBarWidget::UABHpBarWidget(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
{
	MaxHp = -1.0f;
}

void UABHpBarWidget::NativeConstruct()
{
	// 이 함수 호출 시 UI 관련 기능 초기화가 거의 끝났다고 보면 됨
	Super::NativeConstruct();

	HpProgessBar = Cast<UProgressBar>(GetWidgetFromName(TEXT("PbHpBar")));
	ensure(HpProgessBar);
}

void UABHpBarWidget::UpdateHpBar(float NewCurrentHp)
{
	ensure(MaxHp > 0.0f);

	if ( HpProgessBar )
	{

	}
}

NativeConstruct 호출 시엔 UI 기능 초기화가 끝난 상태

저 때 이름으로 위젯을 찾아줄 수 있음

근데 위젯을 캐릭터에 그냥 달 수는 없고, 컴포넌트로 만들어야 함

 

베이스 가서 스탯이랑 위젯 컴포넌트 추가

 

* 위젯은 트랜스폼을 가짐

스탯은 그냥 하나 만들면 됨 

위젯은 애니메이션 블루프린트와 유사하게 클래스 정보를 등록해서

BeginPlay 실행 시 그때 클래스 정보로부터 인스턴스가 생성되는 형태

일단 그냥 위젯 컴포넌트를 만들면 그건 껍데기임

이대로 실행 시 위 이미지처럼 나옴

이제 대미지 가해서 체력바 업데이트 되게 만들어야 함

 

액터의 초기화 과정

액터의 라이프 사이클

액터가 초기화될 때

- 디스크에 저장된 레벨 정보가 로딩이 되면서 초기화 되는 과정 (로딩)

- 스크립트를 사용해서 런타임에서 생성하는 스폰

거의 마지막 단계에서는 PostInitializeComponents 함수가 호출됨

모든 컴포넌트들이 초기화가 완료된 뒤에 호출이 되는 함수임

그 다음에 BeginPlay가 불림, 이때부터 Tick 발동

 

위젯 컴포넌트와 위젯

- 위젯 컴포넌트는 액터 위에 UI 위젯을 띄우는 컴포넌트

- 3차원 모드와 2차원 모드를 지원

- 위젯 컴포넌트는 컨테이너 역할만 할 뿐, 둘은 서로 독립적으로 동작

 

위젯 컴포넌트의 초기화 과정

- 발행 구독 모델을 위해 스탯 컴포넌트의 존재를 위젯이 알아야 함 

- UI 관련 컴포넌트들, 위젯은 액터의 BeginPlay 이후 호출됨 -> 적당한 타이밍이 따로 필요함 

- 생성 시 위젯 컴포넌트의 InitWidget 함수와 위젯의 NativeConstruct 함수를 호출

- 유저 위젯의 경우 내가 등록을 하기 위해서는 자신을 소유하는 위젯 컴포넌트한테 액터 정보를 받아야 하는데, 이걸 못하게 해놨음 => 위젯 컴포넌트와 유저 위젯을 확장해서 해당 정보를 받아올 수 있도록 클래스를 확장할 필요가 있음

- 위젯 컴포넌트에 SetOwningActor 추가, 위젯에 SetupCharacteWidget 추가

 

위젯 컴포넌트와 위젯의 확장

- 위젯에 소유한 액터 정보를 보관할 수 있도록 클래스를 확장(ABUserWidget)

- 위젯 컴포넌트 초기화 단계에서 이를 설정할 수 있도록 클래스를 확장(ABWidgetComponent)

- 위젯 초기화 단계에서 부모 클래스 정보를 읽고 자신을 등록(ABCharacterWidgetInterface)

 

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

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

/**
 * 
 */
UCLASS()
class ARENABATTLE_API UABUserWidget : public UUserWidget
{
	GENERATED_BODY()
	
public:
	FORCEINLINE void SetOwningActor(AActor* NewOwner) { OwningActor = NewOwner; }

protected:
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Actor")
	TObjectPtr<AActor> OwningActor;
};

유저 위젯은 액터 정보를 알 수 있게만 해주고

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Components/WidgetComponent.h"
#include "ABWidgetComponent.generated.h"

/**
 * 
 */
UCLASS()
class ARENABATTLE_API UABWidgetComponent : public UWidgetComponent
{
	GENERATED_BODY()
	
protected:
	virtual void InitWidget() override;
};
// Fill out your copyright notice in the Description page of Project Settings.


#include "UI/ABWidgetComponent.h"
#include "ABUserWidget.h"

void UABWidgetComponent::InitWidget()
{
	Super::InitWidget();	// Super에서 CreateWidget 발생 

	UABUserWidget* ABUserWidget = Cast<UABUserWidget>(GetWidget());
	if ( ABUserWidget )
	{
		ABUserWidget->SetOwningActor(GetOwner());
	}
}

위젯 컴포넌트에서 SetOwningActor 해주기

 

// Fill out your copyright notice in the Description page of Project Settings.


#include "UI/ABHpBarWidget.h"
#include "Components/ProgressBar.h"
#include "Interface/ABCharacterWidgetInterface.h"

UABHpBarWidget::UABHpBarWidget(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
{
	MaxHp = -1.0f;
}

void UABHpBarWidget::NativeConstruct()
{
	// 이 함수 호출 시 UI 관련 기능 초기화가 거의 끝났다고 보면 됨
	Super::NativeConstruct();

	HpProgessBar = Cast<UProgressBar>(GetWidgetFromName(TEXT("PbHpBar")));
	ensure(HpProgessBar);

	IABCharacterWidgetInterface* CharacterWidget = Cast<IABCharacterWidgetInterface>(OwningActor);
	if (CharacterWidget)
	{
		CharacterWidget->SetupCharacterWidget(this);
	}
}

void UABHpBarWidget::UpdateHpBar(float NewCurrentHp)
{
	ensure(MaxHp > 0.0f);

	if ( HpProgessBar )
	{
		HpProgessBar->SetPercent(NewCurrentHp / MaxHp);
	}
}

HP 바 위젯인데, 캐릭터마다 헤더 추가하긴 좀 그러니까 인터페이스 추가

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "ABCharacterWidgetInterface.generated.h"

// This class does not need to be modified.
UINTERFACE(MinimalAPI)
class UABCharacterWidgetInterface : public UInterface
{
	GENERATED_BODY()
};

/**
 * 
 */
class ARENABATTLE_API IABCharacterWidgetInterface
{
	GENERATED_BODY()

	// Add interface functions to this class. This is the class that will be inherited to implement this interface.
public:
	virtual void SetupCharacterWidget(class UABUserWidget* InUserWidget) = 0;
};

인터페이스에선 이렇게 되어 있는데

캐릭터 베이스에서 상속해서 위처럼 구현

최대HP, 현재 HP 설정 후 델리게이트 등록

추가적으로 죽는 이벤트도 델리게이트로 추가해둠

 

// Fill out your copyright notice in the Description page of Project Settings.


#include "UI/ABHpBarWidget.h"
#include "Components/ProgressBar.h"
#include "Interface/ABCharacterWidgetInterface.h"

UABHpBarWidget::UABHpBarWidget(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
{
	MaxHp = -1.0f;
}

void UABHpBarWidget::NativeConstruct()
{
	// 이 함수 호출 시 UI 관련 기능 초기화가 거의 끝났다고 보면 됨
	Super::NativeConstruct();

	HpProgessBar = Cast<UProgressBar>(GetWidgetFromName(TEXT("PbHpBar")));
	ensure(HpProgessBar);

	IABCharacterWidgetInterface* CharacterWidget = Cast<IABCharacterWidgetInterface>(OwningActor);
	if (CharacterWidget)
	{
		CharacterWidget->SetupCharacterWidget(this);
	}
}

void UABHpBarWidget::UpdateHpBar(float NewCurrentHp)
{
	ensure(MaxHp > 0.0f);

	if ( HpProgessBar )
	{
		HpProgessBar->SetPercent(NewCurrentHp / MaxHp);
	}
}

HP 바 구현은 위와 같이 됨

OwningActor에서 위젯 등록 함수만 빼와서 등록

 

 

잘 작동하는 것을 볼 수 있음