[Unreal] 이득우 Part 1. 13강 언리얼 오브젝트 관리 I - 직렬화

Kim Dongil·2024년 3월 19일
0

언리얼엔진

목록 보기
16/18

직렬화란?

  • 오브젝트나 연결된 오브젝트의 묶음(오브젝트 그래프)을 바이트 스트림으로 변환하는 과정
    • 복잡한 데이터를 일렬로 세우기 때문에 직렬화
  • 거꾸로 복구시키는 과정도 포함해서 의미
    • 시리얼라이제이션 : 오브젝트 그래프에서 바이트 스트림으로
    • 디시리얼라이제이션 : 바이트 스트림에서 오브젝트 그래프로
  • 직렬화가 가지는 장점
    • 현재 프로그램의 상태를 저장하고 필요한 때 복원할 수 있다. (게임의 저장)
    • 현재 객체의 정보를 클립보드에 복사해서 다른 프로그램에 전송할 수 있다.
    • 네트워크를 통해 현재 프로그램의 상태를 다른 컴퓨터에 복원할 수 있다. (멀티플레이어 게임)
    • 데이터 압축, 암호화를 통해 데이터를 효율적이고 안전하게 보관할 수도 있음.

직렬화 구현시 고려할 점

  • 이러한 직렬화를 직접 구현할 경우 다양한 상황을 고려해야 함
    • 데이터 레이아웃 : 오브젝트가 소유한 다양한 테이터를 변환할 거인가?
    • 이식성 : 서로 다른 시스템에 전송해도 이식될 수 있는가?
    • 버전 관리 : 새로운 기능이 추가될 때 이를 어떻게 확장하고 처리할 것인가?
    • 성능 : 네트웍 비용을 줄이기 위해 어떤 데이터 형식을 사용할 것인가?
    • 보안 : 데이터를 어떻게 안전하게 보호할 것인가?
    • 에러 처리 : 전송 과정에서 문제가 발생할 경우 이를 어떻게 인식하고 처리할 것인가?

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

  • 언리얼 엔진은 이러한 상황을 모두 고려한 직렬화 시스템을 자체적으로 제공하고 있음
  • 직렬화 시스템을 위해 제공하는 클래스 FArchive와 연산자
    • 아카이브 클래스(FArchive)
    • Shift(<<) operator
  • 다양한 아카이브 클래스의 제공
    • 메모리 아카이브 (FMemoryReader, FMemoryWriter)
    • 파일 아카이브 (FArchiveFileReaderGeneric, FArchiveFileWiterGeneric)
    • 기타 언리얼 오브젝트와 관련된 아카이브 클래스 (FArchiveUObject)
  • Json 직렬화 기능 : 별도의 라이브러리를 통해 제공하고 있음.

