Sav 파일
메모리 카드나 하드 드라이브와 같은 저장 매체와 함께 사용하도록 개발된 데이터 파일 형식
언리얼은 윈도우 기준으로 Saved/SavedGames
폴더에 .sav 파일로 저장된다.
이 파일을 사용하려면 USaveGame 클래스를 상속받아야 한다.
Serialize(직렬화)
객체를 바이트 스트림 형태로 연속(일직선)적인 데이터로 변환하는 것
현재 오브젝트의 상태를 보관하고, 다른 컴퓨터 환경에서 동일한 오브젝트를 만들어주는 기법?
어떠한 언어든 데이터의 메모리 구조는 크게 다음 2가지로 나뉜다.
값 형식 데이터 | 오브젝트(레퍼런스) 형식 데이터 |
integer, float(single), charactor(또는 char 의 집합인 string) 등 | 메모리 번지(주소, Address)값 --> 주소값을 최종적으로 따라가면 값 형식 데이터를 참조 하게 됨. |
이 중에서 저장 / 전송 가능한 데이터는 값 형식 데이터
-> 서로 물리적으로 사용중인 메모리 공간(OS의 가상메모리 포함) 은 일치하지 않기 때문
-> 같은 컴퓨터에서 같은 코드를 동작시켜도 인스턴스들의 주소값은 항상 바뀌기 때문
해당 인스턴스의 오브젝트 형식 데이터를 -> 값 형식 데이터로 변환하는 작업을 Serialization(직렬화) 이라한다.
이렇게 직렬화 된 데이터 형식은 텍스트 / 바이너리 등의 형식을 띄게된다.
컴퓨터 메모리 설계상 큰 데이터 덩어리를 순차적으로 읽어오는 것이 가장 빠르기 때문에 직력화된 데이터는 RDBMS 구조랑 다르게, 대게 일직선의 연속적인 값들의 집합인 형태를 띄게 됨.
이렇게 직렬화(분해)한 데이터를 다른 컴퓨터 또는 환경에 전송/저장 하여 역직렬화하여 같은 오브젝트를 만들어 줄 수 있다.
언리얼에서 마찬가지로 UObject (포인터 이므로) 에 대한 정보를 직렬화하여 저장하려고 할 때, 오브젝트 형식 데이터를 저장 할 수 없으므로 해당 인스턴스의 멤버변수들의 값 들을 저장해야한다.
아카이브 (Archive)
언리얼에서 Serialize에 대한 모든 동작은 FArchive를 상속받아서 구현
다른 플랫폼이나 디스크 메모리 등 다양한 매체에 맞게 직렬화 / 역직렬화 할 수 있게 멀티플랫폼에서 동작하는 매체의 규약의 Base Class
(바이너리 / 압축 / 버퍼 등 모두 해당 클래스를 상속받아서 동작한다.)
/*
* Base class for archives that can be used for loading, saving, and garbage
* collecting in a byte order neutral way.
*/
class FArchive : private FArchiveState
// 로드, 저장, 가비지 등에 사용할 수 있는 아카이브의 기본 클래스
// 중립적인 방식으로 바이트 순서로 수집합니다.
UObject 를 Serialize 하기 위해선 해당 구조체를 이용해야 한다.
/**
* Implements a proxy archive that serializes UObjects and FNames as string data.
*
* Expected use is:
* FArchive* SomeAr = CreateAnAr();
* FObjectAndNameAsStringProxyArchive Ar(*SomeAr);
* SomeObject->Serialize(Ar);
* FinalizeAr(SomeAr);
*
* @param InInnerArchive The actual FArchive object to serialize normal data types (FStrings, INTs, etc)
*/
struct FObjectAndNameAsStringProxyArchive : public FNameAsStringProxyArchive
{
/**
* Creates and initializes a new instance.
*
* @param InInnerArchive - The inner archive to proxy.
* @param bInLoadIfFindFails - Indicates whether to try and load a ref'd object if we don't find it
*/
FObjectAndNameAsStringProxyArchive(FArchive& InInnerArchive, bool bInLoadIfFindFails)
: FNameAsStringProxyArchive(InInnerArchive)
, bLoadIfFindFails(bInLoadIfFindFails)
{ }
/** If we fail to find an object during loading, try and load it. */
bool bLoadIfFindFails;
COREUOBJECT_API virtual FArchive& operator<<(UObject*& Obj) override;
COREUOBJECT_API virtual FArchive& operator<<(FWeakObjectPtr& Obj) override;
COREUOBJECT_API virtual FArchive& operator<<(FSoftObjectPtr& Value) override;
COREUOBJECT_API virtual FArchive& operator<<(FSoftObjectPath& Value) override;
COREUOBJECT_API virtual FArchive& operator<<(FObjectPtr& Obj) override;
};
해당 구조체를 보면 UObject 와 ObjectPtr에 대한 << 연산자가 정의되어있다.
보통 C++의 경우 << (출력) >> (입력) 으로 사용하는데 FArchive에는 << 연산자 밖에없음
구현
Struct -> (포인터를 사용하지않는)단순 구조체 값 직렬화 (컴파일)
UObject -> 인스턴스의 런타임에 필요한 메타데이터 필요
Struct 의 경우 operator<< 을 전역함수로 구현하면되고, UObject의 경우 Serailize 함수를 가지고있어 오버라이드 해서 사용하면 된다.
USTRUCT()
struct FTestStruct
{
GENERATED_BODY()
UPROPERTY()
int32 TestInt;
UPROPERTY()
FString TestString;
// 구조체 Serialize
friend FORCEINLINE FArchive& operator <<(FArchive& Ar, FTestStruct& TheStruct)
{
Ar << TheStruct.TestInt;
Ar << TheStruct.TestString;
return Ar;
}
};
UCLASS()
class TEST_API UTestObject : public UObject
{
GENERATED_BODY()
public:
virtual void Serialize(FArchive& Ar) override
{
Super::Serialize(Ar);
Ar << ItemCmsId;
Ar << ItemDBId;
Ar << ItemCount;
}
UPROPERTY()
int32 A;
UPROPERTY()
int64 B;
UPROPERTY()
int32 C;
}
Serialize는 Archive 의 옵션에 따라 기록되는 데이터가 좀 다르기 때문에 필요하다면 FArchive인자를 받은 함수를 직접 정의해서 사용해도 상관 없다.
TestUObject 를 는 직렬화 과정을 하면 코드 순서대로 TestFloat / TestArray / TestStruct 가 기록된다.
이 멤버변수 값들을 기록할 FObjectRecord라는 구조체를 하나 정의해야한다.
USTRUCT()
struct FObjectRecord
{
GENERATED_BODY()
UPROPERTY()
UClass* ObjectClass;
UPROPERTY()
FString ObjectName;
UPROPERTY()
TArray<uint8> ObjectData;
};
// UObject의 데이터 담는 구조체 반환
FObjectRecord UTestSaveGameManager::CreateObjectRecord(UObject* Element)
{
FObjectRecord ObjectRecord;
ObjectRecord.ObjectClass = Element->GetClass();
ObjectRecord.ObjectName = Element->GetName();
FMemoryWriter MemoryWriter(ObjectRecord.ObjectData, true);
FObjectAndNameAsStringProxyArchive Ar(MemoryWriter, true);
Element->Serialize(Ar);
return ObjectRecord;
}
// ObjectRecord에 저장된 데이터 기반으로 UObject생성
UObject* UTestSaveGameManager::DeSerializeObjectRecord(const FObjectRecord& ObjectRecord)
{
TArray<uint8> ObjectData = ObjectRecord.ObjectData;
FMemoryReader MemReader(ObjectData);
UObject* CreatedObject = NewObject<UObject>(GetTransientPackage(), ObjectRecord.ObjectClass);
FObjectAndNameAsStringProxyArchive Ar(MemReader, true);
CreatedObject->Serialize(Ar);
return CreatedObject;
}
ObjectData를 보면 TArray<uint8>로 되어있는데 이는 바이트 단위로 처리하기위해 적절하기 때문
int 나 char은 플랫폼마다 크기나 부호가 다를 수 있지만, uInt8은 어느 플랫폼이나 같기 떄문
이렇게 하면 UObject를 보내주게되면 ObjectRecord에 Element의 정보들이
해당 실제 클래스 / 인스턴스 이름 / ObjectData (TestUObject 경우 TestFloat / TestArray / TestStruct) 가 차례대로 Archive에 쓰여진다.
FMemoryWriter는 TArray<uint8> Bytes 멤버변수를 가지고 있고, 바이트 단위로 기록된다.
예를들어 다음과 같은 ObjectData가 기록되어있다 할 때

