이번에는 모든 생명체에 스탯(공격력 방어력 스피드 등등)을 부여해보자.
그런데 그냥 단순하게 생각하면 이미 짜여진 CharacterBase(움직이는 물체의 최상위 부모)에 우리가 정의하고자 하는 스탯을 가지고 있으면 되지 않을까?
물론 틀린말은 아니다. 게임의 규모가 크지 않고 한 두마리만 관리할 것이라면 당연히 그렇게 해도 크게 상관은 없다.
하지만 게임의 규모가 커지고 몬스터가 많이짐에 따라 몬스터별로 스탯을 관리하기 불편해진다.
이러한 특성을 고려해 몬스터의 스탯을 관리해보자.
구조 설계
일단 스탯이라고 하는 구조체를 정의하자.
FCreatureStat이라는 구조체를 정의하고 이 구조체에서 우리가 사용하고자 하는 스탯의 정보를 가지고 있자.
가령 공격력, 방어력, 스피드 등등을 말이다.
그리고 이를 바로 캐릭터들이 가지고 있는게 아니라 컴포넌트로 가지고 있게 한다. 즉, UActorComponent를 상속받은 PMStatComponent를 생성한 후 이 컴포넌트가 FCreatureStat을 가지고 있고 여기서 Stat에 관한 모든 추상적인 처리를 하는 것이다.
이렇게 분리를 해야만 게임에서 추상적인 동작(캐릭터가 피해를 입었거나 공격력이 올라갔다 등)을 StatComponent로 처리하게 되거 이 StatComponent에서 실제 CreatureStat을 처리하게 된다.
마지막으로 StatComponent를 Charcter가 들고 있으면 된다.
이런 구조를 가진다고 해서 당연히 다양한 종류의 몬스터를 관리할 수 있는 것은 아니다.
여기서 생각한 방안은 바로 데이터 에셋을 이용하는 것이다.
데이터 에셋은 한마디로 말하면 우리가 원하는 데이터의 집합이라고 할 수 있다.
나는 데이터 에셋에 몬스터의 이름을 키로 벨류로 구조체를 가지고 있는 Map을 하나 생성하고 그 Map에 몬스터에 대한 정보를 채울 것이다.
그리고 Singleton 객체를 하나 생성해서 게임이 시작했을 때 위 데이터 에셋을 로드하고 메모리에 저장해두었다가 몬스터가 생성됐을 때, 이 Singleton에 자신의 이름으로 로드해 정보를 가져오게 할것이다.
이제 이 구조를 직접 만들어보자.
FCreatureStat
Stat에 대한 구조체는 다음과 같다.
USTRUCT 매크로를 써서 데이터 에셋에서 작동할 수 있게 한다.
#pragma once
#include "CoreMinimal.h"
#include "PMStat.generated.h"
USTRUCT(BlueprintType)
struct FPMCreatureStat
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
float MaxHp = 100.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
float CurrentHp = 100.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
float MaxMp = 100.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
float CurrentMp = 100.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
float Str = 10.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
float Dex = 10.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
float Int = 10.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
float Defense = 5.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
float MovementSpeed = 100.f;
FPMCreatureStat operator+(const FPMCreatureStat& Other) const
{
const float* const ThisPtr = reinterpret_cast<const float* const>(this);
const float* const OtherPtr = reinterpret_cast<const float* const>(&Other);
FPMCreatureStat Result;
float* ResultPtr = reinterpret_cast<float*>(&Result);
int32 StatNum = sizeof(FPMCreatureStat) / sizeof(float);
for (int32 i = 0; i < StatNum; i++)
{
ResultPtr[i] = ThisPtr[i] + OtherPtr[i];
}
return Result;
}
float GetAttack()
{
return Str + Dex * 0.5f;
}
float GetSpeed()
{
return MovementSpeed + Dex * 0.8f;
}
};
게임의 기획에따라 위 구조체가 설계되겠지만 나는 힘과 민첩이 공격력을 관여하고 민첩은 스피드에 또 관여하게 할 것이다.
그리고 지력은 최대 마나를 늘려주는 역할을 할것이다.
더하기 오퍼레이터를 만들어서 구조체끼리의 합을 미리 만들어준다.(스탯이 상승했을 경우 등을 위한)
UPMStatComponent
컴포넌트는 추상적인 동작들을 처리하는데 유리하다.
가령 데미지를 입거나 스탯이 변화했을 때 처리를 이 컴포넌트에서 할 것이다. 또한 나중에 UI가 추가되면 여기에 델리게이트를 통해 함수를 전달해줄것이다.
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "Data/PMStat.h"
#include "PMStatComponent.generated.h"
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class PROJECTM_API UPMStatComponent : public UActorComponent
{
GENERATED_BODY()
public:
// Sets default values for this component's properties
UPMStatComponent();
FORCEINLINE void SetStat(FPMCreatureStat InStat) { BaseStat = InStat; }
FORCEINLINE void SetModifierStat(const FPMCreatureStat InModifierStat) { ModifierStat = InModifierStat; }
FORCEINLINE const float GetAttack() { return BaseStat.GetAttack() + ModifierStat.GetAttack() + WeaponAttack; }
FORCEINLINE const float GetCurrentHp() const { return BaseStat.CurrentHp + ModifierStat.CurrentHp; }
FORCEINLINE const float GetCurrentMp() const { return BaseStat.CurrentMp + ModifierStat.CurrentMp; }
FORCEINLINE const float GetMaxHp() const { return BaseStat.MaxHp + ModifierStat.MaxHp; }
FORCEINLINE const float GetMaxMp() const { return BaseStat.MaxMp + ModifierStat.MaxMp; }
FORCEINLINE const float GetSpeed() { return BaseStat.GetSpeed() + ModifierStat.GetSpeed(); }
FORCEINLINE FPMCreatureStat GetTotalStat() const { return BaseStat + ModifierStat; }
protected:
// Called when the game starts
virtual void BeginPlay() override;
UPROPERTY(Transient, VisibleInstanceOnly, Category = Stat)
float WeaponAttack = 0.f;
UPROPERTY(Transient, VisibleInstanceOnly, Category = Stat, Meta = (AllowPrivateAccess = "true"))
FPMCreatureStat BaseStat;
UPROPERTY(Transient, VisibleInstanceOnly, Category = Stat, Meta = (AllowPrivateAccess = "true"))
FPMCreatureStat ModifierStat;
};
이 역시 게임의 기획에 따라 다르게 구성되겠지만 공격력의 경우 본인 스탯 + 추가로 올라간 스탯 + 무기 공격력으로 이루어져 있다.
Character 최상위 부모
모든 생명체의 최상위 부모에는 이런 Stat을 가지고 있게 한 후 나중에 초기화 해줄 것이다.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Stat)
TObjectPtr<class UPMStatComponent> Stat;
데이터 에셋
데이터 에셋은 PrimaryDataAsset을 상속받아 구성한다.
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "Data/PMStat.h"
#include "PMStatDataAsset.generated.h"
/**
*
*/
UCLASS()
class PROJECTM_API UPMStatDataAsset : public UPrimaryDataAsset
{
GENERATED_BODY()
public:
UPMStatDataAsset();
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stat")
TMap<FName, FPMCreatureStat> Stats;
};
이렇게 TMap을 이용해 이름과 구조체를 저장한다.


