본문 바로가기

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

[Study] Part 2 - 캐릭터 스탯과 위젯 (7/15)

728x90
반응형

 

 

 

  • 정리
    • 액터 컴포넌트를 활용한 기능 모듈화
      1. 액터 컴포넌트를 사용해 캐릭터가 가진 기능을 분산
      2. 언리얼 델리게이트를 활용한 발행 구독 모델의 구현
      3. 위젯 컴포넌트 초기화 시점을 파악하기 위한 기존 클래스 구조의 확장 설계
  • 액터 컴포넌트를 활용한 스탯의 설계
    • 액터 컴포넌트란, 액터에 부착할 수 있는 컴포넌트 중 트랜스폼이 없는 컴포넌트를 말한다.
    • 액터의 기능을 확장할 때 컴포넌트로 분리해 모듈화할 수 있음
    • 스탯 데이터를 담당하는 컴포넌트UI 위젯을 담당하는 컴포넌트로 분리
    • 액터는 두 컴포넌트가 서로 통신하도록 중개하는 역할로 지정

 

 

 

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

 

  • 언리얼 델리게이트를 활용한 발행 구독 모델의 구현
    • 푸시(Push)형태의 알림(Notification)을 구현하는데 적합한 디자인 패턴
    • 스탯이 변경되면 델리게이트에 연결된 컴포넌트에 알림을 보내 데이터를 갱신
    • 스탯 컴포넌트와 UI 컴포넌트사이의 느슨한 결합의 생성
    • 사실 스탯 컴포넌트와 UI 위젯 컴포넌트는 서로 데이터를 주고 받지만 서로 참조할 이유는 없다. 그렇기 때문에 발행 구독 모델으로 구현

 

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

 

 

 

WBP_HpBar

  • HpBar로 사용할 VerticalBoxProgress Bar를 만들어준다.

 

  • UserWidget을 상속받은 HpBar 위젯을 만들어주고, 이것을 담는 C++클래스를 생성해준다.
    • UserWidget을 상속받되, 특수한 동작을 할 수 있도록 ABHpBarWidget 클래스를 만들어준다.

HpBar 위젯의 Parent Class를 방금 만든 ABHpBarWidget으로 변경해준다.

  • 이렇게 되면 그래프에서 별도로 로직을 지정하지 않더라도 C++에서 모든 로직을 처리하고 위젯은 자동으로 업데이트 될 것이다.

 

 

 

// 델리게이트 선언
DECLARE_MULTICAST_DELEGATE(FOnHpZeroDelegate);		// Hp가 0일때, 죽었다라는 시그널 델리게이트
DECLARE_MULTICAST_DELEGATE_OneParam(FOnHpChangedDelegate, float /*CurrentHp*/);		// 변경된 현재 Hp 값을 구독한 객체들에게 보내도록 인자 값을 설정


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:
	// 내부적으로 Hp값이 변동이 됐을때 실행할 함수
	// Hp가 변경이 되려면 반드시 이 함수를 통해 가지고 호출하도록 설정
	void SetHp(float NewHp);

	// VisibleInstanceOnly = 인스턴스마다 hp값을 다르게 설정할 수 있음
	UPROPERTY(VisibleInstanceOnly,Category = Stat)
	float MaxHp;

	// 디스크에 저장할 필요가 없는 휘발성 데이터의 경우 Transient 라는 키워드를 추가하여 디스크에 저장할때 불필요한 공간이 낭비되지 않도록 지정할 수 있음
	UPROPERTY(VisibleInstanceOnly, Category = Stat)
	float CurrentHp;
};
  • UABCharacterStatComponent.h
#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();

	// CurrentHp를 변경할 때는 SetHp 함수를 통해서 변경하도록 설정
	//CurrentHp = MaxHp;
	SetHp(MaxHp);
	
}

// 데미지 적용 로직
float UABCharacterStatComponent::ApplyDamage(float InDamage)
{
	const float PrevHp = CurrentHp;

	// 매개 변수가 음수가 들어올 수도 있으니 Clamp를 통해 다시 정의
	const float ActualDamage = FMath::Clamp<float>(InDamage, 0, InDamage);

	// CurrentHp 또한 최솟값과 최댓값을 넘지 않도록 Clamp로 정의
	// CurrentHp를 변경할 때는 SetHp 함수를 통해서 변경하도록 설정
	SetHp(PrevHp - ActualDamage);

	// 허용할수 없을정도로 작은 값일 경우
	if (CurrentHp <= KINDA_SMALL_NUMBER)
	{
		// 죽었다. 라는 델리게이트 실행/전파
		OnHpZero.Broadcast();
	}

	return ActualDamage;
}

