728x90
반응형
- 정리
- 액터 컴포넌트를 활용한 기능 모듈화
- 액터 컴포넌트를 사용해 캐릭터가 가진 기능을 분산
- 언리얼 델리게이트를 활용한 발행 구독 모델의 구현
- 위젯 컴포넌트 초기화 시점을 파악하기 위한 기존 클래스 구조의 확장 설계
- 액터 컴포넌트를 활용한 기능 모듈화
- 액터 컴포넌트를 활용한 스탯의 설계
- 액터 컴포넌트란, 액터에 부착할 수 있는 컴포넌트 중 트랜스폼이 없는 컴포넌트를 말한다.
- 액터의 기능을 확장할 때 컴포넌트로 분리해 모듈화할 수 있음
- 스탯 데이터를 담당하는 컴포넌트와 UI 위젯을 담당하는 컴포넌트로 분리
- 액터는 두 컴포넌트가 서로 통신하도록 중개하는 역할로 지정
- 언리얼 델리게이트를 활용한 발행 구독 모델의 구현
- 푸시(Push)형태의 알림(Notification)을 구현하는데 적합한 디자인 패턴
- 스탯이 변경되면 델리게이트에 연결된 컴포넌트에 알림을 보내 데이터를 갱신
- 스탯 컴포넌트와 UI 컴포넌트사이의 느슨한 결합의 생성
- 사실 스탯 컴포넌트와 UI 위젯 컴포넌트는 서로 데이터를 주고 받지만 서로 참조할 이유는 없다. 그렇기 때문에 발행 구독 모델으로 구현
- HpBar로 사용할 VerticalBox와 Progress Bar를 만들어준다.
- UserWidget을 상속받은 HpBar 위젯을 만들어주고, 이것을 담는 C++클래스를 생성해준다.
- UserWidget을 상속받되, 특수한 동작을 할 수 있도록 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 값을 구독한 객체들에게 보내도록 인자 값을 설정
- DECLARE_MULTICAST_DELEGATE(FOnHpZeroDelegate);
- 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 클래스에 Stat과 UI Widget과 관련된 섹션을 추가하고, 생성자에서 초기화 및 기본 설정을 마무리해준다.
- 액터가 초기화 될 때는 디스크에 저장된 레벨 정보가 로딩이 되면서 초기화되는 과정이 있고, 아니면 스크립트를 사용해서 런타임에서 생성하는 스폰과정 두 가지가 있다.
- 여기서 중요한 것은 거의 마지막 단계에서는 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() 를 오버라이딩하여 OnHpZero에 SetDead함수를 바인딩해준다.
- ABCharacterBase에서 사용할 HpBarWidget을 ABCharacterWidgetInterface를 통해서 위젯을 넘겨주는 형태로 구현해주고, ABCharacterBase에서 인터페이스를 상속받아 구현해주고, 델리게이트에 등록시켜준다.
- 앞으로 Stat의 CurrentHp 값이 변경될 때마다 UpdateHpBar 함수가 호출되도록 함수를 바인딩해준다.
해당 포스트는 인프런의 <이득우의 언리얼 프로그래밍 Part2 -언리얼 게임 프레임웍의 이해>
강의를 수강하고 정리한 내용입니다.
이득우의 언리얼 프로그래밍 Part2 - 언리얼 게임 프레임웍의 이해 | 이득우 - 인프런
청강문화산업대학교에서 언리얼 엔진, 게임 수학, UEFN 게임제작을 가르치고 있습니다. - 이득우의 언리얼 C++ 프로그래밍, 넥슨 코리아 공식 교육 교재 선정 2023 - 스마일게이트 언리얼 프로그래
www.inflearn.com
728x90
'공부 > 이득우의 언리얼 프로그래밍' 카테고리의 다른 글
[Study] Part 2 - 무한맵의 제작 (9/15) (0) | 2024.06.17 |
---|---|
[Study] Part 2 - 아이템 시스템 (8/15) (0) | 2024.06.13 |
[Study] Part 2 - 캐릭터 공격 판정 (6/15) (0) | 2024.06.03 |
[Study] Part 2 - 캐릭터 콤보 액션 (5/15) (2) | 2024.06.02 |
[Study] Part 2 - 캐릭터 애니메이션 설정 (4/15) (2) | 2024.06.01 |