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
생성을 누르고 Row Structure를 앞서 FTableRowBase
를 상속해 만든 클래스로 지정한다.
이후, Reimport
버튼을 눌러서 csv 파일을 로드하면 다음과 같이 성공적으로 CSV 파일이 로드된 것을 확인할 수 있다.
DataTable을 관리하는 Singleton 클래스를 만들어보자.
지금까지 다루어보았던 Singleton Class 종류에는, Part 1에서 다루었던 GameInstance class. Part 2에서 다루었던 Asset manager가 있고 그 밖에는 멀티플레이를 포함한 전체 게임내에서 단 하나만 존재하는 GameMode가 존재한다.
UObject
를 상속해서 Singleton C++ 클래스를 하나 만든다.
이후, 아래 경로로 Singleton Class를 프로젝트 Singleton에 등록한다.
Project settings
-> General Settings
-> default classes
-> Advanced
-> Game Singleton Class
언리얼 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를 넘겨준다.
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
에서 BaseStat
과 ModifierStat
을 더해서 캐릭터의 최종 Stat 값을 결정한다.
우리는 Gimmick를 하나씩 클리어 할때마다 캐릭터의 기본 stat이 하나씩 올라가게 만들 것이다.
지금까지 코드를 잘 따라 했으면 한가지 문제점이 생기는 것을 확인할 수 있을 것이다. Stage가 넘어감에 따라 캐릭터의 MaxHp는 CSV에 적힌 HP 변화량에 따라 업데이트가 되지만 캐릭터의 CurrentHp은 업데이트가 되지 않는다는 사실. 이 이유는 코드 상에서 SpawnActor
를 통해서 Actor의 스폰을 확정시킨 상황에서 CurrentHp를 업데이트 했기 때문이다. 우리는 이 문제를 우리가 생성하라는 명령 전에는 Actor를 Spawn 시키지 않는 방법인 SpawnDeferredActor
을 통해서 해결할 것이다.
SpawnActor
는 언리얼 엔진에서 새로운 Actor를 생성하고 월드에 추가하는 가장 일반적인 방법이다. 이 방법은 지정된 클래스의 인스턴스를 생성하여 월드의 특정 위치와 회전 값으로 배치한다. 액터를 생성할 때 필요한 모든 초기화 작업이 자동으로 수행되며, 생성된 액터는 월드 내에서 즉시 상호작용할 수 있게 된다. SpawnActor
는 게임 플레이 중 동적으로 액터를 생성할 때 주로 사용되며, 블루프린트나 C++ 코드에서 호출할 수 있습니다.
SpawnDeferred
는 언리얼 엔진에서 Actor를 지연 생성하는 방법이다. 일반적인 액터 생성은 SpawnActor
함수를 사용하지만, SpawnDeferred
는 생성 과정을 두 단계로 나누어 초기화 전에 필요한 설정을 할 수 있도록 한다. 먼저 UWorld::SpawnActorDeferred
함수를 호출하여 액터의 기본 객체를 생성하고, 이후 FinishSpawning
함수를 사용하여 액터를 완전히 생성한다. 이를 통해 초기화가 완료되기 전에 액터의 속성이나 컴포넌트를 설정할 수 있어 복잡한 초기화 작업을 보다 유연하게 처리할 수 있다.
SpawnDeferred
는 특히 액터의 생성 중 특정 설정이나 조건을 적용해야 할 때 유용하다. 예를 들어, 스폰된 액터가 특정 위치나 회전 값, 또는 고유한 매개변수를 필요로 할 경우 이 방법을 사용하면 생성 후 수정할 필요 없이 초기화 전에 필요한 모든 설정을 적용할 수 있다. 이는 퍼포먼스 최적화와 코드의 가독성을 높이는 데 도움이 된다.
// 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);
}
}
[/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 파일을 로드함으로서 사용할 수 있다.
#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 변수를 하나 선언해준다.