// CurrentHp를 변경할 때는 SetHp 함수를 통해서 변경하도록 설정
void UABCharacterStatComponent::SetHp(float NewHp)
{
	CurrentHp = FMath::Clamp<float>(NewHp, 0.0f, MaxHp);

	// CurrentHp가 변경됐다라는 델리게이트 실행/전파
	OnHpChanged.Broadcast(CurrentHp);
}
  • UABCharacterStatComponent.cpp

 

  • HpBar의 스탯 컴포넌트로 사용할 로직 부분을 작성해준다.
    • 델리게이트 사용
      • DECLARE_MULTICAST_DELEGATE(FOnHpZeroDelegate);
        • Hp가 0일때, 죽었다라는 시그널 델리게이트
      • DECLARE_MULTICAST_DELEGATE_OneParam(FOnHpChangedDelegate, float /*CurrentHp*/);
        • 변경된 현재 Hp 값을 구독한 객체들에게 보내도록 인자 값을 설정
    • Set 함수를 통해서 한곳으로만 CurrentHp를 관리/변경할 수 있도록 설정

 

 

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

/**
 * 
 */
UCLASS()
class ARENABATTLE_API UABHpBarWidget : public UUserWidget
{
	GENERATED_BODY()
	
public:
	// UserWidget의 경우 일반적인 생성자를 쓰지않음
	UABHpBarWidget(const FObjectInitializer& ObjectInitializer);

protected:
	// 그냥 UProgressBar를 가져오게 되면 Null값이니, 위젯이 초기화될 때 HpProgressBar를 가져와서 포인터를 가져오는 기능을 추가
	// UserWidget에서 사용하는 함수임
	virtual void NativeConstruct() override;

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

protected:
	UPROPERTY()
	TObjectPtr<class UProgressBar> HpProgressBar;		// Build Dependency에서 UMG 추가하기

	UPROPERTY()
	float MaxHp;
};
  • UABHpBarWidget.h
#include "UI/ABHpBarWidget.h"
#include "Components/ProgressBar.h"

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

// NativeConstruct 함수가 호출될 때면 UI에 관련된 모든 기능들이 거의 초기화가 완료됐다고 보면 된다.
void UABHpBarWidget::NativeConstruct()
{
	Super::NativeConstruct();

	// 아까 PbHpBar라고 Canvas에 설정함
	HpProgressBar = Cast<UProgressBar>(GetWidgetFromName(TEXT("PbHpBar")));
	ensure(HpProgressBar);
}

void UABHpBarWidget::UpdateHpBar(float NewCurrentHp)
{
	ensure(MaxHp > 0.0f);
	if (HpProgressBar)
	{
		HpProgressBar->SetPercent(NewCurrentHp / MaxHp);
	}
}
  • UABHpBarWidget.cpp

 

  • 위젯 컴포넌트를 추가해준다.
  • NativeConstruct() 함수의 경우 해당 함수가 호출될 때면 UI에 관련된 모든 기능들이 거의 초기화 완료되었다고 봐도 무방하다.

 

 

// 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;
  • ABCharacterBase.h
#include "CharacterStat/ABCharacterStatComponent.h"			// StatComponent 추가
#include "Components/WidgetComponent.h"						// WidgetComponent 추가

	// Stat Component
	Stat = CreateDefaultSubobject<UABCharacterStatComponent>(TEXT("Stat"));

	// Widget Component
	// UWidgetComponent는 트랜스폼을 가지고 있는 컴포넌트라서 트랜스폼을 설정해줘야 한다.
	HpBar = CreateDefaultSubobject<UWidgetComponent>(TEXT("Widget"));
	HpBar->SetupAttachment(GetMesh());
	HpBar->SetRelativeLocation(FVector(0.0f, 0.0f, 180.0f));

	static ConstructorHelpers::FClassFinder<UUserWidget> HpBarWidgetRef(TEXT("/Game/ArenaBattle/UI/WBP_HpBar.WBP_HpBar_C"));

	if (HpBarWidgetRef.Class)
	{
		HpBar->SetWidgetClass(HpBarWidgetRef.Class);
		HpBar->SetWidgetSpace(EWidgetSpace::Screen);		// 2D 스크린으로 공간 정의
		HpBar->SetDrawSize(FVector2D(150.0f, 15.0f));		// 캔버스의 작업공간의 크기 설정
		HpBar->SetCollisionEnabled(ECollisionEnabled::NoCollision);		// UI 충돌설정 해제
	}
  • ABCharacterBase.cpp

 

  • ABCharacterBase 클래스에 StatUI Widget과 관련된 섹션을 추가하고, 생성자에서 초기화 및 기본 설정을 마무리해준다.

 

