SaveGame 시스템
USaveGame 파생 클래스에 진행도와 설정 데이터를 설계하고 슬롯 단위로 저장·불러오는 흐름을 구현합니다.
이전 장에서는 UMG로 게임 UI를 만들고 관리하는 방법을 다뤘습니다.
이번에는 게임의 또 다른 핵심 요소인 데이터 저장(Saving Data)으로 넘어갑니다.
플레이어가 게임을 다시 실행했을 때 진행도, 아이템, 설정을 그대로 불러오려면 SaveGame 시스템이 필요합니다.
이번 절에서는 SaveGame 시스템의 핵심 개념과 저장/불러오기 구현 흐름을 정리합니다.
SaveGame은 진행도, 아이템, 설정처럼 세션이 끝나도 남아야 하는 값을 슬롯 단위로 저장하고 다시 복원하는 경계입니다.
- USaveGame 슬롯 저장·복원 순서
SaveGame은 게임 데이터를 저장하고 다시 불러오는 계층입니다. 2 저장할 데이터를 고른 뒤 USaveGame 상속 C++ 클래스로 담습니다. 3 저장은 SaveGame 객체 생성, 값 채우기, 슬롯 이름 선택, 디스크 기록 순서로 진행됩니다. 4 불러오기는 슬롯 존재 확인, 타입 캐스팅, 값 검증, 런타임 상태 반영 순서로 나눕니다.
- SaveGame 개념
SaveGame은 게임 데이터를 저장하고 다시 불러오는 계층입니다.
- SaveGame 데이터 구조 설계
저장할 데이터를 고른 뒤 USaveGame 상속 C++ 클래스로 담습니다.
- 게임 데이터 저장하기
저장은 SaveGame 객체 생성, 값 채우기, 슬롯 이름 선택, 디스크 기록 순서로 진행됩니다.
- 게임 데이터 불러오기
불러오기는 슬롯 존재 확인, 타입 캐스팅, 값 검증, 런타임 상태 반영 순서로 나눕니다.
- SaveGame 호환성 검수
진행도와 설정처럼 세션 밖으로 보존할 값만 저장 대상으로 분리합니다. 진행도, 아이템, 설정 복원은 SaveGame으로 분리합니다. 플랫폼별 저장 차이는 SaveGame 계층에서 흡수합니다.
SaveGame 시스템이란?
언리얼 엔진의 SaveGame 시스템은 게임 데이터를 디스크에 저장하고, 필요할 때 다시 불러올 수 있도록 설계된 추상화된 계층입니다.
이 시스템의 핵심은 USaveGame이라는 특별한 UObject 클래스이며, 이 클래스의 인스턴스에 저장하고자 하는 모든 데이터를 담아 직렬화(Serialization)하여 파일로 만듭니다.
SaveGame 시스템의 주요 특징
- 플랫폼 독립적: PC, 콘솔, 모바일 등 다양한 플랫폼에서 일관된 방식으로 데이터를 저장하고 불러올 수 있습니다.
- 쉬운 사용성: 복잡한 파일 I/O를 직접 다룰 필요 없이, 언리얼 엔진이 제공하는 함수를 통해 직관적으로 데이터를 저장하고 불러옵니다.
- 데이터 직렬화:
USaveGame클래스에 정의된 모든UPROPERTY변수들은 자동으로 직렬화되어 파일에 저장됩니다. (단, UObject나 Actor 참조는 추가적인 처리가 필요할 수 있습니다.) - 여러 Save Slot 지원: 여러 개의 독립적인 저장 슬롯을 지원하여, 플레이어가 여러 게임 진행 상태를 관리할 수 있습니다.
SaveGame 데이터 구조 설계
게임을 저장하려면, 먼저 어떤 데이터를 저장할지 결정하고 이를 담을 USaveGame을 상속받는 C++ 클래스를 정의해야 합니다.
이 클래스는 마치 저장될 데이터의 설계도와 같습니다.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/SaveGame.h"
#include "MySaveGame.generated.h"
UCLASS()
class MYPROJECT_API UMySaveGame : public USaveGame
{
GENERATED_BODY()
public:
// 생성자 (선택 사항, 필요에 따라 초기화 로직 추가)
UMySaveGame();
// 저장될 데이터들 정의 (UPROPERTY 매크로 필수)
// BlueprintReadWrite를 사용하여 블루프린트에서도 접근 가능하게 할 수 있습니다.
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Basic")
FString PlayerName; // 플레이어 이름
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Basic")
int32 PlayerScore; // 플레이어 점수
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Basic")
float PlayerHealth; // 플레이어 체력
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Basic")
FVector PlayerLocation; // 플레이어 위치 (월드 좌표)
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Basic")
FRotator PlayerRotation; // 플레이어 회전 (월드 회전)
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Inventory")
TArray<FString> InventoryItems; // 인벤토리 아이템 목록 (문자열 배열)
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Game Progress")
TMap<FString, bool> QuestsCompleted; // 완료된 퀘스트 목록 (퀘스트 이름, 완료 여부)
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Game Progress")
int32 CurrentLevelIndex; // 현재 레벨 인덱스
};#include "MySaveGame.h"
UMySaveGame::UMySaveGame()
{
// 변수들의 기본값 설정 (선택 사항)
PlayerName = TEXT("DefaultPlayer");
PlayerScore = 0;
PlayerHealth = 100.0f;
PlayerLocation = FVector::ZeroVector;
PlayerRotation = FRotator::ZeroRotator;
InventoryItems.Empty();
QuestsCompleted.Empty();
CurrentLevelIndex = 0;
}USaveGame을 상속받아야 합니다.- 저장하고자 하는 모든 변수는 반드시
UPROPERTY()매크로로 선언되어야 합니다. 그렇지 않으면 언리얼 엔진의 직렬화 시스템이 해당 변수를 인식하지 못하고 파일에 저장되지 않습니다. UObject나AActor타입의 포인터는 직접적으로 저장되지 않습니다. 이들의 참조를 저장하려면 해당 오브젝트의 고유한 ID(예: 이름, FString 경로)를 저장하고, 로드 시 해당 ID를 기반으로 월드에서 오브젝트를 찾아 다시 연결해야 합니다. (이 부분은 고급 주제이며, 이 절에서는 기본 타입에 집중합니다.)
SaveGame 클래스에는 현재 Actor 자체가 아니라 다음 실행에서 같은 상태를 재구성하는 데 필요한 안정적인 값만 담습니다.
- UPROPERTY저장 대상 데이터
UPROPERTY 1 이름, 점수, 체력, 위치, 회전처럼 직렬화 가능한 값을 저장합니다. 2 퀘스트 완료 여부, 열린 스테이지, 옵션 값처럼 게임 규칙에 필요한 상태를 보관합니다.
- 값 타입 상태
름, 점수, 체력, 위치, 회전처럼 직렬화 가능한 값을 저장합니다.
- 진행도와 설정
퀘스트 완료 여부, 열린 스테이지, 옵션 값처럼 게임 규칙에 필요한 상태를 보관합니다.
- 참조 복원그대로 저장하면 위험한 데이터
참조 복원 ! 실행마다 메모리 주소와 월드 상태가 달라질 수 있으므로 직접 저장 대상으로 삼지 않습니다. ID 이름, 경로, 데이터 테이블 키 같은 식별자를 저장한 뒤 로드 시 월드에서 다시 연결합니다.
- Actor와 UObject 포인터
! 실행마다 메모리 주소와 월드 상태가 달라질 수 있으므로 직접 저장 대상으로 삼지 않습니다.
- 고유 ID로 다시 찾기
ID 이름, 경로, 데이터 테이블 키 같은 식별자를 저장한 뒤 로드 시 월드에서 다시 연결합니다.
게임 데이터 저장하기
C++에서 저장 기능은 SaveGame 클래스를 만들고, 현재 게임 데이터를 채운 뒤, UGameplayStatics로 슬롯에 저장하는 흐름입니다.
- 1CreateSaveGameObject
저장 데이터 클래스로 객체를 생성합니다.
- 2Fill Data
위치, 체력, 점수 등 필요한 값을 복사합니다.
- 3SaveGameToSlot
SlotName과 UserIndex로 디스크에 저장합니다.
- 4LoadGameFromSlot
슬롯에서 SaveGame 객체를 다시 읽습니다.
- 5Restore
읽은 값을 Actor와 GameInstance에 적용합니다.
게임을 저장하는 과정은 다음 단계로 진행됩니다.
이는 일반적으로 APlayerController, AGameModeBase, 또는 별도의 ASaveGameManager 액터 클래스에서 담당합니다.
CreateSaveGameObject(): 저장할 USaveGame 클래스의 인스턴스를 생성합니다.
데이터 채우기: 생성된 SaveGame 오브젝트에 현재 게임 상태의 데이터를 채워 넣습니다.
SaveGameToSlot(): SaveGame 오브젝트를 특정 슬롯 이름과 사용자 인덱스를 사용하여 디스크에 저장합니다.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "MyGameModeBase.generated.h"
class UMySaveGame; // 우리가 만든 SaveGame 클래스 선언
UCLASS()
class MYPROJECT_API AMyGameModeBase : public AGameModeBase
{
GENERATED_BODY()
public:
AMyGameModeBase();
// 블루프린트에서 할당할 SaveGame 클래스
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "SaveGame")
TSubclassOf<UMySaveGame> SaveGameClass;
// 게임 저장 함수
UFUNCTION(BlueprintCallable, Category = "SaveGame")
void SaveMyGame(FString SlotName, int32 UserIndex);
};#include "MyGameModeBase.h"
#include "MySaveGame.h" // 우리가 만든 SaveGame 클래스 헤더
#include "Kismet/GameplayStatics.h" // UGameplayStatics 함수들을 위해 포함
AMyGameModeBase::AMyGameModeBase()
{
// ...
}
void AMyGameModeBase::SaveMyGame(FString SlotName, int32 UserIndex)
{
// 1. SaveGameClass가 유효한지 확인
if (!SaveGameClass)
{
UE_LOG(LogTemp, Error, TEXT("SaveGameClass is not set in MyGameModeBase!"));
return;
}
// 2. SaveGame 오브젝트 인스턴스 생성
// CreateSaveGameObject(SaveGameClass)
UMySaveGame* SaveGameInstance = Cast<UMySaveGame>(UGameplayStatics::CreateSaveGameObject(SaveGameClass));
if (!SaveGameInstance)
{
UE_LOG(LogTemp, Error, TEXT("Failed to create SaveGame object."));
return;
}
// 3. 현재 게임 데이터로 SaveGame 인스턴스 채우기
// 이 부분은 실제 게임의 상태에 따라 달라집니다.
// 예: 플레이어 컨트롤러에서 플레이어 데이터 가져오기
APlayerController* PlayerController = UGameplayStatics::GetPlayerController(GetWorld(), UserIndex);
if (PlayerController)
{
// 예시 데이터 설정
SaveGameInstance->PlayerName = TEXT("Hero");
SaveGameInstance->PlayerScore = 12345;
SaveGameInstance->PlayerHealth = 75.5f;
// 플레이어 캐릭터의 위치와 회전 가져오기 (예시)
APawn* PlayerPawn = PlayerController->GetPawn();
if (PlayerPawn)
{
SaveGameInstance->PlayerLocation = PlayerPawn->GetActorLocation();
SaveGameInstance->PlayerRotation = PlayerPawn->GetActorRotation();
}
SaveGameInstance->InventoryItems.Add(TEXT("Sword"));
SaveGameInstance->InventoryItems.Add(TEXT("Shield"));
SaveGameInstance->QuestsCompleted.Add(TEXT("FindTheLostGem"), true);
SaveGameInstance->CurrentLevelIndex = UGameplayStatics::GetCurrentLevelSequenceIndex(GetWorld()); // 현재 레벨 인덱스
}
else
{
UE_LOG(LogTemp, Warning, TEXT("Could not get PlayerController for UserIndex %d. Saving default data."), UserIndex);
}
// 4. SaveGame 오브젝트를 지정된 슬롯에 저장
// SaveGameToSlot(SaveGameObject, SlotName, UserIndex)
if (UGameplayStatics::SaveGameToSlot(SaveGameInstance, SlotName, UserIndex))
{
UE_LOG(LogTemp, Warning, TEXT("Game saved successfully to Slot: %s, User: %d"), *SlotName, UserIndex);
}
else
{
UE_LOG(LogTemp, Error, TEXT("Failed to save game to Slot: %s, User: %d"), *SlotName, UserIndex);
}
}게임 데이터 불러오기
저장 파일을 최신 구조라고 가정하지 말고 슬롯, 캐스팅, 버전, 기본값, 반영 시점을 순서대로 좁힌다.
| 순서 | 확인 | 실패 분기 | 반영 조건 |
|---|---|---|---|
| 01 슬롯 파일 존재 확인 | DoesSaveGameExist로 저장 슬롯을 찾는다. | 없으면 새 SaveGame 객체를 만든다. | 아직 게임 상태에는 쓰지 않는다. |
| 02 로드 객체 변환 | LoadGameFromSlot 결과를 원하는 클래스로 캐스팅한다. | 백업 슬롯 또는 기본 데이터로 이동한다. | 유효한 객체만 다음 검증으로 넘긴다. |
| 03 버전 구조 차이 | SaveVersion으로 예전 필드와 현재 구조를 비교한다. | 마이그레이션 규칙을 적용한다. | 누락 필드를 채운 뒤 계속한다. |
| 04 기본값 범위 보정 | 빈 배열, 범위 밖 수치, 사라진 옵션을 찾는다. | 안전한 기본값으로 치환한다. | 검증된 값만 저장 구조에 남긴다. |
| 05 적용 게임 시스템 연결 | 캐릭터, 인벤토리, 설정의 준비 시점을 맞춘다. | 레벨 로드 전후 적용을 분리한다. | 준비된 시스템부터 순차 적용한다. |
게임을 불러오는 과정은 저장 과정의 역순입니다.
DoesSaveGameExist(): 특정 슬롯에 저장된 게임이 존재하는지 먼저 확인합니다.
LoadGameFromSlot(): 지정된 슬롯에서 SaveGame 오브젝트를 불러옵니다.
데이터 사용: 불러온 SaveGame 오브젝트의 데이터를 게임의 현재 상태에 적용합니다.
#pragma once
// ... 기존 코드 ...
UCLASS()
class MYPROJECT_API AMyGameModeBase : public AGameModeBase
{
GENERATED_BODY()
public:
// ... 기존 코드 ...
// 게임 불러오기 함수
UFUNCTION(BlueprintCallable, Category = "SaveGame")
void LoadMyGame(FString SlotName, int32 UserIndex);
};#include "MyGameModeBase.h"
#include "MySaveGame.h"
#include "Kismet/GameplayStatics.h"
// ... 기존 SaveMyGame 함수 ...
void AMyGameModeBase::LoadMyGame(FString SlotName, int32 UserIndex)
{
// 1. 지정된 슬롯에 저장된 게임이 있는지 확인
if (!UGameplayStatics::DoesSaveGameExist(SlotName, UserIndex))
{
UE_LOG(LogTemp, Warning, TEXT("No SaveGame found for Slot: %s, User: %d. Starting new game."), *SlotName, UserIndex);
// 저장된 게임이 없으므로, 새 게임 시작 로직을 여기에 구현
return;
}
// 2. SaveGame 오브젝트 불러오기
// LoadGameFromSlot(SlotName, UserIndex)
UMySaveGame* LoadedGameInstance = Cast<UMySaveGame>(UGameplayStatics::LoadGameFromSlot(SlotName, UserIndex));
if (!LoadedGameInstance)
{
UE_LOG(LogTemp, Error, TEXT("Failed to load SaveGame from Slot: %s, User: %d."), *SlotName, UserIndex);
return;
}
// 3. 불러온 데이터로 게임 상태 업데이트
UE_LOG(LogTemp, Warning, TEXT("Game loaded successfully from Slot: %s, User: %d"), *SlotName, UserIndex);
UE_LOG(LogTemp, Warning, TEXT("Player Name: %s"), *LoadedGameInstance->PlayerName);
UE_LOG(LogTemp, Warning, TEXT("Player Score: %d"), LoadedGameInstance->PlayerScore);
UE_LOG(LogTemp, Warning, TEXT("Player Health: %f"), LoadedGameInstance->PlayerHealth);
UE_LOG(LogTemp, Warning, TEXT("Player Location: %s"), *LoadedGameInstance->PlayerLocation.ToString());
UE_LOG(LogTemp, Warning, TEXT("Player Rotation: %s"), *LoadedGameInstance->PlayerRotation.ToString());
// 인벤토리 아이템 출력
for (const FString& Item : LoadedGameInstance->InventoryItems)
{
UE_LOG(LogTemp, Warning, TEXT(" - Inventory Item: %s"), *Item);
}
// 퀘스트 완료 여부 출력
for (const TPair<FString, bool>& Quest : LoadedGameInstance->QuestsCompleted)
{
UE_LOG(LogTemp, Warning, TEXT(" - Quest '%s' Completed: %s"), *Quest.Key, Quest.Value ? TEXT("True") : TEXT("False"));
}
// 현재 레벨 인덱스를 기반으로 레벨 로드 (예시)
// UGameplayStatics::OpenLevel(GetWorld(), FName(*UGameplayStatics::GetLevelSequenceName(GetWorld(), LoadedGameInstance->CurrentLevelIndex)));
// 이 데이터를 게임의 실제 액터와 컴포넌트에 적용하는 로직 구현
APlayerController* PlayerController = UGameplayStatics::GetPlayerController(GetWorld(), UserIndex);
if (PlayerController)
{
APawn* PlayerPawn = PlayerController->GetPawn();
if (PlayerPawn)
{
PlayerPawn->SetActorLocation(LoadedGameInstance->PlayerLocation);
PlayerPawn->SetActorRotation(LoadedGameInstance->PlayerRotation);
// 플레이어 체력/인벤토리 등도 업데이트
}
}
}에디터에서 SaveGame 클래스 할당
위 코드를 컴파일한 후, 게임 모드 블루프린트 (예: BP_MyGameModeBase)를 열고 디테일 패널의 SaveGame 카테고리에서 Save Game Class 변수에 우리가 만든 UMySaveGame 블루프린트(또는 C++ 클래스 자체)를 할당해야 합니다.
저장된 파일의 위치
언리얼 엔진은 저장 파일을 플랫폼별로 적절한 위치에 자동으로 생성합니다.
- Windows:
C:\Users\[사용자명]\AppData\Local\[프로젝트명]\Saved\SaveGames\ - Android/iOS: 각 플랫폼의 앱 데이터 디렉토리 내에 저장됩니다.
파일 이름은 [SlotName].sav 형식으로 저장됩니다.
SaveGame 코드는 Create, Fill, Save, Load 함수 호출만 외우기보다 슬롯 정책과 복원 대상을 함께 잡아 두는 것이 중요합니다.
어떤 값이 저장 슬롯에 들어가고, 로드 후 어느 액터와 시스템에 적용되는지까지 한 흐름으로 보면 저장 실패나 부분 복원 문제를 더 빨리 찾을 수 있습니다.
SaveGame 객체는 저장 파일의 모양이고, 실제 복원은 플레이어와 월드 시스템에 값을 다시 적용하는 작업입니다.
- 1
슬롯 결정 Save slot name과 user index를 고정합니다.
- 2
컨테이너 생성 CreateSaveGameObject 로 저장 객체를 만듭니다.
- 3
값 복사 점수, 위치, 인벤토리 ID를 파일용 필드에 담습니다.
- 4
실패 확인 SaveGameToSlot 반환값과 로그를 남깁니다.
- 5
런타임 반영 로드 값은 플레이어, 월드, UI에 순서대로 적용합니다.
마지막으로 저장 기능을 운영 기준으로 보면 슬롯 선택, 데이터 채우기, 실패 처리, 복원 시점 확인을 한 묶음으로 점검해야 합니다.
레벨의 액터 포인터를 그대로 저장하는 것이 아니라, 다시 로드할 수 있는 ID와 값으로 현재 진행 상태를 기록합니다.
- Schema USaveGame 파생 클래스에 저장할
데이터와 버전 필드를 명시해 변경 이력을 견딥니다.
- Slot SlotName
UserIndex를 규칙화해 자동 저장, 수동 저장, 프로필 저장을 섞지 않습니다.
- Async Save 플레
중 저장은 AsyncSaveGameToSlot로 프레임 멈춤을 줄이고 완료 콜백을 처리합니다.
- Load Fallback 슬롯
없거나 버전이 낮을 때 기본값과 마이그레이션 경로를 준비합니다.
언리얼 엔진의 SaveGame 시스템은 게임의 진행 상태를 영구적으로 저장하고 불러오는 데 필수적인 기능입니다.
USaveGame 클래스를 사용하여 저장할 데이터의 구조를 정의하고, UGameplayStatics의 CreateSaveGameObject(), SaveGameToSlot(), LoadGameFromSlot() 함수를 통해 직관적으로 저장/불러오기 작업을 수행할 수 있습니다.
이 시스템을 통해 플레이어는 자신의 게임 진행 상황을 잃지 않고 언제든지 이어서 플레이할 수 있게 됩니다.