PART 5 · 강의 5/7

세이브/로드 시스템

USaveGame 기반 게임 진행 상황 저장

01

세이브 시스템 개요

언리얼 엔진의 저장 메커니즘

언리얼 엔진은 USaveGame 클래스를 통해 게임 데이터를 직렬화하고 파일로 저장합니다. SaveGame 속성이 지정된 UPROPERTY만 자동으로 직렬화됩니다.

슬롯 1: 용사의 여정

Lv.42 전사 플레이 시간: 35시간 22분 2026-02-01 18:30

슬롯 2: 마법사 도전

Lv.28 마법사 플레이 시간: 18시간 45분 2026-01-28 22:15
저장 위치

Windows: %LOCALAPPDATA%/[ProjectName]/Saved/SaveGames/
저장 파일은 바이너리 형식으로 직렬화됩니다.

02

SaveGame 클래스 정의

저장할 데이터 구조 설계

저장 데이터 구조

CharacterData FSaveCharacterData
InventoryItems TArray<FSaveItemData>
EquipmentData FSaveEquipmentData
ActiveQuests TArray<FSaveQuestData>
GameFlags TMap<FName, bool>
TotalPlayTime float
MySaveGame.h #pragma once #include "CoreMinimal.h" #include "GameFramework/SaveGame.h" #include "MySaveGame.generated.h" // 아이템 저장 데이터 USTRUCT(BlueprintType) struct FSaveItemData { GENERATED_BODY() UPROPERTY(SaveGame) FPrimaryAssetId ItemAssetId; UPROPERTY(SaveGame) int32 StackCount = 1; UPROPERTY(SaveGame) int32 EnhancementLevel = 0; UPROPERTY(SaveGame) float Durability = 100.f; UPROPERTY(SaveGame) FGuid ItemGuid; }; // 캐릭터 저장 데이터 USTRUCT(BlueprintType) struct FSaveCharacterData { GENERATED_BODY() UPROPERTY(SaveGame) FString CharacterName; UPROPERTY(SaveGame) int32 Level = 1; UPROPERTY(SaveGame) int32 ExperiencePoints = 0; UPROPERTY(SaveGame) float Health = 100.f; UPROPERTY(SaveGame) float Mana = 50.f; UPROPERTY(SaveGame) FVector Location = FVector::ZeroVector; UPROPERTY(SaveGame) FRotator Rotation = FRotator::ZeroRotator; UPROPERTY(SaveGame) FName CurrentMapName; }; // 퀘스트 저장 데이터 USTRUCT(BlueprintType) struct FSaveQuestData { GENERATED_BODY() UPROPERTY(SaveGame) FPrimaryAssetId QuestAssetId; UPROPERTY(SaveGame) EQuestState State = EQuestState::NotStarted; UPROPERTY(SaveGame) TArray<FQuestObjectiveProgress> ObjectiveProgress; };
MySaveGame.h (계속) UCLASS() class MYGAME_API UMySaveGame : public USaveGame { GENERATED_BODY() public: UMySaveGame(); // 버전 관리 (호환성 유지용) UPROPERTY(SaveGame) int32 SaveVersion = 1; UPROPERTY(SaveGame) FDateTime SaveDateTime; // 캐릭터 데이터 UPROPERTY(SaveGame) FSaveCharacterData CharacterData; // 인벤토리 UPROPERTY(SaveGame) TArray<FSaveItemData> InventoryItems; // 장비 UPROPERTY(SaveGame) TMap<EEquipmentSlot, FSaveItemData> EquippedItems; // 퀘스트 UPROPERTY(SaveGame) TArray<FSaveQuestData> ActiveQuests; UPROPERTY(SaveGame) TArray<FPrimaryAssetId> CompletedQuestIds; // 게임 플래그 (대화 진행 상황 등) UPROPERTY(SaveGame) TMap<FName, bool> GameFlags; // 언락된 태그 UPROPERTY(SaveGame) FGameplayTagContainer UnlockedTags; // 플레이 시간 UPROPERTY(SaveGame) float TotalPlayTimeSeconds = 0.f; };
중요: SaveGame 지정자

UPROPERTY에 SaveGame 지정자가 있는 속성만 직렬화됩니다. 누락하면 해당 데이터는 저장되지 않습니다.

03

저장/불러오기 구현

SaveLoadSubsystem 구현

SaveLoadSubsystem.h UCLASS() class MYGAME_API USaveLoadSubsystem : public UGameInstanceSubsystem { GENERATED_BODY() public: DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam( FOnSaveCompleted, bool, bSuccess); DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam( FOnLoadCompleted, bool, bSuccess); UPROPERTY(BlueprintAssignable) FOnSaveCompleted OnSaveCompleted; UPROPERTY(BlueprintAssignable) FOnLoadCompleted OnLoadCompleted; // 동기 저장 UFUNCTION(BlueprintCallable, Category = "SaveLoad") void SaveGame(int32 SlotIndex); // 비동기 저장 (대용량 데이터 권장) UFUNCTION(BlueprintCallable, Category = "SaveLoad") void SaveGameAsync(int32 SlotIndex); // 동기 불러오기 UFUNCTION(BlueprintCallable, Category = "SaveLoad") bool LoadGame(int32 SlotIndex); // 비동기 불러오기 UFUNCTION(BlueprintCallable, Category = "SaveLoad") void LoadGameAsync(int32 SlotIndex); UFUNCTION(BlueprintPure, Category = "SaveLoad") bool DoesSaveExist(int32 SlotIndex) const; UFUNCTION(BlueprintCallable, Category = "SaveLoad") bool DeleteSave(int32 SlotIndex); protected: FString GetSlotName(int32 SlotIndex) const; UMySaveGame* CreateSaveGameData() const; void ApplySaveGameData(UMySaveGame* SaveData); private: void OnAsyncSaveCompleted(const FString& SlotName, int32 UserIndex, bool bSuccess); void OnAsyncLoadCompleted(const FString& SlotName, int32 UserIndex, USaveGame* LoadedSave); };
SaveLoadSubsystem.cpp #include "SaveLoadSubsystem.h" #include "Kismet/GameplayStatics.h" FString USaveLoadSubsystem::GetSlotName(int32 SlotIndex) const { return FString::Printf(TEXT("SaveSlot_%d"), SlotIndex); } void USaveLoadSubsystem::SaveGame(int32 SlotIndex) { UMySaveGame* SaveData = CreateSaveGameData(); if (!SaveData) return; SaveData->SaveDateTime = FDateTime::Now(); FString SlotName = GetSlotName(SlotIndex); bool bSuccess = UGameplayStatics::SaveGameToSlot( SaveData, SlotName, 0); OnSaveCompleted.Broadcast(bSuccess); } void USaveLoadSubsystem::SaveGameAsync(int32 SlotIndex) { UMySaveGame* SaveData = CreateSaveGameData(); if (!SaveData) return; SaveData->SaveDateTime = FDateTime::Now(); FString SlotName = GetSlotName(SlotIndex); FAsyncSaveGameToSlotDelegate Delegate; Delegate.BindUObject(this, &USaveLoadSubsystem::OnAsyncSaveCompleted); UGameplayStatics::AsyncSaveGameToSlot( SaveData, SlotName, 0, Delegate); } bool USaveLoadSubsystem::LoadGame(int32 SlotIndex) { FString SlotName = GetSlotName(SlotIndex); if (!UGameplayStatics::DoesSaveGameExist(SlotName, 0)) return false; USaveGame* LoadedSave = UGameplayStatics::LoadGameFromSlot( SlotName, 0); if (UMySaveGame* MySave = Cast<UMySaveGame>(LoadedSave)) { ApplySaveGameData(MySave); OnLoadCompleted.Broadcast(true); return true; } OnLoadCompleted.Broadcast(false); return false; } bool USaveLoadSubsystem::DoesSaveExist(int32 SlotIndex) const { return UGameplayStatics::DoesSaveGameExist( GetSlotName(SlotIndex), 0); } bool USaveLoadSubsystem::DeleteSave(int32 SlotIndex) { return UGameplayStatics::DeleteGameInSlot( GetSlotName(SlotIndex), 0); }
04

데이터 수집 및 적용

게임 상태와 SaveGame 간 변환

SaveLoadSubsystem.cpp (계속) UMySaveGame* USaveLoadSubsystem::CreateSaveGameData() const { UMySaveGame* SaveData = NewObject<UMySaveGame>(); // 플레이어 캐릭터 찾기 APlayerController* PC = UGameplayStatics::GetPlayerController( GetWorld(), 0); if (!PC) return SaveData; AMyCharacter* Character = Cast<AMyCharacter>(PC->GetPawn()); if (!Character) return SaveData; // ===== 캐릭터 데이터 ===== SaveData->CharacterData.CharacterName = Character->CharacterName; SaveData->CharacterData.Level = Character->Level; SaveData->CharacterData.ExperiencePoints = Character->ExperiencePoints; SaveData->CharacterData.Location = Character->GetActorLocation(); SaveData->CharacterData.Rotation = Character->GetActorRotation(); SaveData->CharacterData.CurrentMapName = *GetWorld()->GetMapName(); // GAS 스탯 if (UAbilitySystemComponent* ASC = Character->GetAbilitySystemComponent()) { SaveData->CharacterData.Health = ASC->GetNumericAttribute( UMyAttributeSet::GetHealthAttribute()); SaveData->CharacterData.Mana = ASC->GetNumericAttribute( UMyAttributeSet::GetManaAttribute()); } // ===== 인벤토리 ===== if (UInventoryComponent* Inventory = Character->FindComponentByClass< UInventoryComponent>()) { for (UItemInstance* Item : Inventory->GetAllItems()) { if (Item && Item->ItemDefinition) { FSaveItemData ItemData; ItemData.ItemAssetId = Item->ItemDefinition->GetPrimaryAssetId(); ItemData.StackCount = Item->StackCount; ItemData.EnhancementLevel = Item->EnhancementLevel; ItemData.Durability = Item->Durability; ItemData.ItemGuid = Item->ItemGuid; SaveData->InventoryItems.Add(ItemData); } } } // ===== 퀘스트 ===== if (UQuestManagerSubsystem* QuestMgr = GetGameInstance()->GetSubsystem<UQuestManagerSubsystem>()) { for (UQuestInstance* Quest : QuestMgr->GetActiveQuests()) { FSaveQuestData QuestData; QuestData.QuestAssetId = Quest->QuestDefinition.Get()->GetPrimaryAssetId(); QuestData.State = Quest->State; QuestData.ObjectiveProgress = Quest->ObjectiveProgress; SaveData->ActiveQuests.Add(QuestData); } } return SaveData; }
SaveLoadSubsystem.cpp (적용) void USaveLoadSubsystem::ApplySaveGameData(UMySaveGame* SaveData) { if (!SaveData) return; // 맵 이동 (필요시) FString CurrentMap = GetWorld()->GetMapName(); if (CurrentMap != SaveData->CharacterData.CurrentMapName.ToString()) { // 맵 로드 후 나머지 데이터 적용 UGameplayStatics::OpenLevel( GetWorld(), SaveData->CharacterData.CurrentMapName.ToString()); return; } // 캐릭터 찾기 APlayerController* PC = UGameplayStatics::GetPlayerController( GetWorld(), 0); if (!PC) return; AMyCharacter* Character = Cast<AMyCharacter>(PC->GetPawn()); if (!Character) return; // 위치 및 회전 적용 Character->SetActorLocation(SaveData->CharacterData.Location); Character->SetActorRotation(SaveData->CharacterData.Rotation); // 인벤토리 복원 if (UInventoryComponent* Inventory = Character->FindComponentByClass< UInventoryComponent>>()) { // 기존 인벤토리 클리어 Inventory->ClearInventory(); // Asset Manager로 아이템 로드 UAssetManager& AssetMgr = UAssetManager::Get(); for (const FSaveItemData& ItemData : SaveData->InventoryItems) { UItemDefinition* ItemDef = Cast<UItemDefinition>( AssetMgr.GetPrimaryAssetObject(ItemData.ItemAssetId)); if (ItemDef) { UItemInstance* Item = NewObject<UItemInstance>(); Item->Initialize(ItemDef, ItemData.StackCount); Item->EnhancementLevel = ItemData.EnhancementLevel; Item->Durability = ItemData.Durability; Item->ItemGuid = ItemData.ItemGuid; Inventory->AddItemInstance(Item); } } } }
SUMMARY

핵심 요약

  • USaveGame 상속 — 저장할 데이터 구조를 정의하고 SaveGame 지정자로 직렬화 대상을 표시합니다.
  • PrimaryAssetId 사용 — 아이템, 퀘스트 등의 Data Asset은 ID만 저장하고 로드 시 Asset Manager로 복원합니다.
  • 비동기 저장 — 대용량 데이터는 AsyncSaveGameToSlot으로 게임 프리즈를 방지합니다.
  • 버전 관리 — SaveVersion 필드로 구버전 세이브 파일 호환성을 관리합니다.
  • Subsystem 패턴 — GameInstanceSubsystem으로 전역 세이브/로드 기능을 제공합니다.
PRACTICE

도전 과제

배운 내용을 직접 실습해보세요

실습 1: SaveGame 기반 세이브 시스템

USaveGame을 상속한 URPGSaveGame을 만들고, 캐릭터 스탯, 인벤토리, 퀘스트 진행, 월드 위치를 저장하세요. UGameplayStatics::SaveGameToSlot()/LoadGameFromSlot()을 사용하세요.

실습 2: 비동기 세이브/로드 구현

UGameplayStatics::AsyncSaveGameToSlot()과 AsyncLoadGameFromSlot()을 사용하여 세이브/로드 중 프레임 드롭을 방지하세요. 완료 콜백에서 UI 업데이트를 처리하세요.

심화 과제: SaveGame과 직렬화 커스터마이즈

FArchive를 활용하여 커스텀 직렬화를 구현하세요. 버전 관리(SaveVersion)를 추가하여 패치 후에도 이전 세이브 데이터를 마이그레이션할 수 있도록 하세요. UPROPERTY(SaveGame) 지정자도 활용하세요.