스토리텔링 개발자

[UE4] 직렬화(Serialization) 본문

개발/언리얼 엔진

[UE4] 직렬화(Serialization)

김디트 2021. 6. 8. 17:35
728x90

직렬화

프로그램은 메모리에 올라가게 됩니다.

그러니 프로그램 내의 객체들도 메모리에 올라가 있을 것입니다.

 

그렇다면 그 객체들은 메모리에 어떤 형태로 올라가 있을까요?

.

.

.

답은 알 수 없다 입니다.

 

컴퓨터가 메모리를 관리하면서 배정하므로 하나의 객체라도 연속적으로 모여있다고 단정지을 순 없습니다.

즉, 운 좋게 한 곳에 모두 모여 있는 경우도 있을테지만, 반대로 여기 저기 심하게 파편화 되어 있을 수도 있다는 것이죠.

 

이 객체의 데이터를 전송 / 저장하려 한다고 가정해 보죠.

그러기 위해선 우선 메모리의 어디에 어떻게 위치할지 모를 객체의 데이터들을 모아야 할 것입니다.

 

사실 직렬화란 그것이 전부입니다.

데이터를 모으고 전송하기 위해 패키징하는 작업을 뜻합니다.

현재 객체의 상태를 온전히 동일한 형태로 전송, 저장하기 위한 프로세스입니다.

 

좀 더 고급스럽게 말하면, 객체를 바이트화 하여 메모리, 데이터베이스, 파일 등에 저장하거나 전송하는 프로세스입니다.

 

물론 그렇게 묶어둔 패키지는 풀어야만 재사용이 가능할테죠.

그 반대 프로세스는 "역 직렬화"라고합니다.

 

 

 

C++ 직렬화

C++는 언어 차원에서 직렬화를 지원하지 않습니다.

 

그러므로 iostream / fstream 등의 라이브러리를 통해서 처리합니다.

 

이 경우 << 연산자를 입력, >> 연산자를 출력으로 사용할 수 있으며,

커스텀 객체 직렬화를 할 땐 각 연산자를 오버라이딩(override) 하여 구현해야 합니다.

 

아래는 예제입니다.

#include <iostream>
#include <fstream>
using namespace std;

class Student {
public:
    char FullName[40];
    char CompleteAddress[120];
    char Gender;
    double Age;
    bool LivesInASingleParentHome;

    // 연산자 오버로딩: 객체를 스트림에 직렬화
    friend ostream& operator<<(ostream& os, const Student& obj) {
        os.write(obj.FullName, sizeof(obj.FullName));
        os.write(obj.CompleteAddress, sizeof(obj.CompleteAddress));
        os.write((char*)&obj.Gender, sizeof(obj.Gender));
        os.write((char*)&obj.Age, sizeof(obj.Age));
        os.write((char*)&obj.LivesInASingleParentHome, sizeof(obj.LivesInASingleParentHome));
        return os;
    }

    // 연산자 오버로딩: 스트림에서 객체 역직렬화
    friend istream& operator>>(istream& is, Student& obj) {
        is.read(obj.FullName, sizeof(obj.FullName));
        is.read(obj.CompleteAddress, sizeof(obj.CompleteAddress));
        is.read((char*)&obj.Gender, sizeof(obj.Gender));
        is.read((char*)&obj.Age, sizeof(obj.Age));
        is.read((char*)&obj.LivesInASingleParentHome, sizeof(obj.LivesInASingleParentHome));
        return is;
    }
};

int main() {
    // 객체 생성
    Student one;
    strcpy(one.FullName, "Ernestine Waller");
    strcpy(one.CompleteAddress, "824 Larson Drv, Silver Spring, MD 20910");
    one.Gender = 'F';
    one.Age = 16.50;
    one.LivesInASingleParentHome = true;

    // 파일 스트림 열고 객체를 스트림에 쓰기
    ofstream ofs("fifthgrade.ros", ios::binary);
    ofs << one;
    ofs.close();

    // 파일 스트림 열고 객체를 스트림에서 읽기
    ifstream ifs("fifthgrade.ros", ios::binary);
    Student two;
    ifs >> two;
    ifs.close();

    // 읽은 객체 정보 출력
    cout << "Student Information" << endl;
    cout << "Student Name: " << two.FullName << endl;
    cout << "Address: " << two.CompleteAddress << endl;
    if (two.Gender == 'F' || two.Gender == 'f')
        cout << "Gender: Female" << endl;
    else if (two.Gender == 'M' || two.Gender == 'm')
        cout << "Gender: Male" << endl;
    else
        cout << "Gender: Unknown" << endl;
    cout << "Age: " << two.Age << endl;
    if (two.LivesInASingleParentHome)
        cout << "Lives in a single parent home" << endl;
    else
        cout << "Doesn't live in a single parent home" << endl;

    return 0;
}

 

 

 

 