ABCharacterBase를 클래스로 가진 캐릭터와 NPC 모두에게 HpBar가 생긴걸 볼 수 있다.

 

 

 

액터의 라이프 싸이클(Life Cycle)

  • 액터가 초기화 될 때는 디스크에 저장된 레벨 정보가 로딩이 되면서 초기화되는 과정이 있고, 아니면 스크립트를 사용해서 런타임에서 생성하는 스폰과정 두 가지가 있다.
  • 여기서 중요한 것은 거의 마지막 단계에서는 PostInitializeComponents라는 함수가 호출된다는 것. 해당 함수는 모든 컴포넌트들이 초기화가 완료된 후에 호출이 되는 함수
  • PostInitializeComponents함수가 실행되고 나면 다음은 BeginPlay 함수가 실행된다. 이때부터는 Tick이 발동 된다고 보면 된다.
  • 액터를 최종으로 마무리하고자 할 때는 PostInitializeComponents함수를 통해서 마무리를 진행해주고, 만약에 시작할 때에 초기화를 진행하고 싶을 때는 BeginPlay에서 작업을 진행해 주면 된다.
  • PostInitializeComponents
    • Tick 실행 x
  • BeginePlay
    • Tick 실행 o

 

 

  • 위젯 컴포넌트와 위젯
    • 위젯 컴포넌트는 액터 위에 UI 위젯을 띄우는 컴포넌트
    • 3차원, 2차원 모드를 지원
    • 위젯 컴포넌트는 컨테이너 역할만 할 뿐, 둘은 서로 독립적으로 동작함

위젯 컴포넌트와 위젯

 

 

  • 위젯 컴포넌트의 초기화 과정
    • 발행 구독 모델을 구현해서 스탯 컴포넌트의 데이터가 업데이트가 될 때 자동으로 위젯이 갱신되도록 기능을 구현 해야되는데, 이를 위해서는 스탯 컴포넌트의 존재를 위젯이 알아야한다.
    • 스탯 컴포넌트의 경우 PostInitializeComponents함수에서 모든 세팅이 종료가 되는데, Hp바를 구현한 UserWidget의 경우 BeginPlay 이후에 생성이 되기 때문에 이를 위해서 위젯 컴포넌트와 위젯의 확장이 필요하다.
  • 위젯 컴포넌트와 위젯의 확장
    • 위젯에 소유한 액터 정보를 보관할 수 있도록 클래스를 확장 (ABUserWidget)
    • 위젯 컴포넌트 초기화 단계에서 이를 설정할 수 있도록 클래스를 확장 (ABWidgetComponent)
    • 위젯 초기화 단계에서 부모 클래스 정보를 읽고 자신을 등록 (ABCharacterWidgetInterface)

 

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

 

 

#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;		// 액터 정보를 보관할 수 있도록
};
  • ABUserWidget.h
#pragma once

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

/**
 * 
 */
UCLASS()
class ARENABATTLE_API UABWidgetComponent : public UWidgetComponent
{
	GENERATED_BODY()
	
protected:
	// 확장을 위해 InitWidget을 override하여 로직 구성하기
	virtual void InitWidget() override;
};
  • ABWidgetComponent.h
#include "UI/ABWidgetComponent.h"
#include "ABUserWidget.h"

