UE5 C++ 함수 AsyncSaveGameToSlot에 관한 고찰

tktj12·2025년 6월 8일
0

AsyncSaveGameToSlot() 함수는 용량이 큰 저장이 일어날 때에도 게임이 최대한 부드럽게 진행될 수 있도록 해준다.

동기 저장인 SaveGameToSlot() 함수는 구조체 직렬화와 disk 저장을 전부 game thread에서 한다. 저장하는 정보가 많을 경우엔 게임 프레임이 순간적으로 드랍될 수 있다.
반면 비동기 버전인 AsyncSaveGameToSlot() 함수는 직렬화를 game thread에서 진행하지만, worker thread를 활용하여 직렬화된 정보( byte data )를 disk에 옮긴다. (보통은 직렬화가 시간을 다 잡아먹긴 한다.)




의문점

AsyncSaveGameToSlot() 함수가 SomeSlot에 대해 권한을 얻고, 쓰기를 진행하는 도중에 같은 슬롯(SomeSlot)에 대해 (Async)SaveGameToSlot() 함수가 호출되면 어떻게 될까?

보통은 이런 상황에서 안정적으로 처리가 될 것이라 기대하긴 힘들다. 그런데 언리얼은 어째선지 (최소한 윈도우에서는) 생각보다 안정적으로 게임을 세이브 해주었다.




여러가지 실험들

실험 환경

  • UE5.5.4
  • Windows 11
  • Visual Studio 2022

실험 방식

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 처리가 어떻게 이루어졌는지 본다.

실험 1 : 연속 비동기 저장

  • 코드
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); // 비동기 저장
  • 결과 : 2KB 저장 성공
  • Unreal Insights 분석

아래쪽에 I/O Activity를 보면 노란색은 Open, 분홍색은 Write, 파란색(끝에 아주 잠시)은 Close를 나타낸다.

첫 파일 오픈은 첫 End Serialize 직후에 시작했다. 오픈 이후 거의 바로 쓰기를 시작했다. 반면, 두 번째 End Serialize는 노란색 수직선과 같은 시점이지만 한참 뒤에야 파일을 오픈하고 (왜인지 오픈을 오래 한다), 2KB이기 때문에 아주 짧은 시간 파일을 썼다.(너무 순간이라 잘 안 보인다.)

(3연속 AsyncSaveGameToSlot() 또한 안정적으로 된다.)


실험 2 : 비동기 이후 동기 저장

  • 코드
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); // 동기 저장
  • 결과 : 2KB 저장 성공
  • Unreal Insights 분석

첫 파일 오픈과 쓰기는 첫 End Serialize 직후에 시작했다. 두 번째 End Serialize 직후 파일을 오픈하고 첫 파일 쓰기가 완료할 때 까지 기다린다. 첫 파일 쓰기가 완료되고 좀 지나서 파일을 쓰기 시작한다.


실험 3 : 비동기 -> 비동기 -> 동기 저장

  • 코드
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);
  • 결과 : Player Two, 2KB 저장
  • Unreal Insights 분석

    유일하게 기대한 대로 되지 않았다. 마지막에 저장을 시도한 Player Three가 아니라 Player Two의 데이터가 저장되었다. Player Three를 위한 파일 오픈이 Player Two보다 빨랐기 때문이다.


실험 4 : 비동기 저장 이후 다른 슬롯 비동기 저장

  • 코드
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);
  • 결과 : 성공
  • Unreal Insights 분석

    두 번째 End Serialize 이후 한참 뒤에 Slot2.sav 오픈, 쓰기를 하였다.

실험 5 : 비동기 저장 이후 다른 슬롯 동기 저장

  • 코드
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);
  • 결과 : 성공
  • Unreal Insights 분석

    두 번째 End Serialize 직후, 대기 없이 Slot2.sav 오픈, 그리고 쓰기를 시작한다.

결론

  • (윈도우 기준) 같은 파일에 대해서 (Async)SaveGameToSlot은 동시 접근을 안정적으로 처리한다.
  • 비동기 버전과 동기 버전을 섞어 쓰면 순서가 보장되지 않는다.



참고로 0.1GB를 저장하는 데 약 1초가 걸렸는데 , 직렬화에만 0.9초 가량을 사용했다. 하지만 하드웨어 성능이나 예기치 못한 IO 이슈가 있을 경우를 대비해서 비동기 버전을 선호하는 것이 좋겠다. 공식 문서에서도 이를 추천한다.

0.1GB가 온전히 저장되었는지는 이 실험에서 확인하지 않았다.

공식 문서에서는 API를 간략히 소개하기 때문에 디테일한 요소를 알기 힘들다.

profile
C++, 알고리즘, UE 공부

0개의 댓글