UE4 직렬화

UE4에서는 FArchive 객체를 통해 입출력 스트림을 제어합니다.

C++의 stream 클래스와 비슷한 느낌으로 사용할 수 있습니다.

 

특이 사항으로는, C++의 stream은 입력, 출력 연산자(<<, >>)를 구분해서 사용하는 데 반해,

FArchive는 입출력 모두에 << 연산자만을 사용합니다.

FArchive의 타입에 따라 직렬화, 역직렬화 동작을 하게 됩니다.

 

모든 객체는 FArchive를 통해 스트림화 하여 메모리, 데이터베이스, 파일 등에 사용됩니다.

 

 

 

UObject의 직렬화

UObject의 직렬화는 void Serialize( FArchive& Ar ) 인터페이스를 사용해서 구현합니다.

입력 / 출력의 연산자가 <<로 동일하므로 Serialize 인터페이스의 구현 내용을 입력용, 출력용 구분하여 구현하지는 않습니다.

즉, Serialize() 함수 하나로 입력 / 출력을 모두 처리합니다.

 

Serialize 동작이 입력인지 출력인지는 매개변수로 넘어오는 FArchive 객체의 상태에 따릅니다.

 

이를테면,

FileManager::CreateFileWriter 로 생성된 FArchive는 출력 스트림입니다.

FileManager::CreateFileReader 로 생성된 FArchive는 입력 스트림입니다.

 

아래는 파일 입출력 직렬화, 역직렬화의 예제입니다.

#include "MyCustomSaveGame.h"
#include "Serialization/Archive.h"

// MyCustomSaveGame.h
UCLASS()
class MYPROJECT_API UMyCustomSaveGame : public USaveGame
{
    GENERATED_BODY()

public:
    UPROPERTY()
    int32 MyIntValue;

    UPROPERTY()
    FString MyStringValue;
};

// MyCustomSaveGame.cpp
void SaveGameToFile(const FString& SaveSlotName, UMyCustomSaveGame* SaveGameInstance)
{
    if (SaveGameInstance)
    {
        // 저장을 위한 Archive
        FBufferArchive BufferArchive;
        
        // 직렬화
        BufferArchive << SaveGameInstance;

        // 파일 저장
        FString SavePath = FPaths::ProjectSavedDir() + TEXT("SaveGames/") + SaveSlotName + TEXT(".sav");
        FFileHelper::SaveArrayToFile(BufferArchive, *SavePath);
    }
}

UMyCustomSaveGame* LoadGameFromFile(const FString& SaveSlotName)
{
    UMyCustomSaveGame* LoadedSaveGame = nullptr;

    // 파일 불러오기
    FString SavePath = FPaths::ProjectSavedDir() + TEXT("SaveGames/") + SaveSlotName + TEXT(".sav");
    TArray<uint8> FileData;
    if (FFileHelper::LoadFileToArray(FileData, *SavePath))
    {
        // 불러오기를 위한 Archive
        FMemoryReader MemoryReader(FileData);
        
        // 역직렬화
        LoadedSaveGame = NewObject<UMyCustomSaveGame>();
        MemoryReader << LoadedSaveGame;
    }

    return LoadedSaveGame;
}

 

 

 

 

UPackage -> uasset

언리얼은 모든 리소스를 uasset 혹은 umap으로 관리합니다.

 

레벨은 umap으로, 레벨을 제외한 리소스는 uasset으로 관리합니다.

 

UPackage는 직렬화 과정(오브젝트 순회 및 스트림 생성, 그리고 결과적으로 파일(uasset / umap)으로 변환 등)을 위한 다양하고 편리한 함수들을 포함하고 있습니다.

 

아래의 과정 각각에서도 UPackage가 사용됩니다.

 

레벨 저장 시나리오

 

1. 레벨은 트리 형태로 구성된 하위 오브젝트들을 순회하며 각각 직렬화.

2. 직렬화로최종적인 FArchive 스트림을 생성

3. FArchive 스트림을 umap으로 저장

 

 

728x90
Comments