언리얼 엔진5 Basic - 언리얼 오브젝트 관리 1 (직렬화)

타입·2024년 3월 26일

언리얼 강의

목록 보기
5/47

직렬화 (Serialization)

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

  • 복잡한 데이터를 일렬로 세우기 때문에 직렬화
  • 거꾸로 복구시키는 과정도 포함해서 의미
    • 시리얼라이제이션(Serialization)
      오브젝트 그래프에서 바이트 스트림으로
    • 디시리얼라이제이션(Deserialization)
      바이트 스트림에서 오브젝트 그래프로
  • 직렬화가 가지는 장점
    • 현재 프로그램의 상태를 저장하고 필요할 때 복원할 수 있음
    • 현재 객체의 정보를 클립보드에 복사해서 다른 프로그램에 전송할 수 있음
    • 네트워크를 통해 현재 프로그램의 상태를 다른 컴퓨터에 복원할 수 있음
    • 데이터 압축, 암호화를 통해 데이터를 효율적이고 안전하게 보관할 수 있음
  • 직렬화 구현 시 고려할 점
    • 데이터 레이아웃
      오브젝트가 소유한 다양한 데이터를 변환할 것인가?
    • 이식성
      서로 다른 시스템에 전송해도 이식될 수 있는가?
    • 버전 관리
      새로운 기능이 추가될 때 이를 어떻게 확장하고 처리할 것인가?
    • 성능
      네트워크 비용을 줄이기 위해 어떤 데이터 형식을 사용할 것인가?
    • 보안
      데이터를 어떻게 안전하게 보호할 것인가?
    • 에러 처리
      전송 과정에서 문제가 발생할 경우 이를 어떻게 인식하고 처리할 것인가?
  • 언리얼 엔진의 직렬화 시스템
    • 직렬화 시스템을 위해 제공하는 클래스 FArchive와 연산자
      아카이브 클래스 (FArchive)
      Shift operator (<<)
    • 다양한 아카이브 클래스의 제공
      • 메모리 아카이브
        FMemoryReader, FMemoryWriter
      • 파일 아카이브
        FArchiveFileReaderGeneric, FArchiveFileWriterGeneric
      • 기타 언리얼 오브젝트와 관련된 아카이브 클래스
        FArchiveUObject
    • Json 직렬화 기능은 별도의 라이브러리를 통해 제공하고 있음

C++ 구조체 저장/불러오기

// MyGameInstance.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
{
	...
	virtual void Init() override;
};


// MyGameInstance.cpp
void UMyGameInstance::Init()
{
	Super::Init();
    
    FStudentData RawDataSrc(16, TEXT("이득우"));
    
    // 언리얼 프로젝트의 Saved 폴더
    const FString SavedDir = FPaths::Combine(FPlatformMisc::ProjectDir(), TEXT("Saved"));
    UE_LOG(LogTemp, Log, TEXT("저장할 파일 폴더 : %s"), *SavedDir);
    
    {
    	const FString RawDataFileName(TEXT("RawData.bin"));
        FString AbsolutePath = FPaths::Combine(*SavedDir, *RawDataFileName);
        UE_LOG(LogTemp, Log, TEXT("저장할 파일 전체 경로 : %s"), *AbsolutePath);
        FPaths::MakeStandardFilename(AbsolutePath);
        UE_LOG(LogTemp, Log, TEXT("변경할 파일 전체 경로 : %s"), *AbsolutePath); // 깔끔한 파일 경로
        
        // 데이터 저장
        FArchive* RawFileWriterAr = IFileManager::Get().CreateFileWriter(*AbsolutePath);
        if (RawFileWriterAr != nullptr)
        {
        	//*RawFileWriterAr << RawDataSrc.Order;
        	//*RawFileWriterAr << RawDataSrc.Name;
        	*RawFileWriterAr << RawDataSrc; // << 연산자 오버로딩
            RawFileWriterAr->Close(); // 전송 완료 후 파일 닫기
            delete RawFileWriterAr;
            RawFileWriterAr = nullptr
		}
        
        // 데이터 불러오기
        FStudentData RawDataDest;
        FArchive* RawFileReaderAr = IFileManager::Get().CreateFileReader(*AbsolutePath);
        if (RawFileReaderAr != nullptr)
        {
        	*RawFileReaderAr << RawDataDest; // Writer와 꼴은 같지만 거꾸로 흘러가서 RawDataDest로 불러옴
            RawFileReaderAr->Close();
            delete RawFileReaderAr;
            RawFileReaderAr = nullptr;
            
            UE_LOG(LogTemp, Log, TEXT("[RawData] 이름 %s 순번 %d"), *RawDataDest.Name, RawDataDest.Order);  // 이득우 16
        }
    }
}

