다양한 기록

[UE] Serialization (직렬화) 본문

언리얼 엔진/Unreal C++

[UE] Serialization (직렬화)

라구넹 2024. 12. 30. 18:04

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

- 복잡한 데이터를 일렬로  -> 직렬

Serialization : 오브젝트 그래프 -> 바이트 스트림

Deserialization : 바이트 스트림 -> 오브젝트 그래프

 

장점

- 현재 프로그램 상태를 저장 및 복원 가능 (게임 저장)

- 현재 객체 정보를 클립보드에 복사 -> 다른 프로그램에 전송 가능

- 네트워크를 통해 현재 프로그램의 상태를 다른 컴퓨터에 복원 가능 (멀티게임)

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

 

직접 구현?

- 데이터 레이아웃 (오브젝트가 소유한 다양한 데이터를 변환?)

- 이식성 (서로 다른 시스템 간)

- 버전 관리 

- 성능

- 보안

- 에러 처리

쉽지 않음

그냥 언리얼 엔진이 제공하는 거 쓰면 됨

 

직렬화 시스템

- FArchive

- << 연산자

 

다양한 아카이브 클래스

- FMemoryReader, FMemoryWriter

- FArchiveFileReaderGeneric, FArchiveFileWriterGeneric

- FArchiveUObject

 

Json 직렬화 기능


구조체 직렬화

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Engine/GameInstance.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:
	virtual void Init() override;
};

그냥 Ar << Student.Order 이런 식으로 넣을 수도 있는데,

그냥 << 새로 정의해두는게 편함

 

// Fill out your copyright notice in the Description page of Project Settings.


#include "MyGameInstance.h"
#include "Student.h"

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 RawDataAbsolutePath = FPaths::Combine(*SavedDir, *RawDataFileName);
		UE_LOG(LogTemp, Log, TEXT("저장할 파일 전체 경로 %s"), *RawDataAbsolutePath);
		
		FPaths::MakeStandardFilename(RawDataAbsolutePath);
		UE_LOG(LogTemp, Log, TEXT("표준화된 파일 전체 경로 %s"), *RawDataAbsolutePath);

		FArchive* RawDataFileWriterAr = IFileManager::Get().CreateFileWriter(*RawDataAbsolutePath);
		if ( nullptr != RawDataFileWriterAr)
		{
			*RawDataFileWriterAr << RawDataSrc;
			RawDataFileWriterAr->Close();
			delete RawDataFileWriterAr;
			RawDataFileWriterAr = nullptr;
		}
		FStudentData RawDataDst;
		FArchive* RawDataFileReaderAr = IFileManager::Get().CreateFileReader(*RawDataAbsolutePath);
		if ( nullptr != RawDataFileReaderAr)
		{
			*RawDataFileReaderAr << RawDataDst;
			RawDataFileReaderAr->Close();
			delete RawDataFileReaderAr;
			RawDataFileReaderAr = nullptr;

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

}

ProjectDir() : 프로젝트 경로

FPaths::Combine(~, ~) 경로 합치기 .. Saved라는 폴더랑 합침

FPaths::MakeStandardFileName(~) : 경로 표준화하기

파일 열고 쓰기는 파일 라이터 아카이브랑 파일 리더 아카이브로 이루어짐

 


UObject 직렬

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

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

/**
 * 
 */
UCLASS()
class UNREALSERIALIZATION_API UMyGameInstance : public UGameInstance
{
	GENERATED_BODY()
	
public:
	virtual void Init() override;

private:
	TObjectPtr<class UStudent> StudentSrc;
};
// Fill out your copyright notice in the Description page of Project Settings.


#include "MyGameInstance.h"
#include "Student.h"

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

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

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

	StudentSrc = NewObject<UStudent>();
	StudentSrc->SetName(TEXT("라구넹"));
	StudentSrc->SetOrder(31);

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

		// Buffer
		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* StudentDst = NewObject<UStudent>();
		StudentDst->Serialize(MemoryReaderAr);
		
		PrintStudentInfo(StudentDst, TEXT("ObjectData"));
	}
}

게임인스턴스

메모리 리더, 라이터를 사용하여 버퍼와 연결함

