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()
매크로로 선언되어야 합니다. 그렇지 않으면 언리얼 엔진의 직렬화 시스템이 해당 변수를 인식하지 못하고 파일에 저장되지 않습니다. UObject
나AActor
타입의 포인터는 직접적으로 저장되지 않습니다. 이들의 참조를 저장하려면 해당 오브젝트의 고유한 ID(예: 이름, FString 경로)를 저장하고, 로드 시 해당 ID를 기반으로 월드에서 오브젝트를 찾아 다시 연결해야 합니다. (이 부분은 고급 주제이며, 이 절에서는 기본 타입에 집중합니다.)
게임 데이터 저장하기
게임을 저장하는 과정은 다음 단계로 진행됩니다. 이는 일반적으로 APlayerController
, AGameModeBase
, 또는 별도의 ASaveGameManager
액터 클래스에서 담당합니다.
CreateSaveGameObject()
: 저장할USaveGame
클래스의 인스턴스를 생성합니다.- 데이터 채우기: 생성된
SaveGame
오브젝트에 현재 게임 상태의 데이터를 채워 넣습니다. 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);
}
}
게임 데이터 불러오기
게임을 불러오는 과정은 저장 과정의 역순입니다.
DoesSaveGameExist()
: 특정 슬롯에 저장된 게임이 존재하는지 먼저 확인합니다.LoadGameFromSlot()
: 지정된 슬롯에서SaveGame
오브젝트를 불러옵니다.- 데이터 사용: 불러온
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
클래스를 사용하여 저장할 데이터의 구조를 정의하고, UGameplayStatics
의 CreateSaveGameObject()
, SaveGameToSlot()
, LoadGameFromSlot()
함수를 통해 직관적으로 저장/불러오기 작업을 수행할 수 있습니다. 이 시스템을 통해 플레이어는 자신의 게임 진행 상황을 잃지 않고 언제든지 이어서 플레이할 수 있게 됩니다.