AsyncSaveGameToSlot()
함수는 용량이 큰 저장이 일어날 때에도 게임이 최대한 부드럽게 진행될 수 있도록 해준다.
동기 저장인 SaveGameToSlot()
함수는 구조체 직렬화와 disk 저장을 전부 game thread에서 한다. 저장하는 정보가 많을 경우엔 게임 프레임이 순간적으로 드랍될 수 있다.
반면 비동기 버전인 AsyncSaveGameToSlot()
함수는 직렬화를 game thread에서 진행하지만, worker thread를 활용하여 직렬화된 정보( byte data )를 disk에 옮긴다. (보통은 직렬화가 시간을 다 잡아먹긴 한다.)
AsyncSaveGameToSlot()
함수가 SomeSlot
에 대해 권한을 얻고, 쓰기를 진행하는 도중에 같은 슬롯(SomeSlot
)에 대해 (Async)SaveGameToSlot()
함수가 호출되면 어떻게 될까?
보통은 이런 상황에서 안정적으로 처리가 될 것이라 기대하긴 힘들다. 그런데 언리얼은 어째선지 (최소한 윈도우에서는) 생각보다 안정적으로 게임을 세이브 해주었다.
inline void PrintLog(FString Str) // 로그 출력
{
UE_LOG(LogTemp, Warning, TEXT("%s"), *Str)
}
UCLASS()
class MYPROJ_API UMySaveGame : public USaveGame
{
GENERATED_BODY()
public:
UPROPERTY(VisibleAnywhere, Category = Basic)
FString PlayerName;
UPROPERTY()
TArray<int32> Arr;
// 직렬화 시작, 완료 확인하기 위해 override
virtual void Serialize(FArchive& Ar) override
{
PrintLog(TEXT("Start Serialize"));
Super::Serialize(Ar);
PrintLog(TEXT("End Serialize"));
}
}
// ...
UMySaveGame* SaveGameInstance = Cast<UMySaveGame>(UGameplayStatics::CreateSaveGameObject(UMySaveGame::StaticClass()))
UObject::Serialize()
를 오버라이드하여 직렬화가 언제 끝났는지 확인하고 Unreal Insights 툴을 활용하여 I/O 처리가 어떻게 이루어졌는지 본다.
SaveGameInstance->PlayerName = TEXT("Player One");
SaveGameInstance->Arr.Init(10, 25000000); // 0.1GB
PrintLog(TEXT("Start Async1"));
UGameplayStatics::AsyncSaveGameToSlot(SaveGameInstance, TEXT("Slot1"), 0); // 비동기 저장
SaveGameInstance->PlayerName = TEXT("Player Two");
SaveGameInstance->Arr.Init(10, 1); // 2KB
PrintLog(TEXT("Start Async2"));
UGameplayStatics::AsyncSaveGameToSlot(SaveGameInstance, TEXT("Slot1"), 0); // 비동기 저장
아래쪽에 I/O Activity
를 보면 노란색은 Open
, 분홍색은 Write
, 파란색(끝에 아주 잠시)은 Close
를 나타낸다.
첫 파일 오픈은 첫 End Serialize 직후에 시작했다. 오픈 이후 거의 바로 쓰기를 시작했다. 반면, 두 번째 End Serialize는 노란색 수직선과 같은 시점이지만 한참 뒤에야 파일을 오픈하고 (왜인지 오픈을 오래 한다), 2KB이기 때문에 아주 짧은 시간 파일을 썼다.(너무 순간이라 잘 안 보인다.)
(3연속 AsyncSaveGameToSlot()
또한 안정적으로 된다.)
SaveGameInstance->PlayerName = TEXT("Player One");
SaveGameInstance->Arr.Init(10, 25000000); // 0.1GB
PrintLog(TEXT("Start Async1"));
UGameplayStatics::AsyncSaveGameToSlot(SaveGameInstance, TEXT("Slot1"), 0); // 비동기 저장
SaveGameInstance->PlayerName = TEXT("Player Two");
SaveGameInstance->Arr.Init(10, 1); // 2KB
PrintLog(TEXT("Start Normal"));
UGameplayStatics::SaveGameToSlot(SaveGameInstance, TEXT("Slot1"), 0); // 동기 저장
첫 파일 오픈과 쓰기는 첫 End Serialize 직후에 시작했다. 두 번째 End Serialize 직후 파일을 오픈하고 첫 파일 쓰기가 완료할 때 까지 기다린다. 첫 파일 쓰기가 완료되고 좀 지나서 파일을 쓰기 시작한다.
SaveGameInstance->PlayerName = TEXT("Player One");
SaveGameInstance->Arr.Init(10, 25000000); // 0.1GB
PrintLog(TEXT("Start Async1"));
UGameplayStatics::AsyncSaveGameToSlot(SaveGameInstance, TEXT("Slot1"), 0);
SaveGameInstance->PlayerName = TEXT("Player Two");
SaveGameInstance->Arr.Init(10, 1); // 2KB
PrintLog(TEXT("Start Async2"));
UGameplayStatics::AsyncSaveGameToSlot(SaveGameInstance, TEXT("Slot1"), 0);
SaveGameInstance->PlayerName = TEXT("Player Three");
SaveGameInstance->Arr.Init(10, 1); // 2KB
PrintLog(TEXT("Start Normal"));
UGameplayStatics::SaveGameToSlot(SaveGameInstance, TEXT("Slot1"), 0);
SaveGameInstance->PlayerName = TEXT("Player One");
SaveGameInstance->Arr.Init(10, 25000000); // 0.1GB
PrintLog(TEXT("Start Async1"));
UGameplayStatics::AsyncSaveGameToSlot(SaveGameInstance, TEXT("Slot1"), 0);
SaveGameInstance->PlayerName = TEXT("Player Two");
SaveGameInstance->Arr.Init(10, 1); // 2KB
PrintLog(TEXT("Start Async2"));
UGameplayStatics::AsyncSaveGameToSlot(SaveGameInstance, TEXT("Slot2"), 0);
Slot2.sav
오픈, 쓰기를 하였다.SaveGameInstance->PlayerName = TEXT("Player One");
SaveGameInstance->Arr.Init(10, 25000000); // 0.1GB
PrintLog(TEXT("Start Async1"));
UGameplayStatics::AsyncSaveGameToSlot(SaveGameInstance, TEXT("Slot1"), 0);
SaveGameInstance->PlayerName = TEXT("Player Two");
SaveGameInstance->Arr.Init(10, 1); // 2KB
PrintLog(TEXT("Start Normal"));
UGameplayStatics::SaveGameToSlot(SaveGameInstance, TEXT("Slot2"), 0);
Slot2.sav
오픈, 그리고 쓰기를 시작한다.(Async)SaveGameToSlot
은 동시 접근을 안정적으로 처리한다. 참고로 0.1GB를 저장하는 데 약 1초가 걸렸는데 , 직렬화에만 0.9초 가량을 사용했다. 하지만 하드웨어 성능이나 예기치 못한 IO 이슈가 있을 경우를 대비해서 비동기 버전을 선호하는 것이 좋겠다. 공식 문서에서도 이를 추천한다.
0.1GB가 온전히 저장되었는지는 이 실험에서 확인하지 않았다.
공식 문서에서는 API를 간략히 소개하기 때문에 디테일한 요소를 알기 힘들다.