GameStudy 2023. 5. 13. 12:38

13.1 언리얼 엔진의 직렬화

13.1-1 직렬화란

  - 오브젝트나 연결된 오브젝트의 묶음(오브젝트 그래프)을 바이트 스트림으로 변환하는 과정

    복잡한 데이터를 일렬로 세우기 때문에 직렬화라고 부름.

  - 거꾸로 복구시키는 과정도 포함해서 의미함.

    시리얼라이제이션: 오브젝트 그래프에서 바이트 스트림으로

    디시리얼라이제이션: 바이트 스트림에서 오브젝트 그래프로

  - 직렬화가 가지는 장점

    현재 프로그램의 상태를 저장하고 필요할 때 복원할 수 있음.(게임의 저장)

    현재 개체의 정보를 클립보드에 복사해서 다른 프로그램에 전송할 수 있음.

    네트워크를 통해 현재 프로그램의 상태를 다른 컴퓨터에 복원할 수 있음.(멀티플레이)

    데이터 압축, 암호화를 통해 데이터를 효율적이고 안전하게 보관할 수 있음.

 

13.1-2 직렬화 구현시 고려할 점

  - 데이터 레이아웃: 오브젝트가 소유한 다양한 데이터를 어떻게 변환할 것인가

  - 이식성: 서로 다른 시스템에 전송해도 이식될 수 있게끔 할것인가(.ex 리틀엔디안/빅엔디안)

  - 버전 관리: 새로운 기능이 추가될 때 이를 어떻게 확장하고 처리할 것인가(기존의 직렬화가 무효화됨)

  - 성능: 네트워크 비용을 줄이기 위해 어떤 데이터 형식을 사용할 것인가(회전을 모두 다 보낼 필욘 없음. 양자화)

  - 보안: 데이터를 어떻게 안전하게 보호할 것인가

  - 에러 처리: 전송 과정에서 문제가 발생할 경우 이를 어떻게 인식하고 처리할 것인

 

13.1-3 언리얼 엔진의 직렬화 시스템

  - 직렬화 시스템을 위해 언리얼에서 제공하는 클래스 FArchive와 << 연산자

  - 다양한 아카이브 클래스의 제공

    메모리 아카이브(FMemoryReader, FMemoryWriter)

    파일 아카이브(FArchiveFileReaderGeneric, FArchiveFileWriterGeneric)

    기타 언리얼 오브젝트와 관련된 아카이브 클래스(FArchiveUObject)

    Json 직렬화 기능도 별도의 라이브러리로 제공하고 있음.

 

13.1-4 직렬화 시스템 실습

  - 새 프로젝트 > "UnrealSerialization" 생성

  - 새 C++ 클래스 > GameInstance 부모 클래스 > "MyGameInstance"

    새 C++ 클래스 > Object 부모 클래스 > "Student"

<hide/>

// Student.h

#pragma once

#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "Student.generated.h"

UCLASS()
class UNREALSERIALIZATION_API UStudent : public UObject
{
	GENERATED_BODY()
	
public:
	UStudent();

	int32 GetOrder() const { return Order; }
	void SetOrder(int32 InOrder) { Order = InOrder; }

	const FString& GetName() const { return Name; }
	void SetName(const FString& InName) { Name = InName; }

	virtual void Serialize(FArchive& Ar) override;

private:
	UPROPERTY()
	int32 Order;

	UPROPERTY()
	FString Name;
};
<hide/>

// Student.cpp

#include "Student.h"

UStudent::UStudent()
{
	Order = -1;
	Name = TEXT("홍길동");
}

void UStudent::Serialize(FArchive& Ar)
{
	Super::Serialize(Ar);

	Ar << Order;
	Ar << Name;
}
<hide/>

// MyGameInstance.h

#pragma once

#include "CoreMinimal.h"
#include "Engine/GameInstance.h"
#include "Engine/StreamableManager.h"
#include "MyGameInstance.generated.h"

struct FStudentData
{
	FStudentData() {}
	FStudentData(int32 InOrder, const FString& InName) : Order(InOrder), Name(InName) {}

	friend FArchive& operator<<(FArchive& Ar, FStudentData& InStudentData)
	{
		Ar << InStudentData.Order;
		Ar << InStudentData.Name;
		return Ar;
	}

