언리얼 오브젝트 관리(1) - 직렬화

myeongrangcoding·2023년 12월 2일

직렬화

  • 오브젝트나 연결된 오브젝트의 묶음을 바이트 스트림으로 변환하는 과정
    • 복잡한 데이터를 일렬로 세움
  • 시리얼라이제이션: 오브젝트 그래프에서 바이트 스트림으로
  • 디시리얼라이제이션: 바이트 스트림에서 오브젝트 그래프로

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

  • 직렬화 시스템을 위해 제공하는 클래스 FArchive와 연산자
    • 아카이브 클래스(FArchive)
    • Shift(<<)operator

일반 C++ 구조체를 저장, 불러들임

struct FStudentData

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("홍길동");
};
// Fill out your copyright notice in the Description page of Project Settings.


#include "MyGameInstance.h"


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

		FPaths::MakeStandardFilename(RawDataAbsolutePath);
		UE_LOG(LogTemp, Log, TEXT("변경할 파일 전체 경로: %s"), *RawDataAbsolutePath);


		FArchive* RawFileWriterAr = IFileManager::Get().CreateFileWriter(*RawDataAbsolutePath);
		if (nullptr != RawFileWriterAr)
		{
			//*RawFileWriterAr << RawDataSrc.Order;
			//*RawFileWriterAr << RawDataSrc.Name;

			*RawFileWriterAr << RawDataSrc;

			RawFileWriterAr->Close();
			delete RawFileWriterAr;
			RawFileWriterAr = nullptr;
		}

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

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

언리얼 오브젝트를 저장, 불러들임

Student.cpp

// 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;
}

MyGameInstance.cpp

// 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());
}

UMyGameInstance::UMyGameInstance()
{
}

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

	StudentSrc = NewObject<UStudent>();
	StudentSrc->SetName(TEXT("명랑코딩"));
	StudentSrc->SetOrder(59);

	const FString SavedDir = FPaths::Combine(FPlatformMisc::ProjectDir(), TEXT("Saved"));

	{
		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"));
        
        /* LogTemp: [ObjectData] 이름 명랑코딩 순번 59 */
	}
}

Json 직렬화

  • Json(JavaScript Object Notation)의 약자
  • 웹 환경에서 서버와 클라이언트 사이에 데이터를 주고받을 때 사용하는 텍스트 기반 데이터 포맷
  • Json 장점
    • 텍스트임에도 데이터 크기가 가벼움.
    • 읽기 편해서 데이터를 보고 이해할 수 있음.
    • 웹 통신의 표준으로 널리 사용됨.
  • Json 단점
    • 문자, 숫자, 불리언, 널, 배열, 오브젝트만 사용 가능.
    • 텍스트 형식으로만 사용할 수 있음.

Json 데이터 예시

  • 오브젝트: {}
    • {"key":10}
  • 배열: []
    • ["value1", "value2", "value3"]
  • 문자열: "string"
  • 숫자: 10, 3.14
  • 불리언: true, false
  • 널: null

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

  • 언리얼 엔진이 제공하는 Json 라이브러리를 사용하려면 언리얼 엔진이 제공하는 스마트 포인터 라이브러리를 알아두면 좋다.
  • TUniquePtr(유니크포인터): 지정한 곳에서만 메모리를 관리하는 포인터.
  • TSharedPtr(공유포인터): 더 이상 사용되지 않으면 자동으로 메모리를 해지하는 포인터
    • 여러 로직에서 할당된 오브젝트가 공유해서 사용되는 경우
    • 다른 함수로부터 할당된 오브젝트를 Out으로 받는 경우.
    • Null 일 수 있음.
  • TSharedRef(공유레퍼런스): 공유포인트와 동일하지만, 유효한 객체를 항상 보장받는 레퍼런스
    • Not Null을 보장받으며 오브젝트를 편리하게 사용하고 싶은 경우

// 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());
}

UMyGameInstance::UMyGameInstance()
{
}

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

	StudentSrc = NewObject<UStudent>();
	StudentSrc->SetName(TEXT("명랑코딩"));
	StudentSrc->SetOrder(59);

	const FString SavedDir = FPaths::Combine(FPlatformMisc::ProjectDir(), TEXT("Saved"));

	{
		const FString JsonDataFileName(TEXT("StudentJsonData.txt"));
		FString JsonDataAbsolutePath = FPaths::Combine(*SavedDir, *JsonDataFileName);
		FPaths::MakeStandardFilename(JsonDataAbsolutePath);
		
		// 언리얼 오브젝트를 제이슨 오브젝트로 바꿔줘야 된다.
		TSharedRef<FJsonObject> JsonObjectSrc = MakeShared<FJsonObject>();

		// 프로젝트의 모듈에 Json에 관련된 라이브러리를 연동을 시켜줘야 된다.
		// UnrealSerialization.Build.cs
		// PublicDependencyModuleNames.AddRange(new string[] { "Json", "JsonUtilities" });
		FJsonObjectConverter::UStructToJsonObject(StudentSrc->GetClass(), StudentSrc, JsonObjectSrc);

		FString JsonOutString;
		// JsonWriterFactory에 의해서 Json으로 써주는 아카이브가 만들어지 됨.
		TSharedRef<TJsonWriter<TCHAR>> JsonWriterAr = TJsonWriterFactory<TCHAR>::Create(&JsonOutString);
		if (FJsonSerializer::Serialize(JsonObjectSrc, JsonWriterAr))
		{
			// if문이 성공했다면 Json 텍스트 스트링이 만들어졌을 것.
			// 이것을 파일에다 직접 씀.
			FFileHelper::SaveStringToFile(JsonOutString, *JsonDataAbsolutePath);
		}

		// 불러들임.
		FString JsonInString;
		// 파일로부터 문자열을 불러들임.
		FFileHelper::LoadFileToString(JsonInString, *JsonDataAbsolutePath);

		// 불러들인 스트링을 하용해서 리더 아카이브를 만듦.
		TSharedRef<TJsonReader<TCHAR>> JsonReaderAr = TJsonReaderFactory<TCHAR>::Create(JsonInString);

		// 문자열이 이상한 값이 들어오면 안 만들어질 수도 있기 때문에 null이 들어갈 수도 있어서 포인터로 선언.
		TSharedPtr<FJsonObject> JsonObjectDest;
		if (FJsonSerializer::Deserialize(JsonReaderAr, JsonObjectDest))
		{
			UStudent* JsonStudentDest = NewObject<UStudent>();
			if (FJsonObjectConverter::JsonObjectToUStruct(JsonObjectDest.ToSharedRef(), JsonStudentDest->GetClass(), JsonStudentDest))
			{
				PrintStudentInfo(JsonStudentDest, TEXT("JsonData"));
			}
		}
	}
}

profile
명랑코딩!

0개의 댓글