다양한 기록

[UE Game Framework] #10 Game Data Management 본문

언리얼 엔진/Unreal Game Framework

[UE Game Framework] #10 Game Data Management

라구넹 2025. 1. 9. 15:05

엑셀 csv로 바꾸면 언리얼에서 임포트 가능

같은 이름을 가지는 구조체 필요

 

액셀 데이터의 임포트

- DataAsset과 유사하게 FTableRowBase를 상속받은 구조체를 선언

- 액셀의 Name 컬럼을 제외한 컬럼과 동일하게 UPROPERTY 속성을 선언

* 엑셀에  Name이라는게 존재는 해야 하는데, 이게 키값이 되기 때문 

- 엑셀 데이터를 csv로 익스포트 한 후 언리얼 엔진에 임포트

 

#pragma once

#include "CoreMinimal.h"
#include "Engine/DataTable.h"
#include "ABCharacterStat.generated.h"

USTRUCT(BlueprintType)
struct FABCharacterStat : public FTableRowBase
{
	GENERATED_BODY()

public:
	FABCharacterStat() : MaxHp(0.0f), Attack(0.0f), AttackRange(0.0f), AttackSpeed(0.0f), MovementSpeed(0.0f) {}

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Stat)
	float MaxHp;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Stat)
	float Attack;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Stat)
	float AttackRange;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Stat)
	float AttackSpeed;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Stat)
	float MovementSpeed;

	FABCharacterStat operator+(const FABCharacterStat& Other) const
	{
		const float* const ThisPtr = reinterpret_cast<const float* const>(this);
		const float* const OtherPtr = reinterpret_cast<const float* const>(&Other);

		FABCharacterStat Result;
		float* ResultPtr = reinterpret_cast<float*>(&Result);
		int32 StatNum = sizeof(FABCharacterStat) / sizeof(float);
		for (int32 i = 0; i < StatNum; i++)
		{
			ResultPtr[i] = ThisPtr[i] + OtherPtr[i];
		}

		return Result;
	}
};

ABCharacterStat.h

그냥 전부 float로 해주기

 

위에서 선언한 구조체로 데이터 테이블 만들고

 

리임포트로 받아올 수 있음

 

데이터를 관리할 싱글톤 클래스의 설정

언리얼 엔진에서 제공하는 싱글톤 클래스

- 게임 인스턴스

- 애셋 매니저

- 게임 플레이 관련 액터 (게임 모드, 게임 스테이트)

- 프로젝트에 싱글톤으로 등록한 언리얼 오브젝트

* 언리얼 오브젝트 생성자에서 사용하지 않도록 주의

* 게임 인스턴스는 보통 어플리케이션의 데이터를 관리하는 용도

하나 만들어서 쓸 거임

만들어서

프로젝트 세팅 들어가서 등록해줌

그 후 에디터 재시작

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

#pragma once

#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "ABCharacterStat.h"
#include "ABGameSingleton.generated.h"

DECLARE_LOG_CATEGORY_EXTERN(LogABGameSingleton, Error, All);

/**
 * 
 */
UCLASS()
class ARENABATTLE_API UABGameSingleton : public UObject
{
	GENERATED_BODY()
	
public:
	UABGameSingleton();
	static UABGameSingleton& Get();

// Character Set Data
public:
	FORCEINLINE FABCharacterStat GetCharacterStat(int32 InLevel) const 
	{ 
		return CharacterStatTable.IsValidIndex(InLevel) ? CharacterStatTable[InLevel] : FABCharacterStat();
	}

	UPROPERTY()
	int32 CharacterMaxLevel;

private:
	TArray<FABCharacterStat> CharacterStatTable;
};

헤더

캐릭터 스탯 테이블에 값을 옮겨받을 거임

그리고 레벨에 따른 스탯을 받는 기능이 있음

 

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


#include "GameData/ABGameSingleton.h"

DEFINE_LOG_CATEGORY(LogABGameSingleton);

UABGameSingleton::UABGameSingleton()
{
	static ConstructorHelpers::FObjectFinder<UDataTable> DataTableRef(TEXT("/Script/Engine.DataTable'/Game/ArenaBattle/GameData/ABCharacterStatTable.ABCharacterStatTable'"));
	if (nullptr != DataTableRef.Object)
	{
		const UDataTable* DataTable = DataTableRef.Object;
		check(DataTable->GetRowMap().Num() > 0);

		TArray<uint8*> ValueArray;
		DataTable->GetRowMap().GenerateValueArray(ValueArray);
		Algo::Transform(ValueArray, CharacterStatTable,
			[](uint8* Value)
			{
				return *reinterpret_cast<FABCharacterStat*>(Value);
			}
		);
	}

	CharacterMaxLevel = CharacterStatTable.Num();
	ensure(CharacterMaxLevel > 0);
}