	int32 Order = -1;
	FString Name = TEXT("홍길동");
};

UCLASS()
class UNREALSERIALIZATION_API UMyGameInstance : public UGameInstance
{
	GENERATED_BODY()
	
public:
	UMyGameInstance();

	virtual void Init() override;
    
private:
	UPROPERTY()
	TObjectPtr<class UStudent> StudentSrc;

	FStreamableManager StreamableManager;
	TSharedPtr<FStreamableHandle> Handle;
};
<hide/>

// MyGameInstance.cpp

#include "MyGameInstance.h"
#include "Student.h"
#include "JsonObjectConverter.h"
#include "UObject/SavePackage.h"

void PrintStudentInfo(const UStudent* InStudent, const FString& InTag)
{
	UE_LOG(LogTemp, Log, TEXT("[%s] 이름 %s 순번 %d"), *InTag, *InStudent->GetName(), InStudent->GetOrder());
}

UMyGameInstance::UMyGameInstance()
{
}

void UMyGameInstance::Init()
{
	Super::Init();

	FStudentData RawDataSrc(16, TEXT("이득우"));

	const FString SavedDir = FPaths::Combine(FPlatformMisc::ProjectDir(), TEXT("Saved"));
	UE_LOG(LogTemp, Log, TEXT("저장할 파일 폴더 : %s"), *SavedDir);

	{
		const FString RawDataFileName(TEXT("RawData.bin"));
		FString RawDataAbosolutePath = FPaths::Combine(*SavedDir, *RawDataFileName);
		UE_LOG(LogTemp, Log, TEXT("저장할 파일 전체 경로 : %s"), *RawDataAbosolutePath);
		FPaths::MakeStandardFilename(RawDataAbosolutePath);
		UE_LOG(LogTemp, Log, TEXT("변경할 파일 전체 경로 : %s"), *RawDataAbosolutePath);

		FArchive* RawFileWriterAr = IFileManager::Get().CreateFileWriter(*RawDataAbosolutePath);
		if (nullptr != RawFileWriterAr)
		{
			*RawFileWriterAr << RawDataSrc;
			RawFileWriterAr->Close();
			delete RawFileWriterAr;
			RawFileWriterAr = nullptr;
		}

		FStudentData RawDataDest;
		FArchive* RawFileReaderAr = IFileManager::Get().CreateFileReader(*RawDataAbosolutePath);
		if (nullptr != RawFileReaderAr)
		{
			*RawFileReaderAr << RawDataDest;
			RawFileReaderAr->Close();
			delete RawFileReaderAr;
			RawFileReaderAr = nullptr;

			UE_LOG(LogTemp, Log, TEXT("[RawData] 이름 %s 순번 %d"), *RawDataDest.Name, RawDataDest.Order);
		}
	}

	StudentSrc = NewObject<UStudent>();
	StudentSrc->SetName(TEXT("이득우"));
	StudentSrc->SetOrder(59);

	{
		const FString ObjectDataFileName(TEXT("ObjectData.bin"));
		FString ObjectDataAbsolutePath = FPaths::Combine(*SavedDir, *ObjectDataFileName);
		FPaths::MakeStandardFilename(ObjectDataAbsolutePath);

		TArray<uint8> BufferArray;
		FMemoryWriter MemoryWriterAr(BufferArray);
		StudentSrc->Serialize(MemoryWriterAr);
		
		if (TUniquePtr<FArchive> FileWriterAr = TUniquePtr<FArchive>(IFileManager::Get().CreateFileWriter(*ObjectDataAbsolutePath)))
		{
			*FileWriterAr << BufferArray;
			FileWriterAr->Close();
		}

		TArray<uint8> BufferArrayFromFile;
		if (TUniquePtr<FArchive> FileReaderAr = TUniquePtr<FArchive>(IFileManager::Get().CreateFileReader(*ObjectDataAbsolutePath)))
		{
			*FileReaderAr << BufferArrayFromFile;
			FileReaderAr->Close();
		}

		FMemoryReader MemoryReaderAr(BufferArrayFromFile);
		UStudent* StudentDest = NewObject<UStudent>();
		StudentDest->Serialize(MemoryReaderAr);
		PrintStudentInfo(StudentDest, TEXT("ObjectData"));
	}

}

 