파일 불러오기, 가져오기는 구조체랑 비슷하게 하는데, 버퍼에 일단 담아야 함 (구조체의 경우 구조체에 바로 담으면 됨)

그리고 직렬화, 역직렬화 시 해당 메모리 리더, 라이터를 이용해서 버퍼에서 꺼내서 오브젝트에 담음

 

// Fill out your copyright notice in the Description page of Project Settings.

#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(const 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;
};
// Fill out your copyright notice in the Description page of Project Settings.


#include "Student.h"

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

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

	Ar << Order;
	Ar << Name;
}

Serialize는 해당 오브젝트에서 따로 구현 필요


Json 직렬화

웹에서 많이 쓰는 서버-클라이언트 데이터 교환 포맷

장점

- 데이터 크기 가벼움, 읽기 편함

단점

- 지원하는 타입이 적음 ( 문자, 숫자, 불리언, 널, 배열, 오브젝트 )

- 텍스트 형식으로만 사용 가능

 

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

 

Json 데이터 유형

- 오브젝트 : {}

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

- 배열 : []

배열 내 데이터는 밸류로만 구성 .. { "v1", "v2", "v3" }

- 이외

문자열, 숫자, 불리언, 널로 구성

 

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

TUniquePtr

- 지정한 곳에서만 메모리를 관리

- 특정 오브젝트에 명확하게 해지 권한을 주고 싶은 경우

- delete 없이 함수 실행 후 자동 소멸

 

TSharedPtr

- 더이상 사용되지 않으면 자동으로 메모리 해지

- 여러 로직에서 할당된 오브젝트가 공유해서 사용

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

- 널일 수 있음

 

TSharedRef

- 공유 포인터와 동일하지만 유효한 객체를 항상 보장

- Not Null

 

이런 것들을 사용해서 Json 직렬화함

 

// Copyright Epic Games, Inc. All Rights Reserved.

using UnrealBuildTool;

public class UnrealSerialization : ModuleRules
{
	public UnrealSerialization(ReadOnlyTargetRules Target) : base(Target)
	{
		PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
	
		PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput", "Json", "JsonUtilities" });

		PrivateDependencyModuleNames.AddRange(new string[] {  });

		// Uncomment if you are using Slate UI
		// PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
		
		// Uncomment if you are using online features
		// PrivateDependencyModuleNames.Add("OnlineSubsystem");

		// To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true
	}
}

UnrealSerialization.Build.cs에 Json이랑 JsonUtilities 추가 필요

 

// Fill out your copyright notice in the Description page of Project Settings.


#include "MyGameInstance.h"
#include "Student.h"
#include "JsonObjectConverter.h"

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

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

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

	StudentSrc = NewObject<UStudent>();
	StudentSrc->SetName(TEXT("라구넹"));
	StudentSrc->SetOrder(31);

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

		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, *JsonAbsoluteFilePath);
		}

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

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

		// 널이 들어갈 수 있어서 포인터로 선언 
		TSharedPtr< FJsonObject > JsonObjectDst;
		if ( FJsonSerializer::Deserialize( JsonReaderAr, JsonObjectDst ) )
		{
			UStudent* JsonStudentDst = NewObject<UStudent>();
			if (FJsonObjectConverter::JsonObjectToUStruct(JsonObjectDst.ToSharedRef(), JsonStudentDst->GetClass(), JsonStudentDst))
			{
				PrintStudentInfo(JsonStudentDst, "JsonData");
			}
		}
	}
}

언리얼 오브젝트 -> Json 오브젝트로 변환

Json 라이터 받고 Json 시리얼라이저 이용해서 직렬화하고 저장

역직렬화도 비슷한 과정

텍스트파일까지 잘 저장된 걸 확인 가능

* Json은 텍스트로 저장됨

'언리얼 엔진 > Unreal C++' 카테고리의 다른 글

[UE] Unreal Build System  (0) 2025.01.02
[UE] Package  (0) 2025.01.01
[UE] Memory Management  (0) 2024.12.29
[UE] Unreal Container Library / Struct, Map  (0) 2024.12.28
[UE] Unreal Container Library / TArray, TSet  (0) 2024.12.28