이렇게 생성해주면 다음과 같이 확인할 수있다.

Modifier 라는 건 그냥 기본 스탯을 0으로 밀기 위한 값이고
Hector라는 몬스터는 다음과 같은 스탯을 가지게 에셋을 구성한다.
이를 이제 로드해서 사용하자.
Singleton
위의 데이터 에셋을 로드해서 사용하기 위해 Singleton을 이용하자.
Singleton은 언리얼엔진에서 기본적으로 제공하는 몇가지가 있지만 사용자 커스텀으로 하나를 생성할 수 있다.
이를 통해 생성된 Singleton은 자유자제로 사용이 가능하고 여러개의 컴포지션을 이용해 게임에 관여한다.
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "PMStat.h"
#include "PMGameSingleton.generated.h"
DECLARE_LOG_CATEGORY_EXTERN(LogABGameSingleton, Error, All);
/**
*
*/
UCLASS()
class PROJECTM_API UPMGameSingleton : public UObject
{
GENERATED_BODY()
public:
UPMGameSingleton();
static UPMGameSingleton& Get();
// Character Stat Data Section
public:
FPMCreatureStat GetStatForName(const FName InName);
private:
TMap<FName, FPMCreatureStat> CreatureStat;
};
이러한 코드를 만들고
프로젝트 세팅 -> General Settings 부분에서 Game Singleton class를 방금 만든 클래스로 세팅해준다.

