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);}USaveGame서브클래스에UPROPERTY(SaveGame)으로 저장할 필드를 표시하면 언리얼이 자동 직렬화한다.- 메인 스레드 블로킹을 피하려면
AsyncSaveGameToSlot/AsyncLoadGameFromSlot을 사용한다. SaveVersion필드를 두고 로드 시 버전 마이그레이션 로직을 실행하면 데이터 구조 변경에 안전하게 대응할 수 있다.- 저장 로직은 Subsystem 또는 ISaveable 인터페이스로 분리해 결합도를 낮춘다.