UABGameSingleton& UABGameSingleton::Get()
{
	UABGameSingleton* Singleton = CastChecked<UABGameSingleton>(GEngine->GameSingleton);
	if (Singleton)
	{
		return *Singleton;
	}
	
	UE_LOG(LogABGameSingleton, Error, TEXT("Invalid Game Singleton"));
	return *NewObject<UABGameSingleton>();
}

Get은 그냥 싱글톤 가져와서 리턴하는 함수

 

데이터 테이블에서 데이터 들고오는 코드

uint8* -> Value의 시작 주소임

그거를 기준으로 구조체에 집어넣으면 알아서 들어감 (reinterpret_cast)

 

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

#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "GameData/ABCharacterStat.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;

	void SetLevelStat(int32 InNewLevel);
	FORCEINLINE float GetCurrentLevel() const { return CurrentLevel; }
	FORCEINLINE void SetModifierStat(const FABCharacterStat& InModifierStat) { ModifierStat = InModifierStat; }
	FORCEINLINE float GetCurrentHp() const { return CurrentHp; }
	FORCEINLINE FABCharacterStat GetTotalStat() const { return (BaseStat + ModifierStat); }
	float ApplyDamage(float InDamage);

protected:
	void SetHp(float NewHp);

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

	UPROPERTY(Transient, VisibleInstanceOnly, Category = Stat)
	float CurrentLevel;

	UPROPERTY(Transient, VisibleInstanceOnly, Category = Stat, Meta = (AllowPrivateAccess = "true"))
	FABCharacterStat BaseStat;
	
	UPROPERTY(Transient, VisibleInstanceOnly, Category = Stat, Meta = (AllowPrivateAccess = "true"))
	FABCharacterStat ModifierStat;
};

캐릭터 스탯 컴포넌트 헤더

베이스 스탯이랑 변경 스탯을 가짐

 

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


#include "CharacterStat/ABCharacterStatComponent.h"
#include "GameData/ABGameSingleton.h"

// Sets default values for this component's properties
UABCharacterStatComponent::UABCharacterStatComponent()
{
	CurrentLevel = 1;
}


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

	SetLevelStat(CurrentLevel);
	SetHp(BaseStat.MaxHp);
}

void UABCharacterStatComponent::SetLevelStat(int32 InNewLevel)
{
	CurrentLevel = FMath::Clamp(InNewLevel, 1, UABGameSingleton::Get().CharacterMaxLevel);
	BaseStat = UABGameSingleton::Get().GetCharacterStat(CurrentLevel);
	check(BaseStat.MaxHp > 0.0f);
}

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, BaseStat.MaxHp);
	OnHpChanged.Broadcast(CurrentHp);
}

싱글톤에서 스탯 가져와서 적용시키는 컴포넌트임

 

 

캐릭터에서 스탯 컴포넌트에서 뽑아서 사용

 

이제 스테이지 클리어 시 NPC 레벨이 올라가도록 할 것

기믹 건드려야 함

* OnGateTriggerBeginOverlap 내부

플레이어에 레벨 설정 넣어주고

 

액터 스폰할 때 레벨 설정해주기

 

근데 CurrentHp가 제대로 설정되지 않았음

 

이유

SpawnActor로 액터 생성 시

액터와 액터의 모든 컴포넌트의 BeginPlay 함수가 바로 호출됨

컴포넌트 코드에서 BeginPlay를 보면 SpawnActor가 수행되자마자 SelLevelStat과 SetHp 함수가 호출되는데,

이러면 Hp는 1레벨 만큼 차오름

그 이후 SetLevel을 해봤자 Hp 설정은 이후에 이루어짐

유니티도 마찬가지이긴 한데, 언리얼은 이런 문제 해결을 위해 SpawnActorDefferred 제공 (지연 생성)

마지막에 FinishSpawning 함수를 호출해여 BeginPlay 호출됨

 

이렇게 해주면 됨

 

클래스 정보는 필수로 저장해줘야 함

** PostInitializeComponents도 Finishing 이후임

보상 상자 등등 이렇게 설정해줌

이제 무기에다 스탯 넣어주기

 

아이템에 스탯이 추가됨

공격력 100, 범위 60으로 설정

장착 시 스탯 변화 설정

 

 

강해지고 거리 늘어난 것 확인 가능

 

다음은 NPC가 다른 캐릭터로 스폰되도록 하기

ABCharacterNonPlayer에 NPCMeshes가 선언되어 있으면 경로 따라가서 추가함

 

Config에 ArenaBattle 설정하면 DefaultArenaBattle.ini 불러 쓰겠다는 의미

속성에 Config 설정하면 해당 설정 이용하겠다는 의미

프로젝트가 로딩될 때 자동으로 값들이 채워지는데, 비동기적으로 처리하고 싶음

 

* 추가 예정