UE5 SaveGame 시스템
UE5의 세이브 시스템은 USaveGame 서브클래스에 UPROPERTY로 표시된 데이터를 직렬화해 슬롯(파일)에 저장합니다. UGameplayStatics::SaveGameToSlot / LoadGameFromSlot API가 직렬화와 파일 I/O를 모두 처리해 줍니다.
1. SaveGame 클래스 정의
섹션 제목: “1. SaveGame 클래스 정의”#pragma once#include "GameFramework/SaveGame.h"#include "MySaveGame.generated.h"
USTRUCT(BlueprintType)struct FInventoryItem{ GENERATED_BODY()
UPROPERTY(SaveGame) FName ItemID;
UPROPERTY(SaveGame) int32 Quantity = 0;};
UCLASS()class MYGAME_API UMySaveGame : public USaveGame{ GENERATED_BODY()
public: // 버전 필드: 마이그레이션에 사용 UPROPERTY(SaveGame) int32 SaveVersion = 1;
UPROPERTY(SaveGame) FString PlayerName;
UPROPERTY(SaveGame) int32 Level = 1;
UPROPERTY(SaveGame) float PlaytimeSeconds = 0.f;
UPROPERTY(SaveGame) FVector LastPosition = FVector::ZeroVector;
UPROPERTY(SaveGame) TArray<FInventoryItem> Inventory;
UPROPERTY(SaveGame) TMap<FName, bool> CompletedQuests;};UPROPERTY(SaveGame) 지정자가 없는 필드는 직렬화에서 제외됩니다.
2. 동기 저장/로드
섹션 제목: “2. 동기 저장/로드”#include "SaveLoadSubsystem.h"#include "MySaveGame.h"#include "Kismet/GameplayStatics.h"
static const FString DefaultSlot = TEXT("SaveSlot_0");static const int32 DefaultUser = 0;
void USaveLoadSubsystem::SaveGame(UMySaveGame* SaveData){ if (!SaveData) return;
bool bSuccess = UGameplayStatics::SaveGameToSlot( SaveData, DefaultSlot, DefaultUser);
UE_LOG(LogTemp, Log, TEXT("Save %s"), bSuccess ? TEXT("succeeded") : TEXT("failed"));}
UMySaveGame* USaveLoadSubsystem::LoadGame(){ if (!UGameplayStatics::DoesSaveGameExist(DefaultSlot, DefaultUser)) { // 새 게임: 기본값으로 초기화 return Cast<UMySaveGame>( UGameplayStatics::CreateSaveGameObject(UMySaveGame::StaticClass())); }
UMySaveGame* Loaded = Cast<UMySaveGame>( UGameplayStatics::LoadGameFromSlot(DefaultSlot, DefaultUser));
if (!Loaded) { UE_LOG(LogTemp, Warning, TEXT("Load failed, creating new save")); return Cast<UMySaveGame>( UGameplayStatics::CreateSaveGameObject(UMySaveGame::StaticClass())); }
// 버전 마이그레이션 MigrateIfNeeded(Loaded); return Loaded;}
void USaveLoadSubsystem::MigrateIfNeeded(UMySaveGame* Save){ if (Save->SaveVersion < 2) { // v1 → v2: PlaytimeSeconds 필드 추가 (기본값 유지) Save->PlaytimeSeconds = 0.f; Save->SaveVersion = 2; UE_LOG(LogTemp, Log, TEXT("Migrated save to version 2")); }}3. 비동기 저장/로드
섹션 제목: “3. 비동기 저장/로드”세이브 파일이 크거나 메인 스레드 블로킹을 피하려면 비동기 API를 사용합니다.
void USaveLoadSubsystem::AsyncSave(UMySaveGame* SaveData){ FAsyncSaveGameToSlotDelegate Delegate; Delegate.BindUObject(this, &USaveLoadSubsystem::OnAsyncSaveComplete);
UGameplayStatics::AsyncSaveGameToSlot( SaveData, DefaultSlot, DefaultUser, Delegate);}
void USaveLoadSubsystem::OnAsyncSaveComplete( const FString& SlotName, int32 UserIndex, bool bSuccess){ UE_LOG(LogTemp, Log, TEXT("Async save [%s] %s"), *SlotName, bSuccess ? TEXT("OK") : TEXT("FAILED"));
// UI 알림, 이벤트 발송 등 OnSaveCompleted.Broadcast(bSuccess);}
void USaveLoadSubsystem::AsyncLoad(){ FAsyncLoadGameFromSlotDelegate Delegate; Delegate.BindUObject(this, &USaveLoadSubsystem::OnAsyncLoadComplete);
UGameplayStatics::AsyncLoadGameFromSlot( DefaultSlot, DefaultUser, Delegate);}
void USaveLoadSubsystem::OnAsyncLoadComplete( const FString& SlotName, int32 UserIndex, USaveGame* SaveGame){ UMySaveGame* Loaded = Cast<UMySaveGame>(SaveGame); if (!Loaded) { UE_LOG(LogTemp, Warning, TEXT("Async load failed for slot: %s"), *SlotName); return; } MigrateIfNeeded(Loaded); OnLoadCompleted.Broadcast(Loaded);}4. 멀티 슬롯 관리
섹션 제목: “4. 멀티 슬롯 관리”// 슬롯 이름 생성FString GetSlotName(int32 SlotIndex){ return FString::Printf(TEXT("SaveSlot_%d"), SlotIndex);}
// 사용 중인 슬롯 목록 조회TArray<int32> GetUsedSlots(int32 MaxSlots = 10){ TArray<int32> Used; for (int32 i = 0; i < MaxSlots; i++) { if (UGameplayStatics::DoesSaveGameExist(GetSlotName(i), 0)) Used.Add(i); } return Used;}
// 슬롯 삭제void DeleteSlot(int32 SlotIndex){ UGameplayStatics::DeleteGameInSlot(GetSlotName(SlotIndex), 0);}5. 저장 대상 컴포넌트 패턴
섹션 제목: “5. 저장 대상 컴포넌트 패턴”게임 전체 상태를 한 SaveGame 클래스에 넣으면 커플링이 심해집니다. 저장 가능한 컴포넌트 인터페이스를 만들어 분리합니다.
UINTERFACE(Blueprintable)class USaveable : public UInterface { GENERATED_BODY() };
class ISaveable{ GENERATED_BODY()public: virtual void OnSave(UMySaveGame* SaveData) = 0; virtual void OnLoad(UMySaveGame* SaveData) = 0;};
// 저장 시 모든 ISaveable 컴포넌트에 위임void USaveLoadSubsystem::CollectAndSave(){ UMySaveGame* Save = Cast<UMySaveGame>( UGameplayStatics::CreateSaveGameObject(UMySaveGame::StaticClass()));
for (TObjectIterator<UActorComponent> It; It; ++It) { if (It->GetWorld() != GetWorld()) continue; if (ISaveable* Saveable = Cast<ISaveable>(*It)) Saveable->OnSave(Save); }
AsyncSave(Save);}6. 세이브 파일 암호화 — 바이트 직렬화 커스터마이징
섹션 제목: “6. 세이브 파일 암호화 — 바이트 직렬화 커스터마이징”// 세이브 데이터를 직접 바이트로 직렬화해 암호화/압축 적용bool USaveLoadSubsystem::SaveWithEncryption(UMySaveGame* SaveData, const FString& SlotName){ TArray<uint8> Bytes; FMemoryWriter Writer(Bytes, true); FObjectAndNameAsStringProxyArchive Archive(Writer, false); Archive.ArIsSaveGame = true;
// 오브젝트를 바이트 배열로 직렬화 SaveData->Serialize(Archive);
// XOR 단순 암호화 (프로덕션에서는 AES 권장) const uint8 Key = 0x42; for (uint8& Byte : Bytes) Byte ^= Key;
// 파일로 직접 저장 FString SavePath = FPaths::ProjectSavedDir() / SlotName + TEXT(".sav"); return FFileHelper::SaveArrayToFile(Bytes, *SavePath);}
UMySaveGame* USaveLoadSubsystem::LoadWithEncryption(const FString& SlotName){ FString SavePath = FPaths::ProjectSavedDir() / SlotName + TEXT(".sav");
TArray<uint8> Bytes; if (!FFileHelper::LoadFileToArray(Bytes, *SavePath)) return nullptr;
// 복호화 const uint8 Key = 0x42; for (uint8& Byte : Bytes) Byte ^= Key;
UMySaveGame* SaveGame = Cast<UMySaveGame>( UGameplayStatics::CreateSaveGameObject(UMySaveGame::StaticClass()));
FMemoryReader Reader(Bytes, true); FObjectAndNameAsStringProxyArchive Archive(Reader, true); Archive.ArIsSaveGame = true; SaveGame->Serialize(Archive);
return SaveGame;}7. 자동 저장 — 타이머 패턴
섹션 제목: “7. 자동 저장 — 타이머 패턴”// GameInstance에서 자동 저장 관리void UMyGameInstance::StartAutoSave(float IntervalSeconds){ GetWorld()->GetTimerManager().SetTimer( AutoSaveTimerHandle, this, &UMyGameInstance::PerformAutoSave, IntervalSeconds, true // bLoop );}
void UMyGameInstance::PerformAutoSave(){ USaveLoadSubsystem* SaveSub = GetSubsystem<USaveLoadSubsystem>(); if (!SaveSub) return;
// 현재 게임 상태 수집 UMySaveGame* CurrentSave = CollectCurrentGameState();
// 비동기 저장 (메인 스레드 블로킹 없음) SaveSub->AsyncSave(CurrentSave);
UE_LOG(LogTemp, Log, TEXT("Auto-saved at %f"), GetWorld()->GetTimeSeconds());}USaveGame서브클래스에UPROPERTY(SaveGame)으로 저장할 필드를 표시하면 언리얼이 자동 직렬화한다.- 메인 스레드 블로킹을 피하려면
AsyncSaveGameToSlot/AsyncLoadGameFromSlot을 사용한다. SaveVersion필드를 두고 로드 시 버전 마이그레이션 로직을 실행하면 데이터 구조 변경에 안전하게 대응할 수 있다.- 저장 로직은 Subsystem 또는 ISaveable 인터페이스로 분리해 결합도를 낮춘다.
- 민감한 데이터(골드, 진행도)는 바이트 직렬화 후 AES 암호화를 적용해 파일 변조를 방지한다.