13.1-5 Json 직렬화

  - 웹 환경에서 서버와 클라이언트 사이에 데이터를 주고 받을 때 사용하는 텍스트 기반 데이터 포맷

  - Json 장점

    텍스트임에도 데이터 크기가 가벼움.

    읽기 편해서 데이터를 보고 이해할 수 있음.

    사실상 웹 통신의 표준으로 널리 사용됨.

  - Json 단점

    지원하는 타입이 몇 가지 안됨.(문자, 숫자, 불리언, 널, 배열, 오브젝트만 가능)

    텍스트 형식으로만 사용할 수 있어서 극도의 효율 추구는 불가능.

  - 언리얼 엔진의 Json, JsonUtilities 라이브러리 활용

 

13.1-6 Json 데이터 유형

  - 오브젝트 {}

    오브젝트 내 데이터는 키-벨류 조합으로 구성됨. ex) { "key" : 10 }

  - 배열 []

    배열 내 데이터는 벨류로만 구성됨. ex) [ "value1", "value2", "value3" ]

  - 이외 데이터

    문자열("string"), 숫자(10, 3.141592), 불리언(true, false), 널(null)로 구성.

 

13.1-7 언리얼 스마트 포인터 라이브러리

  - TUniquePtr: 지정된 곳에서만 메모리를 관리하는 포인터

    특정 오브젝트에게 명확하게 포인터 해지 권한을 주고 싶은 경우에 사용.

    delete 구문 없이 함수 실행 후 자동으로 소멸시키고 싶을때 사용함.

  - TSharedPtr: 더이상 사용되지 않으면 자동으로 메모리를 해지하는 포인터

    여러 로직에서 할당된 오브젝트가 공유해서 사용되는 경우.

    다른 함수로부터 할당된 오브젝트를 Out으로 받는 경우.

    nullptr일 수도 있기에 주의.

  - TSharedRef: 공유 포인터와 동일하지만, 유효한 개체를 항상 보장받는 레퍼런스

    여러 로직에서 할당된 오브젝트가 공유해서 사용되는 경우.

    Not null을 보장받으며, 오브젝트를 편리하게 사용하고 싶은 경우.

 

 

13.1-8 Json 직렬화 실습

<hide/>

// MyGameInstance.cpp

#include "MyGameInstance.h"
#include "Student.h"
#include "JsonObjectConverter.h"
#include "UObject/SavePackage.h"

...

void UMyGameInstance::Init()
{
	...

	{
		const FString JsonDataFileName(TEXT("StudentJsonData.txt"));
		FString JsonDataAbsolutePath = FPaths::Combine(*SavedDir, *JsonDataFileName);
		FPaths::MakeStandardFilename(JsonDataAbsolutePath);

		TSharedRef<FJsonObject> JsonObjectSrc = MakeShared<FJsonObject>();
		FJsonObjectConverter::UStructToJsonObject(StudentSrc->GetClass(), StudentSrc, JsonObjectSrc);

		FString JsonOutString;
		TSharedRef<TJsonWriter<TCHAR>> JsonWriterAr = TJsonWriterFactory<TCHAR>::Create(&JsonOutString);
		if (FJsonSerializer::Serialize(JsonObjectSrc, JsonWriterAr))
		{
			FFileHelper::SaveStringToFile(JsonOutString, *JsonDataAbsolutePath);
		}

		FString JsonInString;
		FFileHelper::LoadFileToString(JsonInString, *JsonDataAbsolutePath);

		TSharedRef<TJsonReader<TCHAR>> JsonReaderAr = TJsonReaderFactory<TCHAR>::Create(JsonInString);

		TSharedPtr<FJsonObject> JsonObjectDest;
		if (FJsonSerializer::Deserialize(JsonReaderAr, JsonObjectDest))
		{
			UStudent* JsonStudentDest = NewObject<UStudent>();
			if (FJsonObjectConverter::JsonObjectToUStruct(JsonObjectDest.ToSharedRef(), JsonStudentDest->GetClass(), JsonStudentDest))
			{
				PrintStudentInfo(JsonStudentDest, TEXT("JsonData"));
			}
		}
	}

}