2-10강 게임 데이터 관리

Ryan Ham·2024년 7월 8일
0

이득우 Unreal

목록 보기
15/23
post-thumbnail

강의 목표

  • 게임 데이터를 관리하는 Singleton 객체의 등록
  • Excel 데이터 및 INI 파일을 활용한 게임 데이터의 관리
  • 데이터 테이블 기반의 캐릭터 stat 시스템 구축
  • 지연 생성을 활용한 액터 초기화 방법의 이해

언리얼에서 CSV 값 읽기

사전에 CSV column 값 파악하기

Python의 Pandas가 그리워지는 것은 기분탓인가..
언리얼에서 CSV 값을 읽으려면 사전에 CSV Table의 Column value를 미리 다 알아서 해당 header에 변수를 선언해 주어야 한다. 만약, CSV가 아닌 Excel 파일을 초기에 가지고 있었다면, Excel을 다른 이름으로 저장하고 인코딩 방식을 CSV로 하여 CSV 파일을 생성한다.

// CharacterStat.h
USTRUCT(BlueprintType)
// 구조체를 선언할떄는 FTableRowBase하는 구조체를 상속 받아야 한다. 
struct FRyanCharacterStat : public FTableRowBase
{
	GENERATED_BODY()

public:
	FRyanCharacterStat() : MaxHp(0.0f), Attack(0.0f), AttackRange(0.0f), AttackSpeed(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;

	// 덧셈 연산자에 대해서 operator overloading을 구현
	FRyanCharacterStat operator+(const FRyanCharacterStat& Other) const
	{
		const float* const ThisPtr = reinterpret_cast<const float* const>(this);
		const float* const OtherPtr = reinterpret_cast<const float* const>(&Other);

		FRyanCharacterStat Result;
		float* ResultPtr = reinterpret_cast<float*>(&Result);
		int32 StatNum = sizeof(FRyanCharacterStat) / sizeof(float);
		// 모든 member variable이 float으로 되어 있기 때문에 이렇게 순회하면 모든 값들이 업데이트 된다.  
		for (int32 i = 0; i < StatNum; i++)
		{
			ResultPtr[i] = ThisPtr[i] + OtherPtr[i];
		}

		return Result;
	}
};

언리얼에서 CSV를 가져오기 위해서 FTableRowBase라는 클래스를 상속한 구조체를 선언해주고 여기에 CSV에 있는 Column들의 이름을 모두 같이 선언해준다.

야무지게 해당 구조체에 대한 덧셈 operator overloading까지 야무지게 구현해준다.

DataTable에 CSV 파일 로드

언리얼 에디터에서 DataTable 생성을 누르고 Row Structure를 앞서 FTableRowBase를 상속해 만든 클래스로 지정한다.

이후, Reimport 버튼을 눌러서 csv 파일을 로드하면 다음과 같이 성공적으로 CSV 파일이 로드된 것을 확인할 수 있다.


DataTable을 관리하는 Custom Singleton 클래스 만들기

DataTable을 관리하는 Singleton 클래스를 만들어보자.

지금까지 다루어보았던 Singleton Class 종류에는, Part 1에서 다루었던 GameInstance class. Part 2에서 다루었던 Asset manager가 있고 그 밖에는 멀티플레이를 포함한 전체 게임내에서 단 하나만 존재하는 GameMode가 존재한다.

Singleton 생성 & 등록

UObject를 상속해서 Singleton C++ 클래스를 하나 만든다.

이후, 아래 경로로 Singleton Class를 프로젝트 Singleton에 등록한다.

Project settings -> General Settings -> default classes -> Advanced -> Game Singleton Class

Singleton Getter 구현

언리얼 Singleton에 등록을 하면 GEngine이라는 전역 변수에 GEngine->GameSingleton으로 바로 접근이 가능하다.

// GameSingleton.cpp
URyanGameSingleton& URyanGameSingleton::Get()
{
	// 설정에서 Singleton Class로 등록을 해주면 GEngine에서 바로 접근이 가능하다. 
	URyanGameSingleton* Singleton = CastChecked< URyanGameSingleton>(GEngine->GameSingleton);
	if (Singleton)
	{
		return *Singleton;
	}

	UE_LOG(LogRyanGameSingleton, Error, TEXT("Invalid Game Singleton"));
	return *NewObject<URyanGameSingleton>();
}

GEngine->GameSingleton으로 Singleton이 있다면 해당 pointer를 넘겨주고 없다면 NewObject로 생성하고 그 pointer를 넘겨준다.


Layer 구조 이해하기

Game Layer : Gimmick과 NPC
Middleware Layer : 캐릭터의 Stat Component, ItemBox
Data Layer : Stat 데이터, 데이터 관리를 위한 singleton class

의존성 분리를 위해 윗 Layer에서 아래로는 참조 O. 아래 Layer에서 위로는 참조 X하게 설계해보자.

캐릭터의 stat은 총 2가지 stat을 합쳐서 결정된다. GameSingleton에 존재하는 캐릭터의 기본 stat과 캐릭터가 먹은 weapon의 stat이 합산돼 최종 stat이 구해진다.

CharacterComponent.cpp에서 BaseStatModifierStat을 더해서 캐릭터의 최종 Stat 값을 결정한다.

우리는 Gimmick를 하나씩 클리어 할때마다 캐릭터의 기본 stat이 하나씩 올라가게 만들 것이다.


Actor의 생성과 지연 생성의 프로세스

지금까지 코드를 잘 따라 했으면 한가지 문제점이 생기는 것을 확인할 수 있을 것이다. Stage가 넘어감에 따라 캐릭터의 MaxHp는 CSV에 적힌 HP 변화량에 따라 업데이트가 되지만 캐릭터의 CurrentHp은 업데이트가 되지 않는다는 사실. 이 이유는 코드 상에서 SpawnActor를 통해서 Actor의 스폰을 확정시킨 상황에서 CurrentHp를 업데이트 했기 때문이다. 우리는 이 문제를 우리가 생성하라는 명령 전에는 Actor를 Spawn 시키지 않는 방법인 SpawnDeferredActor을 통해서 해결할 것이다.

SpawnActor VS SpawnDeferredActor

SpawnActor란?

SpawnActor는 언리얼 엔진에서 새로운 Actor를 생성하고 월드에 추가하는 가장 일반적인 방법이다. 이 방법은 지정된 클래스의 인스턴스를 생성하여 월드의 특정 위치와 회전 값으로 배치한다. 액터를 생성할 때 필요한 모든 초기화 작업이 자동으로 수행되며, 생성된 액터는 월드 내에서 즉시 상호작용할 수 있게 된다. SpawnActor는 게임 플레이 중 동적으로 액터를 생성할 때 주로 사용되며, 블루프린트나 C++ 코드에서 호출할 수 있습니다.

SpawnDeferredActor란?

SpawnDeferred는 언리얼 엔진에서 Actor를 지연 생성하는 방법이다. 일반적인 액터 생성은 SpawnActor 함수를 사용하지만, SpawnDeferred는 생성 과정을 두 단계로 나누어 초기화 전에 필요한 설정을 할 수 있도록 한다. 먼저 UWorld::SpawnActorDeferred 함수를 호출하여 액터의 기본 객체를 생성하고, 이후 FinishSpawning 함수를 사용하여 액터를 완전히 생성한다. 이를 통해 초기화가 완료되기 전에 액터의 속성이나 컴포넌트를 설정할 수 있어 복잡한 초기화 작업을 보다 유연하게 처리할 수 있다.

SpawnDeferred는 특히 액터의 생성 중 특정 설정이나 조건을 적용해야 할 때 유용하다. 예를 들어, 스폰된 액터가 특정 위치나 회전 값, 또는 고유한 매개변수를 필요로 할 경우 이 방법을 사용하면 생성 후 수정할 필요 없이 초기화 전에 필요한 모든 설정을 적용할 수 있다. 이는 퍼포먼스 최적화와 코드의 가독성을 높이는 데 도움이 된다.

SpawnDeferredActor의 사용법

// StateGimmick.cpp
// NPC Spawn
void ARyanStageGimmick::OnOpponentSpawn()
{
	const FTransform SpawnTransform(GetActorLocation() + FVector::UpVector * 88.0f);
	ARyanNPCCharacter* RyanOpponentCharacter = GetWorld()->SpawnActorDeferred<ARyanNPCCharacter>(OpponentClass, SpawnTransform);
	if (RyanOpponentCharacter)
	{
		RyanOpponentCharacter->OnDestroyed.AddDynamic(this, &ARyanStageGimmick::OnOpponentDestroyed);
		RyanOpponentCharacter->SetLevel(CurrentStageNum);
        // SetLevel까지하고 NPC의 캐릭터 Spawn을 확정한다.
		RyanOpponentCharacter->FinishSpawning(SpawnTransform);
	}
}

생성되는 NPC에 서로 다른 Mesh 입히기

ini 파일 형태

[/Script/ArenaBattle.RyanNPCCharacter]
+NPCMeshes=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Barbarous.SK_CharM_Barbarous 
+NPCMeshes=/Game/InfinityBladeWarriors/Character/CompleteCharacters/sk_CharM_Base.sk_CharM_Base 
+NPCMeshes=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Bladed.SK_CharM_Bladed
...

우리는 Spawn되는 NPC들마다 새로운 Mesh를 입히기 위해 다양한 mesh정보들을 ini 파일에 정리해두고 이를 적극 활용할 것이다. 위는 NPC 캐릭터의 Mesh들이 들어있는 ini 파일.

[/Script/ArenaBattle.RyanNPCCharacter]

위 파일의 첫 문장을 이해해보자. 첫번째로 Script는 C++를 쓴다는 말이다. 다음으로, ArenaBattle는 project명이고 마지막으로 RyanNPCCharacter는 이 파일에서 ini를 참조하겠다는 의미.

밑의 자료를 해석해보면, +는 추가를 의미하고 NPCMeshes는 TArray 배열을 의미한다. 즉, NPCMeshes라는 배열에 Mesh 경로들을 다 집어넣겠다는 의미이다. 총 정리를 해보자면, RyanNPCCharacter라는 파일에서 NPCMeshes라는 TArray 변수가 선언되어 있으면, 이 값을 ini 파일을 로드함으로서 사용할 수 있다.

ini 파일 읽는 방법

#pragma once

#include "CoreMinimal.h"
#include "Character/RyanCharacterBase.h"
#include "Engine/StreamableManager.h"
#include "RyanNPCCharacter.generated.h"

UCLASS(config = ArenaBattle)
class ARENABATTLE_API ARyanNPCCharacter : public ARyanCharacterBase
{
	...

protected:

	// ini 파일 불러들이는 방법
	UPROPERTY(config)
	TArray<FSoftObjectPath> NPCMeshes;

	// 비동기 방식으로 불러들이기
	TSharedPtr<FStreamableHandle> NPCMeshHandle;
    ...
}

ini 파일을 읽는 방법은 먼저 header의 UCLASS metadata안에 config를 지정해주고, TArray의 형태를 띄어야 하는 변수 NPCMeshes를 config가 들어있는 UPROPERTY로 선언해준다. 마지막으로 이 자료를 비동기 방식으로 부르는 TSharedPtr 변수를 하나 선언해준다.


최종화면

Stage마다 NPC에게 랜덤한 Mesh가 적용되는 모습

새로운 Stage에 진입할때마다 DataTable에 명시된 Stat로 매번 성공적으로 업데이트

profile
🏦KAIST EE | 🏦SNU AI(빅데이터 핀테크 전문가 과정) | 📙CryptoHipsters 저자

0개의 댓글