언리얼 오브젝트 관리 Ⅰ - 직렬화
직렬화란?
- 오브젝트나 연결된 오브젝트의 묶음(graph)을 바이트 스트림으로 변환하는 과정
- 양방향을 모두 포함(deserialization)
용례
- 현재 프로그램의 상태를 저장하고 필요한 때 복원(세이브파일)
- 현재 객체의 정보를 클립보드에 복사해 다른 프로그램에 전송
- 네트워크를 통해 현재 프로그램의 상태를 다른 컴퓨터에서 복원(멀티플레이어)
- 데이터 압축, 암호화를 통해 데이터를 효율적이고 안전하게 보관(DB)
구현 시 고려할 점
- 데이터 레이아웃: 오브젝트가 소유한 데이터를 얼만큼 변환할지
- 이식성: 서로 다른 시스템에 전송해도 이식될 수 있는가
- 버전 관리; 새로운 기능이 추가될 때 어떻게 확장하고 처리할지
- 성능: 네트워크 비용을 줄이기 위해 어떤 포맷을 사용할지
- 보안: 어떻게 안전하게 데이터를 보호할지
- 에러 처리: 전송 과정에서 문제가 발생할 경우 어떻게 처리할지
언리얼 엔진의 직렬화 시스템
- 직렬화 시스템을 위해 제공하는 FArchive 클래스와 연산자
- Shift(
<<) operator
- Shift(
- 다양한 아카이브 클래스 제공
- 메모리 아카이브(FMemoryReader, FMemoryWriter)
- 파일 아카이브(FArchiveFileReaderGeneric, FArchiveFileWriterGeneric)
- 기타 언리얼 오브젝트 관련 아카이브(FArchiveUObject)
일반 오브젝트 쓰기/읽기
// StudentData.h#pragma once#include "CoreMinimal.h"struct FStudentData{ FStudentData() {} FStudentData(int32 InOrder, const FString& InName) : Order(InOrder), Name(InName) {} friend FArchive& operator<<(FArchive& Arc, FStudentData& InStudentData) { Arc << InStudentData.Order; Arc << InStudentData.Name; return Arc; } int32 Order = -1; FString Name = TEXT("홍길동");};<< 연산자 오버로딩 통해 일반 오브젝트 한꺼번에 아카이브 객체에 직렬화/역직렬화
void UMyGameInstance::Init(){ Super::Init(); FStudentData RawDataSource(23, TEXT("곽민규")); // 파일로 저장하기 위한 경로 생성 const FString SavePath = FPaths::Combine(FPlatformMisc::ProjectDir(), TEXT("Saved")); { // 쓰기 const FString RawDataFileName(TEXT("RawData.bin")); FString RawDataAbsolutePath = FPaths::Combine(SavePath, RawDataFileName); UE_LOG(LogTemp, Log, TEXT("저장할 파일 전체 경로: %s"), *RawDataAbsolutePath); FPaths::MakeStandardFilename(RawDataAbsolutePath); UE_LOG(LogTemp, Log, TEXT("변경할 파일 전체 경로: %s"), *RawDataAbsolutePath); FArchive* RawFileWriterArc = IFileManager::Get().CreateFileWriter(*RawDataAbsolutePath); if (RawFileWriterArc) { *RawFileWriterArc << RawDataSource; // 쓰기용 아카이브 닫고, 원시포인터이므로 수동 삭제처리 RawFileWriterArc->Close(); delete RawFileWriterArc; RawFileWriterArc = nullptr; } // 읽기 FStudentData RawDataDeserialized; FArchive* RawFileReaderArc = IFileManager::Get().CreateFileReader(*RawDataAbsolutePath); if (RawFileReaderArc) { *RawFileReaderArc << RawDataDeserialized; RawFileReaderArc->Close(); delete RawFileReaderArc; RawFileReaderArc = nullptr; UE_LOG(LogTemp, Log, TEXT("[RawData] 이름: %s, 순번: %d"), *RawDataDeserialized.Name, RawDataDeserialized.Order); } }FPaths, FArchive, IFileManager 모듈 사용(언리얼 내장이라 include 불필요)
언리얼 오브젝트 쓰기/읽기
// Student.h#pragma once#include "CoreMinimal.h"#include "UObject/NoExportTypes.h"#include "Student.generated.h"/** * */UCLASS()class UEPART1_API UStudent : public UObject{ GENERATED_BODY() public: UStudent(); virtual void Serialize(FArchive& Arc) override; int32 GetOrder() const { return Order; } void SetOrder(int32 InOrder) { Order = InOrder; } FString GetName() const { return Name; } void SetName(const FString& InName) { Name = InName; }private: UPROPERTY() int32 Order; UPROPERTY() FString Name;};// Student.cpp#include "Student.h"UStudent::UStudent(){ Order = -1; Name = TEXT("홍길동");}void UStudent::Serialize(FArchive& Arc){ Super::Serialize(Arc); //UObject native serialize 먼저 실행 Arc << Order; Arc << Name;}UObject의 Serialize 함수 오버라이드 후 매개변수 아카이브 객체에 직렬화/역직렬화 추가
StudentSource = NewObject<UStudent>(); StudentSource->SetName(TEXT("곽민규")); StudentSource->SetOrder(10); { // 쓰기 const FString& ObjectDataFileName(TEXT("ObjectData.bin")); FString ObjectDataAbsolutePath = FPaths::Combine(SavePath, ObjectDataFileName); FPaths::MakeStandardFilename(ObjectDataAbsolutePath); TArray<uint8> BufferArray; FMemoryWriter MemoryWriter(BufferArray); StudentSource->Serialize(MemoryWriter); // FMemoryWriter도 FArchive를 상속함 // FArchive 객체를 스마트 포인터로 감싸줌으로써 언리얼 엔진이 자동 관리하게 이관 TUniquePtr<FArchive> FileWriteArc = TUniquePtr<FArchive>(IFileManager::Get().CreateFileWriter(*ObjectDataAbsolutePath)); if (FileWriteArc) { *FileWriteArc << BufferArray; FileWriteArc->Close(); } // 읽기 TArray<uint8> BufferArrayFromFile; TUniquePtr<FArchive> FileReaderArc = TUniquePtr<FArchive>(IFileManager::Get().CreateFileReader(*ObjectDataAbsolutePath)); if (FileReaderArc) { *FileReaderArc << BufferArrayFromFile; FileReaderArc->Close(); // 바이트 스트림을 FMemoryReader로 복원 FMemoryReader MemoryReaderArc(BufferArrayFromFile); UStudent* StudentDest = NewObject<UStudent>(); StudentDest->Serialize(MemoryReaderArc); UE_LOG(LogTemp, Log, TEXT("[ObjectData] 이름: %s, 순번: %d"), *StudentDest->GetName(), StudentDest->GetOrder()); } }}JSON 직렬화 언리얼 스마트 포인터 라이브러리 개요
- 일반 C++ 오브젝트의 포인터 문제를 해결해주는 언리얼 엔진 라이브러리
TUniquePtr: 지정한 곳에서만 메모리를 관리하는 포인터- 특정 오브젝트에게 명확하게 포인터 해지 권한을 주고 싶을 때
- delete 구문 없이 자동으로 소멸시키고 싶을 때
TSharedptr: 더이상 사용되지 않으면 자동으로 메모리를 해지하는 포인터- 여러 로직에서 할당된 오브젝트가 공유되는 경우
- 다른 함수로부터 할당된 오브젝트를 Out으로 받을 때
- Nullable
TSharedRef: 공유포인터와 동일하지만 유효한 객체를 항상 보장받는 레퍼런스- 여러 로직에서 할당된 오브젝트가 공유되는 경우
- Not Null 보장됨
JSON 모듈을 사용하려면 빌드설정을 바꿔줘야 함
// 프로젝트이름.Build.cs...PublicDependencyModuleNames.AddRange( new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput", "Json", // 추가 필요 "JsonUtilities" // 추가 필요 });// JSON 직렬화{ const FString JSONDataFileName(TEXT("StudentData.json")); FString JSONDataAbsolutePath = FPaths::Combine(SavePath, JSONDataFileName); FPaths::MakeStandardFilename(JSONDataAbsolutePath); TSharedRef<FJsonObject> JSONObjectRef = MakeShared<FJsonObject>(); // Reflection 시스템으로 얻을 수 있는 UClass, 언리얼 오브젝트, JSON Out 객체 FJsonObjectConverter::UStructToJsonObject(StudentSource->StaticClass(), StudentSource, JSONObjectRef); FString JSONOutString; TSharedRef<TJsonWriter<TCHAR>> JSONWriterArc = TJsonWriterFactory<TCHAR>::Create(&JSONOutString); if (FJsonSerializer::Serialize(JSONObjectRef, JSONWriterArc)) { FFileHelper::SaveStringToFile(JSONOutString, *JSONDataAbsolutePath); }}FFileHelper::SaveStringToFile(): ANSI/Unicode, 플랫폼 상관없이 안전하게 파일에 저장
// Json 역직렬화FString JsonInString;FFileHelper::LoadFileToString(JsonInString, *JsonDataAbsolutePath);UE_LOG(LogTemp, Log, TEXT("[JSON] %s"), *JsonInString);TSharedRef<TJsonReader<TCHAR>> JsonReaderArc = TJsonReaderFactory<TCHAR>::Create(JsonInString);TSharedPtr<FJsonObject> JsonObjectDest;if (FJsonSerializer::Deserialize(JsonReaderArc, JsonObjectDest)){ UStudent* JsonStudentDest = NewObject<UStudent>(); if (FJsonObjectConverter::JsonObjectToUStruct(JsonObjectDest.ToSharedRef(), JsonStudentDest->GetClass(), JsonStudentDest)) { UE_LOG(LogTemp, Log, TEXT("[JSONData] 이름: %s, 순번: %d"), *JsonStudentDest->GetName(), JsonStudentDest->GetOrder()); }}JSON을 문자열로 읽으면(FFileHelper::LoadFileToString) 개행과 들여쓰기 때문에(\n \t) 로그찍으면 잘 안보임