1Byte = 8bit = 4bit + 4bit = 16진수 + 16진수
UTestObject
|
10진수
|
16진수
|
Little Endian
(하위 바이트부터 먼저 저장) |
저장된 바이트
|
A
|
104000001
|
0x0632EA01
|
[0x01, 0xEA, 0x32, 0x06]
|
[1, 234, 50, 6]
|
B
|
4,673
|
0x1241
|
[0x41, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
|
[65, 18, 0, 0, 0, 0, 0, 0]
|
C
|
99
|
0x63
|
[0x63, 0x00, 0x00, 0x00]
|
[99, 0, 0, 0]
|
![]() |
![]() |
Bytes 에는 << 연산자 순서대로 기록되기 때문에 코드 순서를 바꾸거나 데이터 값을 바꾸는데 유의해야 한다.
FMemoryReader 마찬가지로 데이터를 읽은 후 TArray<uint8> Bytes 에서 << 연산자로 값을 가져올 수 있다.
이제 쓰여진 Archive를 .sav 파일로 만들어야한다.
UCLASS(BlueprintType)
class TEST_API UTestSaveData : public USaveGame
{
GENERATED_BODY()
public:
// All object data in one array
UPROPERTY()
TArray<FObjectRecord> SavedObjects;
};
void UTestSaveGameManager::SaveGameData(const FString& SlotName, TArray<UObject*>& GameData)
{
UTestSaveData* CreatedTestSaveData = Cast<UTestSaveData>(UGameplayStatics::CreateSaveGameObject(UTestSaveData::StaticClass()));
for (UObject* Element : GameData)
{
const FObjectRecord& ObjectRecord = CreateObjectRecord(Element);
CreatedTestSaveData->SavedObjects.Add(ObjectRecord);
}
UGameplayStatics::AsyncSaveGameToSlot(CreatedTestSaveData, SlotName, staticUserIndex, OnCompletedSaveGameDataDelegate);
}
UGameplayStatics 클래스 내부에 동기 저장 / 비동기 저장 방식이 따로 있고, SlotName 과 UserIndex 파라미터를 같이 넘겨주어 저장할 수 있다.
UserIndex 사용 안함
언리얼 공식문서와는 달리 디버깅 해보니까 UserIndex는 쓰지도 않음
그래서 하나의 SlotName 파일에 덮어 씌우고 로드 한다.
(어떤 사이트에선 H5 (Html5) 에서만 사용한다고)
여기까지하면 Saved/SaveGames
폴더에 .sav 파일이 저장되는것을 확인할 수 있다.
대충 .sav 파일 리더로 확인해보면
위에서 인스턴스 클래스 / 인스턴스 이름 / 멤버변수 값 차례대로 저장한 그대로 쓰여진다.
UObject를 상속받은 인스턴스들을 저장하거나 불러올 때는 위처럼 FObjectRecord 에 담아서 그대로 생성하고 값을 세팅해주면되고, 그 외의 TMap / TArray (int / FString / float 같으 기본 자료형일 때) 등의 언리얼에서 사용하는 것들은 그대로 저장 / 로드가 가능하다.
'언리얼' 카테고리의 다른 글
모바일 EnableGestureRecognizer 관련 (0) | 2025.01.02 |
---|---|
AsyncTask 간단 예제 (0) | 2024.12.16 |
SceneCaptureComponent2D (0) | 2024.06.26 |
ChunkDownloader (0) | 2024.05.02 |
소프트 포인터 (1) | 2024.02.19 |