실습

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

	/// Mk.1
	// 구조체에 데이터 넣음
	FStudentData RawDataSrc(16, TEXT("이득우"));

	// 현재 프로젝트 경로에 있는 Saved 폴더 경로를 가져옴
	const FString SavedDir = FPaths::Combine(FPlatformMisc::ProjectDir(), TEXT("Saved"));
	UE_LOG(LogTemp, Log, TEXT("저장할 파일 폴더 : %s"), *SavedDir);

	{
		// Saved폴더 안에 RawData.bin 파일을 만듬
		const FString RawDataFileName(TEXT("RawData.bin"));
		FString RawDataAbosolutePath = FPaths::Combine(SavedDir, "RawDataFileName");
		UE_LOG(LogTemp, Log, TEXT("저장할 파일 전체 경로 : %s"), *RawDataAbosolutePath);

		// RawData.bin 까지의 경로를 다시 표준 형식으로 만듬
		FPaths::MakeStandardFilename(RawDataAbosolutePath);
		UE_LOG(LogTemp, Log, TEXT("변경할 파일 전체 경로 : %s"), *RawDataAbosolutePath);

		// 아카이브 쓰기전용 클래스 생성
		FArchive* RawFileWriterAr = IFileManager::Get().CreateFileWriter(*RawDataAbosolutePath);
		if (nullptr != RawFileWriterAr)
		{
			// bin 파일에 처음에 만든 구조체 정보를 넣음 (<< 는 구조체에서 오퍼레이터 연산자 구현 했음)
			*RawFileWriterAr << RawDataSrc;
			RawFileWriterAr->Close();
			delete RawFileWriterAr;
			RawFileWriterAr = nullptr;
		}

		// 아카이브 읽거 전용 클래스 생성
		FStudentData RawDataDest;
		FArchive* RawFileReaderAr = IFileManager::Get().CreateFileReader(*RawDataAbosolutePath);
		if (nullptr != RawFileReaderAr)
		{
			// 이번에는 역으로 bin 파일에서 구조체로 정보를 가져옴
			*RawFileReaderAr << RawDataDest;
			RawFileReaderAr->Close();
			delete RawFileReaderAr;
			RawFileReaderAr = nullptr;

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

	/// Mk.2
	// 새로운 학생 데이터 생성
	StudentSrc = NewObject<UStudent>();
	StudentSrc->SetName(TEXT("이득우"));
	StudentSrc->SetOrder(59);

	{
		// Saved 폴더에 bin 파일 새로 생성
		const FString ObjectDataFileName(TEXT("ObjectData.bin"));
		FString ObjectDataAbsolutePath = FPaths::Combine(*SavedDir, *ObjectDataFileName);

		// 경로 표준화
		FPaths::MakeStandardFilename(ObjectDataAbsolutePath);

		 // 학생 정보를 시리얼라이즈할 배열 생성
		TArray<uint8> BufferArray;

		// 배열과 writer 연결
		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"));
	}
}
  • 저장할 파일 경로

    • FPlatformMisc::ProjectDir 을 통해 프로젝트 경로
    • FPaths::Combine 을 통해 경로 및 파일 이름 합치기
    • FPaths::MakeStandardFilename 을 통해 경로를 표준화
      • 플랫폼 간 이식성을 확보하기 위해 파일 경로를 표준 형식으로 변환
  • Writer 열기 및 쓰기

    • FArchive 의 상속을 받으므로 FArchive로 선언
    • IFileManager::Get().CreateFileWriter 를 통해 경로의 파일을 열음
    • << 연산자를 통해서 데이터를 쓸 수 있음
      • 데이터에 << 연산자를 오버로딩해서 사용할 수 있음
      • 사용 후 Close 호출 및 delete 및 nullptr 대입
  • Reader 열기 및 읽기

    • Writer와 거의 동일
    • IFileManager::Get().CreateFileReader 로 열음

UObject 실습

  • 오브젝트 직렬화

    • TArray< uint8 >의 버퍼 생성
    • FMemoryWriter 생성
    • Serialize
  • 데이터 쓰기

    • delete, nullptr 안쓰려면 TUniquePtr 사용
    • 읽기도 읽어올 버퍼 생성하고 Reader로 진행
  • 읽어온 파일로 UObject 생성

    • FMemoryReader 생성
    • 오브젝트 생성 및 Serialize로 데이터 채워주기

Json 직렬화

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

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

Json 데이터 예시

  • Json 데이터 유형
    • 오브젝트 : {}
      • 오브젝트 내 데이터는 키, 밸류 조합으로 구성됨.
        { "key" : 10 }
    • 배열 : []
      • 배열 내 데이터는 밸류로만 구성됨.
        { "value1", "value2" }
    • 이외 데이터
      • 문자열 ("string"), 숫자 (10 또는 3.14), 불리언 (true 또는 false), 널 (null)로 구성

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

  • 일반 C++ 오브젝트의 포인터 문제를 해결해주는 언리얼 엔진의 라이브러리
  • TUniquePtr : 지정한 곳에서만 메모리를 관리하는 포인터.
    • 특정 오브젝트에게 명확하게 포인터 해지 권한을 주고 싶은 경우.
    • delete 구문 없이 함수 실행 후 자동으로 소멸시키고 싶을 때
  • TSharedPtr : 더 이상 사용되지 않으면 자동으로 메모리를 해지하는 포인터
    • 여러 로직에서 할당된 오브젝트가 공유해서 사용되는 경우
    • 다른 함수로부터 할당된 오브젝트를 Out으로 받는 경우.
    • Null 일 수 있음.
  • TSharedRef : 공유포인터와 동일하지만, 유효한 객체를 항상 보장받는 레퍼런스
    • 여러 로직에서 할당된 오브젝트가 공유해서 사용하는 경우
    • Not Null을 보장받으며 오브젝트를 편리하게 사용하고 싶은 경우

Json 실습

/// Mk.3 Json + 스마트포인터
{
	/// Json 쓰기
	// 파일 생성 및 경로 표준화
	FString JsonDataFileName(TEXT("StudentJsonData.txt"));
	FString JsonDataAbsolutePath = FPaths::Combine(*SavedDir, *JsonDataFileName);
	FPaths::MakeStandardFilename(JsonDataFileName);

	// Json 오브젝트 생성 (스마트 포인터로)
	TSharedRef<FJsonObject> JsonObjectSrc = MakeShared<FJsonObject>();

	// 학생 정보를 Json 오브젝트로 변환
	FJsonObjectConverter::UStructToJsonObject(StudentSrc->GetClass(), StudentSrc, JsonObjectSrc);

	
	// Json 쓰기 아카이브 생성 및 string 연결
	FString JsonOutString;
	TSharedRef<TJsonWriter<TCHAR>> JsonWriterAr = TJsonWriterFactory<TCHAR>::Create(&JsonOutString);

	// Json 오브젝트 시리얼라이즈
	if (FJsonSerializer::Serialize(JsonObjectSrc, JsonWriterAr))
	{
		// 시리얼라이즈 해서 얻은 string 을 사용하여 json 파일에 저장
		FFileHelper::SaveStringToFile(JsonOutString, *JsonDataAbsolutePath);
	}

	/// Json 읽기
	// Json 파일정보를 string으로 가져오기
	FString JsonInString;
	FFileHelper::LoadFileToString(JsonInString, *JsonDataAbsolutePath);

	// Json 읽기 아카이브 생성 및 string 연결
	TSharedRef<TJsonReader<TCHAR>> JsonReaderAr = TJsonReaderFactory<TCHAR>::Create(JsonInString);

	// Json 오브젝트 생성
	TSharedPtr<FJsonObject> JsonObjectDest;

	// Json 오브젝트 디시리얼라이즈
	if (FJsonSerializer::Deserialize(JsonReaderAr, JsonObjectDest))
	{
		// 테스트용 학생 생성
		UStudent* JsonStudentDest = NewObject<UStudent>();

		// Json 오브젝트 정보를 학생에 기입
		if (FJsonObjectConverter::JsonObjectToUStruct(JsonObjectDest.ToSharedRef(), JsonStudentDest->GetClass(), JsonStudentDest))
		{
			// 테스트 출력
			PrintStudentInfo(JsonStudentDest, TEXT("JsonData"));
		}
	}
}
  • json에 쓰기
    • #include "JsonObjectConverter.h" 해야함
    • 프로젝트.Build.cs 파일에 모듈 이름에 "Json", "JsonUtilities" 추가해야함
    • FJsonObject 생성
    • UObject를 FJsonObject로 변환
    • 문자열로 Json오브젝트 변환 및 파일 저장
      • Json Writer 생성
      • Serialize
      • FFileHelper::SaveStringToFile

  • 읽어들이기
    • 스트링으로 불러오기
    • Json Reader 생성
    • Deserialize
    • FJsonObjectConverter::JsonObjectToUStruct

0개의 댓글