icon
7장 : 저장 및 데이터 관리

SaveGame 시스템


이전 장에서 우리는 UMG 시스템을 사용하여 게임에 사용자 인터페이스를 만들고 관리하는 방법을 배웠습니다. 이제 게임의 또 다른 핵심 요소인 데이터 저장(Saving Data) 에 대해 다룰 시간입니다. 플레이어가 게임을 껐다가 다시 켰을 때, 이전에 진행했던 상태(진행도, 아이템, 설정 등)를 그대로 불러올 수 있어야 합니다. 언리얼 엔진은 이를 위해 강력하고 유연한 SaveGame 시스템을 제공합니다.

이번 절에서는 언리얼 엔진의 SaveGame 시스템이 무엇인지, 그리고 어떻게 이를 활용하여 게임 데이터를 저장하고 불러올 수 있는지에 대해 자세히 알아보겠습니다.


SaveGame 시스템이란?

언리얼 엔진의 SaveGame 시스템은 게임 데이터를 디스크에 저장하고, 필요할 때 다시 불러올 수 있도록 설계된 추상화된 계층입니다. 이 시스템의 핵심은 USaveGame이라는 특별한 UObject 클래스이며, 이 클래스의 인스턴스에 저장하고자 하는 모든 데이터를 담아 직렬화(Serialization)하여 파일로 만듭니다.

SaveGame 시스템의 주요 특징

  • 플랫폼 독립적: PC, 콘솔, 모바일 등 다양한 플랫폼에서 일관된 방식으로 데이터를 저장하고 불러올 수 있습니다.
  • 쉬운 사용성: 복잡한 파일 I/O를 직접 다룰 필요 없이, 언리얼 엔진이 제공하는 함수를 통해 직관적으로 데이터를 저장하고 불러옵니다.
  • 데이터 직렬화: USaveGame 클래스에 정의된 모든 UPROPERTY 변수들은 자동으로 직렬화되어 파일에 저장됩니다. (단, UObject나 Actor 참조는 추가적인 처리가 필요할 수 있습니다.)
  • 여러 Save Slot 지원: 여러 개의 독립적인 저장 슬롯을 지원하여, 플레이어가 여러 게임 진행 상태를 관리할 수 있습니다.

SaveGame 데이터 구조 설계

게임을 저장하려면, 먼저 어떤 데이터를 저장할지 결정하고 이를 담을 USaveGame을 상속받는 C++ 클래스를 정의해야 합니다. 이 클래스는 마치 저장될 데이터의 "설계도"와 같습니다.

// MySaveGame.h
#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; // 현재 레벨 인덱스
};
// MySaveGame.cpp
#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() 매크로로 선언되어야 합니다. 그렇지 않으면 언리얼 엔진의 직렬화 시스템이 해당 변수를 인식하지 못하고 파일에 저장되지 않습니다.
  • UObjectAActor 타입의 포인터는 직접적으로 저장되지 않습니다. 이들의 참조를 저장하려면 해당 오브젝트의 고유한 ID(예: 이름, FString 경로)를 저장하고, 로드 시 해당 ID를 기반으로 월드에서 오브젝트를 찾아 다시 연결해야 합니다. (이 부분은 고급 주제이며, 이 절에서는 기본 타입에 집중합니다.)

게임 데이터 저장하기

게임을 저장하는 과정은 다음 단계로 진행됩니다. 이는 일반적으로 APlayerController, AGameModeBase, 또는 별도의 ASaveGameManager 액터 클래스에서 담당합니다.

  1. CreateSaveGameObject(): 저장할 USaveGame 클래스의 인스턴스를 생성합니다.
  2. 데이터 채우기: 생성된 SaveGame 오브젝트에 현재 게임 상태의 데이터를 채워 넣습니다.
  3. SaveGameToSlot(): SaveGame 오브젝트를 특정 슬롯 이름과 사용자 인덱스를 사용하여 디스크에 저장합니다.
// AMyGameModeBase.h (GameMode에서 저장 로직 예시)
#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);
};
// AMyGameModeBase.cpp
#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);
    }
}

게임 데이터 불러오기

게임을 불러오는 과정은 저장 과정의 역순입니다.

  1. DoesSaveGameExist(): 특정 슬롯에 저장된 게임이 존재하는지 먼저 확인합니다.
  2. LoadGameFromSlot(): 지정된 슬롯에서 SaveGame 오브젝트를 불러옵니다.
  3. 데이터 사용: 불러온 SaveGame 오브젝트의 데이터를 게임의 현재 상태에 적용합니다.
// AMyGameModeBase.h (GameMode에서 불러오기 로직 예시)
#pragma once

// ... 기존 코드 ...

UCLASS()
class MYPROJECT_API AMyGameModeBase : public AGameModeBase
{
    GENERATED_BODY()

public:
    // ... 기존 코드 ...

    // 게임 불러오기 함수
    UFUNCTION(BlueprintCallable, Category = "SaveGame")
    void LoadMyGame(FString SlotName, int32 UserIndex);
};
// AMyGameModeBase.cpp
#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 시스템은 게임의 진행 상태를 영구적으로 저장하고 불러오는 데 필수적인 기능입니다. USaveGame 클래스를 사용하여 저장할 데이터의 구조를 정의하고, UGameplayStaticsCreateSaveGameObject(), SaveGameToSlot(), LoadGameFromSlot() 함수를 통해 직관적으로 저장/불러오기 작업을 수행할 수 있습니다. 이 시스템을 통해 플레이어는 자신의 게임 진행 상황을 잃지 않고 언제든지 이어서 플레이할 수 있게 됩니다.