이렇게 하면 게임이 시작할 때 딱 하나의 PMGameSingleton 인스턴스가 생성되고 전역으로 접근이 가능하다.
// Fill out your copyright notice in the Description page of Project Settings.
#include "Data/PMGameSingleton.h"
#include "Data/PMStatDataAsset.h"
DEFINE_LOG_CATEGORY(LogABGameSingleton);
UPMGameSingleton::UPMGameSingleton()
{
static ConstructorHelpers::FObjectFinder<UPMStatDataAsset> DataAsset(TEXT("/Script/ProjectM.PMStatDataAsset'/Game/ProjectM/Data/DA_PMCreatureStat.DA_PMCreatureStat'"));
if (DataAsset.Object)
{
CreatureStat.Append(DataAsset.Object->Stats);
}
}
UPMGameSingleton& UPMGameSingleton::Get()
{
UPMGameSingleton* Singleton = CastChecked<UPMGameSingleton>(GEngine->GameSingleton);
if (Singleton)
{
return *Singleton;
}
UE_LOG(LogABGameSingleton, Error, TEXT("Invalid Game Singleton"));
return *NewObject<UPMGameSingleton>();
}
FPMCreatureStat UPMGameSingleton::GetStatForName(const FName InName)
{
return CreatureStat[InName];
}
생성자에서 데이터 에셋을 가져와 우리가 사용하고자 하는 Map을 메모리에 올린다.
그 뒤로는 Get과 GetStatFroName 함수를 이용해 원하는 데이터를 복사하여 가져오게된다.
실제 사용
몬스터가 생성될 때 자신의 Name을 생성자에서 정해준다.
그 뒤에 PostInitializeComponent에서 자신의 스탯을 초기화 해준다.
void APMMonster::PostInitializeComponents()
{
Super::PostInitializeComponents();
FPMCreatureStat CreatureStat;
CreatureStat = UPMGameSingleton::Get().GetStatForName(ObjectName);
FPMCreatureStat ModifierStat;
ModifierStat = UPMGameSingleton::Get().GetStatForName(TEXT("Modifier"));
Stat->SetStat(CreatureStat);
Stat->SetModifierStat(ModifierStat);
UE_LOG(LogTemp, Log, TEXT("%f, %f"), Stat->GetMaxHp(), Stat->GetMaxMp());
}
이는 생성자에서 호출하면 안된다. 왜냐하면 아직 StatComponent가 초기화 되지 않았을 가능성이 있기 때문에 모든 컴포넌트가 초기화가 된 다음에 Stat을 넣어준다.

이렇게 실제로 확인해보면 헥터 몬스터에게 데이터가 정상적으로 입력된 모습이다.
이제 우리는 단순히 데이터 에셋에서 Map에 데이터를 추가하기만 몬스터는 자동적으로 Stat이 초기화 될것이다.
플레이어의 경우는 유저가 스탯에 직접 관여하기 때문에 따로 관리한다.
'Unreal Engine 5 > ProjectRAS' 카테고리의 다른 글
| [UE5] ProjectRAS - 콤보 공격 과 Rolling (0) | 2025.02.19 |
|---|---|
| [UE5] ProjectRAS - 몬스터 AI 제작하기 (0) | 2025.02.06 |
| [UE5] ProjectRAS - 스킬 디자인 구성2 (0) | 2025.01.15 |
| [UE5] ProjectRAS - 스킬 디자인 구성 (2) | 2025.01.08 |
| [UE5] ProjectRAS - 공격 애니메이션과 화살 발사 (0) | 2025.01.03 |