KoreanFoodie's Study

언리얼 시리얼라이제이션 (Unreal Serialization) 본문

Game Dev/Unreal C++ : Study

언리얼 시리얼라이제이션 (Unreal Serialization)

GoldGiver 2022. 4. 1. 13:12

시리얼라이제이션 

시리얼라이제이션이라는 개념은, 텔레포트와 같다고 이해하면 쉽다. 텔레포트로 사람을 전송한다고 했을 때, 정상적으로 작동하는 텔레포터는 사람 전체를 온전히 주고받는 형태일 것이다. 즉, 절반만 보낸다거나, 나눠서 보내지 않는다는 뜻이다. (나눠서 보내면... 그건 텔레포터가 아니라 살상무기가 아닐까)

 

시리얼라이제이션은 언리얼 오브젝트를 한번에 안전하게 전달하기 위해 FArchive 클래스를 만들어 파일, 메모리 등등 데이터를 전송하는 모든 매체를 관리하고 있다. 실제로 데이터를 전송하는 모든 매체는 FArchive 클래스를 상속받아서 구현한다.

FArchive 객체를 이용해 파일을 만들고 쓰기/읽기를 구현한 예제 코드를 보자.

FString FullPath = FString::Printf(TEXT("%s%s"), *FPaths::GameSavedDir(), TEXT("BossName.txt"));
FArchive* ArWriter = IFileManager::Get().CreateFileWriter(*Fullpath);

if (ArWriter)
{
    // Fstring 타입의 변수 Name 을 갖고 있는 Boss 객체가 있다고 가정
    *ArWriter << Boss->Name; 
    ArWriter->Close();
    delete ArWriter;
    ArWriter = NULL;
}

TSharedPtr<FArchive> FileReader = MakeShareable(IFileManager::Get().CreateFileReader(*FullPath));
if (FileReader.IsValid())
{
    FString Name;
    *FileReader.Get() << Name;
    FileReader.Close();
    UE_LOG(MyGameInstance, Warning, TEXT("Boss name is %s"), *Name);
}

GameSavedDir 디렉토리는 Saved 디렉토리를 의미한다.

 

C++ 에서는 입출력을 >> , << 로 구분해 사용하지만, 언리얼에서는 << 연산자만 사용한다. 대신 아카이브의 처리시 현 상태에 따라 사용자가 분기해 처리하도록 설계되어 있다.

 

위에서는 Boss 의 Name 값을 직접 지정했는데, 사실 시리얼라이제이션의 장점은 객체의 정보를 한꺼번에 저장하고 보낼 수 있다는 데에 있다. 이를 활용하기 위해 코드를 약간 고쳐보자.

FString FullPath = FString::Printf(TEXT("%s%s"), *FPaths::GameSavedDir(), TEXT("BossName.txt"));
FArchive* ArWriter = IFileManager::Get().CreateFileWriter(*Fullpath);

if (ArWriter)
{
    // class UBoss* MyBoss; Name 을 가지고 있음
    *ArWriter << *MyBoss; 
    ArWriter->Close();
    delete ArWriter;
    ArWriter = NULL;
}

TSharedPtr<FArchive> FileReader = MakeShareable(IFileManager::Get().CreateFileReader(*FullPath));
if (FileReader.IsValid())
{
    UBoss* MyBossFromFile = NewObject<UBoss>(this);
    *FileReader.Get() << *MyBossFromFile;
    FileReader.Close();
    UE_LOG(MyGameInstance, Warning, TEXT("Boss name is %s"), *MyBossFromFile->Name);
}

이제 ArWriter 는 UBoss 타입의 언리얼 오브젝트를 통째로 주고 받게 된다. 이제 이걸 활용하려면 UBoss 에 다음과 같은 입출력 함수를 만들어 주면 된다.

 

Boss.h

friend FArchive& operator<<(FArchive& Ar, UBoss& MB)
{
    if (Ar.IsLoading())
    {
        UE_LOG(LogTemp, Warning, TEXT("Archive is Loading State"));
    }
    else if(Ar.IsSaving())
    {
        UE_LOG(LogTemp, Warning, TEXT("Archive is Saving State"));
    }
    else
    {
        return Ar;
    }
    return Ar << MB.Name;
}

IsLoading 과 IsSaving 함수를 사용하면 현재 아카이브의 상태가 저장 상태인지, 로딩 상태인지를 파악할 수 있다. 

 

 

패키징 클래스

위의 예시처럼 언리얼 오브젝트마다 연산자 오버로딩을 구현하는 것은 매우 불편할 설계이다. 대신 패키징 클래스를 이용해 보자.

패키징 클래스는 저장할 언리얼 오브젝트가 잘 저장되도록 포장하는 역할을 하는 클래스인데, 언리얼 오브젝트에 속한 계층 구조에 있는 모든 오브젝트를 저장할 수 있다.

 

이제 패키징 클래스를 이용해 Boss 언리얼 오브젝트를 한번에 저장해 보자. 

 

MyGameInstance.cpp

void UMyGameInstance::Init()
{
    ...
    
    FString PackageName = TEXT("/Temp/SavedBoss");
    UPackage* NewPackage = CreatePackage(nullptr, *PackageName);
    BossNew = NewObject<UBoss>(NewPackage);
    FString PackageFileName = FPackageName::LongPackageNameToFilename(PackageName, FPackageName::GetAssetPackageExtension());

    BossNew->Name = TEXT("King");

    if (UPackage::SavePackage(NewPackage, BossNew, RF_Standalone, *PackageFileName))
    {
        UPackage* SavedPackage = ::LoadPackage(NULL, *PackageFileName, LOAD_None);
        TArray<UObject *> ObjectsInPackage;
        GetObjectsWithOuter(SavedPackage, ObjectsInPackage, false);

        for (const auto& EachObject : ObjectsInPackage)
        {
            UBoss* BossFromFile = Cast<UBoss>(EachObject);
            if (BossFromFile)
            {
                AB_LOG(Warning, TEXT("Boss From File : Name %s"), *BossFromFile->Name);
            }
        }
    }
}

위에서 패키징의 이름의 루트 폴더를 /Temp 로 지정했는데, 이 패키지 이름을 변환한 실제 폴더의 위치는 현재 프로젝트의 Saved 폴더가 된다. SavePackage 함수를 사용해 저장한 후에는 바로 LoadPackage 함수를 사용해 언리얼 오브젝트를 불러들였다.

컴파일을 하면, Saved 폴더 아래에 "SavedBoss.uasset" 파일이 생성된다!

Comments