void UABWidgetComponent::InitWidget()
{
	// InitWidget()이 실행이 된다는 것은 이미 CreateWidget()이 완료된 시점. Widget에 대한 인스턴스가 생성되어 있음
	Super::InitWidget();

	UABUserWidget* ABUserWidget = Cast<UABUserWidget>(GetWidget());

	if (ABUserWidget)
	{
		// 자신을 소유하고 있는 액터 정보를 얻어올 수 있음. OwningActor
		// ABUserWidget을 상속받는 UserWidget의 경우 여기서 초기화 된 OwningActor 값을 사용할 수 있게 된다.
		// ABHpBarWidget을 UUSerWidget이 아닌 ABUserWidget을 상속받도록 변경하기
		ABUserWidget->SetOwningActor(GetOwner());
	}
}
  • ABWidgetComponent.cpp

 

  • UserWidget을 상속받은 ABUserWidget 클래스를 만들고, 액터 정보를 보관할 수 있도록 변수를 포인터 변수를 만들어주고, Set 함수도 함께 만들어 준다.
  • UWidgetComponent를 상속받은 ABWidgetComponent를 만들 뒤, InitWidget()을 오버라이드 하여 자신을 소유하고 있는 액터 정보를 얻어올 수 있도록 기능을 확장시켜준다.

 

 

#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;
};
  • ABCharacterWidgetInterface.h
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "Interface/ABAnimationAttackInterface.h"
#include "Interface/ABCharacterWidgetInterface.h"
#include "ABCharacterBase.generated.h"

UCLASS()
class ARENABATTLE_API AABCharacterBase : public ACharacter, public IABAnimationAttackInterface, public IABCharacterWidgetInterface		// 인터페이스 상속
{
	GENERATED_BODY()

public:
	// Sets default values for this pawn's properties
	AABCharacterBase();		// 생성자

	// BeginPlay()가 시작되기 전에 Stat의 델리게이트를 등록해서 죽었을때 죽는 모션을 수행하도록 처리
	virtual void PostInitializeComponents() override;	
	
	/ ...

	// 인터페이스를 상속받았으니, 의무적으로 기능을 구현해야 함
	virtual void SetupCharacterWidget(class UABUserWidget* InUserWidget) override;
}
  • ABCharacterBase.h
#include "UI/ABWidgetComponent.h"							// WidgetComponent 추가 -> ABWidgetComponent로 변경
#include "UI/ABHpBarWidget.h"								// HpBar를 사용하기위해 헤더로 추가

// ...

void AABCharacterBase::PostInitializeComponents()
{
	Super::PostInitializeComponents();

	// Stat의 OnHpZero 델리게이트가 실행되면 SetDead 함수를 통해서 죽는 애니메이션이 재생될 수 있도록 설정
	Stat->OnHpZero.AddUObject(this, &AABCharacterBase::SetDead);
}

// ...

void AABCharacterBase::SetupCharacterWidget(UABUserWidget* InUserWidget)
{
	UABHpBarWidget* HpBarWidget = Cast<UABHpBarWidget>(InUserWidget);
	if (HpBarWidget)
	{
		// 위젯을 업데이트
		HpBarWidget->SetMaxHp(Stat->GetMaxHp());
		HpBarWidget->UpdateHpBar(Stat->GetCurrentHp());

		// 앞으로 Stat의 CurrentHp 값이 변경될 때마다 이 UpdateHpBar 함수가 호출되도록 Stat의 델리게이트에 해당 인스턴스의 멤버함수를 등록하도록 -> 두 컴포넌트간의 느슨한 결합
		Stat->OnHpChanged.AddUObject(HpBarWidget, &UABHpBarWidget::UpdateHpBar);
	}
}
  • ABCharacterBase.cpp

 

  • ABCharacterBase 에서 PostInitializeComponents() 를 오버라이딩하여 OnHpZeroSetDead함수를 바인딩해준다.
  • ABCharacterBase에서 사용할 HpBarWidgetABCharacterWidgetInterface를 통해서 위젯을 넘겨주는 형태로 구현해주고, ABCharacterBase에서 인터페이스를 상속받아 구현해주고, 델리게이트에 등록시켜준다.
  • 앞으로 StatCurrentHp 값이 변경될 때마다 UpdateHpBar 함수가 호출되도록 함수를 바인딩해준다.

 

 

CurrentHp가 변경될때마다 HpBarWidget이 업데이트 되고, SetDead()함수가 실행되면 HpBar가 보이지 않도록 설정해준다.

 

 

 

 

 

 

 

 

 

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

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

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

www.inflearn.com

 

 

 

728x90