언리얼 오브젝트 저장/불러오기

// Student.h
UCLASS()
class UNREALSERIALIZATION_API UStudent : public UObject
{
	...
public:
	virtual void Serialize(FArchive& Ar) override;

private:
    UPROPERTY()
    int32 Order;
    UPROPERTY()
    FString Name;
};


// Student.cpp
void UStudent::Serialize(FArchive& Ar)
{
	Super::Serialize(Ar);
    
    Ar << Order;
    Ar << Name;
}


// MyGameInstance.h
UCLASS()
class UNREALSERIALIZATION_API UMyGameInstance : public UGameInstance
{
	...
    UPROPERTY()
    TObjectPtr<class UStudent> StudentSrc;
};


// MyGameInstance.cpp
void UMyGameInstance::Init()
{
	...
    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); // 버퍼에 StudentSrc 데이터 기록
        
        // 파일로 저장
        if (TUniquePtr<FArchive> FileWriterAr = TUniquePtr<FArchive>(IFileManager::Get().CreateFileWriter(*ObjectDataAbsolutePath)))
        {
        	// 스마트포인터를 사용했기 때문에 블럭을 벗어나면 FileWriterAr은 자동으로 메모리 해제
            *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); // StudentDest로 불러옴
        PrintStudentInfo(StudentDest, TEXT("ObjectData")); // 이득우 59
    }
}

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

Json 직렬화

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

  • Json 장점
    텍스트임에도 데이터 크기가 가벼움
    읽기 편해서 데이터를 보고 이해할 수 있음
    사실상 웹 통신의 표준으로 널리 사용됨
  • Json 단점
    지원하는 타입이 몇가지 안됨 (문자, 숫자, boolean, null, 배열, 오브젝트만 사용 가능)
    텍스트 형식으로만 사용할 수 있음
  • 언리얼 스마트 포인터 라이브러리
    일반 C++ 오브젝트의 포인터 문제를 해결해주는 언리얼 엔진의 라이브러리
    • TUniquePtr: 지정한 곳에서만 메모리를 관리하는 포인터
      특정 오브젝트에게 명확하게 포인터 해지 권한을 주고 싶은 경우
      delete 구문 없이 함수 실행 후 자동으로 소멸시키고 싶을 때
    • TSharedPtr: 더 이상 사용되지 않으면 자동으로 메모리를 해지하는 포인터
      여러 로직에서 할당된 오브젝트가 공유해서 사용되는 경우
      다른 함수로부터 할당된 오브젝트를 Out으로 받는 경우
      Null일 수 있음
    • TSharedRef: 공유포인터와 동일하지만, 유효한 객체를 항상 보장받는 레퍼런스
      여러 로직에서 할당된 오브젝트가 공유해서 사용되는 경우
      Not Null을 보장받으며 오브젝트를 편리하게 사용하고 싶은 경우

UnrealSerialzation.Build.cs 파일에 Json과 JsonUtilities 모듈 추가

// MyGameInstance.cpp
#include "JsonObjectConverter.h"
void UMyGameInstance::Init()
{
	...
    {
        const FString JsonDataFileName(TEXT("StudentJsonData.txt"));
        FString JsonDataAbsolutePath = FPaths::Combine(*SavedDir, *JsonDataFileName);
        FPaths::MakeStandardFilename(JsonDataAbsolutePath);
        
        TSharedRef<FJsonObject> JsonObjectSrc = MakeShared<FJsonObject>(); // Null이 아님을 보장
        FJsonObjectConverter::UStructToJsonObject(StudentSrc->GetClass(), StudentSrc, JsonObjectSrc); // UStruct를 JsonObject로 변환 (UObject도 UStruct를 상속 받으므로)
        
        // 파일로 저장
        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; // Json으로 안만들어졌을 수 있으므로 포인터로 받기
        if (FJsonSerializer::Deserialize(JsonReaderAr, JsonObjectDest))
        {
        	UStudent* JsonStudentDest = NewObject<UStudent>();
            if (FJsonObjectConverter::JsonObjectToUStruct(JsonObjectDest.ToSharedRef(), JsonStudentDest->GetClass(), JsonStudetnDest))
            {
            	PrintStudentInfo(JsonStudentDest, TEXT("JsonData")); // 이득우 59
            }
        }
    }
}
profile
언리얼 프로그래머

0개의 댓글