1. 세이브 시스템이 필요한 이유
게임을 종료했다가 다시 켰을 때 플레이어의 진행 상황이 사라진다면, 그 게임은 상품으로서의 가치를 잃습니다. 현대 게임에서 세이브와 로드 기능은 선택 사항이 아니라 기본 요건입니다. 플레이어의 위치, 인벤토리, 퀘스트 진행 상황, 레벨 내 오브젝트 상태 등 수많은 데이터를 디스크에 기록하고 다시 복원하는 작업이 바로 세이브 시스템의 핵심입니다.
언리얼 엔진은 이 문제를 해결하기 위해 USaveGame 클래스와 UGameplayStatics의 저장/로드 API를 기본으로 제공합니다. 별도의 파일 I/O 코드 없이도 데이터를 디스크에 안전하게 쓰고 읽을 수 있습니다. 직접 파일 시스템을 다루는 방식을 채택할 수도 있지만, 플랫폼별 저장 경로, 직렬화 처리, 오류 복구 등의 복잡한 문제를 엔진이 대신 처리해 준다는 점에서 USaveGame 기반 시스템을 먼저 충분히 활용하는 것이 좋습니다.
2. 직렬화(Serialization)란 무엇인가
직렬화란 메모리에 존재하는 객체의 상태를 연속된 바이트 스트림으로 변환하는 과정입니다. 반대로 바이트 스트림을 다시 객체로 복원하는 과정을 역직렬화(Deserialization)라고 합니다. 디스크 저장, 네트워크 전송, 에디터의 언두/리두 기능 모두 직렬화를 기반으로 동작합니다.
언리얼 엔진에서 직렬화의 핵심은 FArchive 클래스입니다. FArchive는 읽기와 쓰기를 동일한 인터페이스로 처리하는 추상 클래스로, << 연산자를 오버로딩하여 데이터를 주고받습니다. 언리얼의 모든 UObject와 AActor는 Serialize(FArchive& Ar) 함수를 가지며, 이 함수 하나로 저장과 로드 양방향을 처리할 수 있습니다.
세이브 시스템에서 주로 사용하는 FArchive 구현체는 두 가지입니다.
FMemoryWriter— 데이터를TArray<uint8>바이트 배열에 씁니다.FMemoryReader— 바이트 배열에서 데이터를 읽어 객체를 복원합니다.
실제 세이브 시스템에서는 이 두 클래스를 직접 쓰기보다, FObjectAndNameAsStringProxyArchive로 래핑하여 사용하는 경우가 많습니다. 이 프록시 아카이브는 오브젝트 포인터를 단순 메모리 주소 대신 문자열(경로) 형태로 직렬화하기 때문에 게임을 재시작해도 올바르게 역직렬화할 수 있습니다.
3. USaveGame 클래스의 구조
USaveGame은 UObject를 상속받은 단순한 베이스 클래스입니다. 엔진이 제공하는 기능이 특별히 많은 클래스가 아니라, UGameplayStatics의 저장/로드 함수가 인식하는 마커 역할을 합니다. 실제 데이터는 이 클래스를 상속받아 UPROPERTY로 멤버 변수를 선언하는 방식으로 정의합니다.
아래는 커스텀 세이브 게임 클래스를 정의하는 예시입니다. 저장할 데이터를 USTRUCT로 묶어 관리하는 패턴이 권장됩니다.
// MySaveGame.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/SaveGame.h"
#include "MySaveGame.generated.h"
// 플레이어 기본 정보를 담는 구조체
USTRUCT()
struct FPlayerSaveData
{
GENERATED_BODY()
UPROPERTY()
FVector Location;
UPROPERTY()
int32 CurrentHealth;
UPROPERTY()
TArray<FName> UnlockedAbilities;
};
// 레벨 내 개별 액터 상태를 담는 구조체
USTRUCT()
struct FActorSaveData
{
GENERATED_BODY()
UPROPERTY()
FName ActorName; // 액터를 식별하는 고유 이름
UPROPERTY()
FTransform Transform; // 이동 가능한 액터의 트랜스폼
UPROPERTY()
TArray<uint8> ByteData; // SaveGame 지정자가 붙은 UPROPERTY 데이터
};
UCLASS()
class MYGAME_API UMySaveGame : public USaveGame
{
GENERATED_BODY()
public:
UPROPERTY()
FPlayerSaveData PlayerData;
UPROPERTY()
TArray<FActorSaveData> SavedActors;
};
위의 코드에서는 플레이어 데이터와 레벨 액터 데이터를 별도의 USTRUCT로 분리하여 USaveGame 클래스에 보관합니다. TArray<uint8> 타입의 ByteData는 액터 내부의 SaveGame 지정자 변수들을 바이트 배열로 직렬화한 결과를 담는 용도입니다.
4. UPROPERTY SaveGame 지정자
UPROPERTY(SaveGame) 지정자는 해당 변수가 직렬화 대상임을 언리얼 엔진의 리플렉션 시스템에 알립니다. 단, 이 지정자 자체가 자동으로 변수를 저장해 주지는 않습니다. FArchive의 ArIsSaveGame 플래그가 true로 설정된 상태에서 Serialize를 호출해야 비로소 SaveGame 지정자가 붙은 변수만 걸러서 직렬화됩니다.
아래는 적 오브젝트에 SaveGame 지정자를 적용하는 예시입니다.
// AEnemyCharacter.h
UCLASS()
class MYGAME_API AEnemyCharacter : public ACharacter
{
GENERATED_BODY()
public:
// SaveGame 지정자: 체력과 활성화 여부만 저장 대상
UPROPERTY(SaveGame)
float CurrentHealth;
UPROPERTY(SaveGame)
bool bIsAlive;
// 일반 UPROPERTY: 저장 대상에서 제외 (계산 가능한 값)
UPROPERTY()
float MaxHealth;
};
위 코드에서는 CurrentHealth와 bIsAlive만 직렬화 대상이며, MaxHealth처럼 데이터 테이블에서 다시 불러올 수 있는 값은 굳이 저장하지 않습니다. 저장해야 할 데이터와 런타임에 복원 가능한 데이터를 구분하는 것이 세이브 시스템 설계의 첫걸음입니다.
5. 기본 저장과 로드
UGameplayStatics는 세이브 게임을 위한 네 가지 핵심 함수를 제공합니다.
| 함수 | 설명 |
|---|---|
CreateSaveGameObject |
USaveGame 파생 클래스의 인스턴스를 생성합니다. |
SaveGameToSlot |
슬롯 이름과 유저 인덱스로 동기 저장합니다. |
LoadGameFromSlot |
슬롯 이름과 유저 인덱스로 동기 로드합니다. |
DoesSaveGameExist |
해당 슬롯에 세이브 파일이 존재하는지 확인합니다. |
다음은 플레이어 데이터를 저장하고 로드하는 기본 패턴입니다.
// 저장 함수
void USaveGameSubsystem::SaveGame()
{
// 세이브 게임 오브젝트가 없으면 새로 생성
if (!CurrentSaveGame)
{
CurrentSaveGame = CastChecked<UMySaveGame>(
UGameplayStatics::CreateSaveGameObject(UMySaveGame::StaticClass())
);
}
// 플레이어 컨트롤러에서 현재 상태를 수집
APlayerController* PC = GetWorld()->GetFirstPlayerController();
if (AMyPlayerCharacter* Player = Cast<AMyPlayerCharacter>(PC->GetPawn()))
{
CurrentSaveGame->PlayerData.Location = Player->GetActorLocation();
CurrentSaveGame->PlayerData.CurrentHealth = Player->GetCurrentHealth();
}
// 슬롯 이름과 유저 인덱스(싱글 플레이어는 0)로 저장
const bool bSuccess = UGameplayStatics::SaveGameToSlot(
CurrentSaveGame, SaveSlotName, 0
);
if (!bSuccess)
{
UE_LOG(LogTemp, Warning, TEXT("SaveGame: 저장 실패 - 슬롯 이름: %s"), *SaveSlotName);
}
}
// 로드 함수
void USaveGameSubsystem::LoadGame()
{
if (!UGameplayStatics::DoesSaveGameExist(SaveSlotName, 0))
{
UE_LOG(LogTemp, Log, TEXT("SaveGame: 저장 파일 없음, 새 게임 시작"));
return;
}
USaveGame* LoadedGame = UGameplayStatics::LoadGameFromSlot(SaveSlotName, 0);
CurrentSaveGame = Cast<UMySaveGame>(LoadedGame);
if (!CurrentSaveGame)
{
UE_LOG(LogTemp, Error, TEXT("SaveGame: 로드 실패 또는 캐스트 실패"));
return;
}
// 플레이어에게 데이터 복원
APlayerController* PC = GetWorld()->GetFirstPlayerController();
if (AMyPlayerCharacter* Player = Cast<AMyPlayerCharacter>(PC->GetPawn()))
{
Player->SetActorLocation(CurrentSaveGame->PlayerData.Location);
Player->SetCurrentHealth(CurrentSaveGame->PlayerData.CurrentHealth);
}
}
위의 코드에서는 DoesSaveGameExist로 파일 존재 여부를 먼저 확인하고, Cast를 통해 올바른 타입으로 복원하는 흐름을 따릅니다. SaveGameToSlot의 반환값(bool)은 저장 성공 여부를 나타내므로, 실패 시 로그를 남기도록 처리하는 것이 중요합니다.
6. 액터 상태 직렬화: FObjectAndNameAsStringProxyArchive 활용
플레이어 데이터처럼 값 타입(int32, FVector 등)은 USaveGame의 멤버 변수로 직접 저장하면 됩니다. 하지만 레벨에 배치된 다수의 액터 상태(예: 부숴진 상자, 열린 문)를 저장할 때는 액터 자신의 SaveGame 지정자 변수를 바이트 배열로 직렬화하는 방식을 사용합니다.
이 방식의 핵심은 FObjectAndNameAsStringProxyArchive이며, ArIsSaveGame = true를 설정하면 SaveGame 지정자가 붙은 변수만 필터링하여 직렬화합니다.
아래는 인터페이스를 구현하는 액터들을 순회하며 상태를 저장하고 복원하는 전체 예시입니다.
#include "Serialization/ObjectAndNameAsStringProxyArchive.h"
#include "Kismet/GameplayStatics.h"
// 저장 대상 액터를 식별하기 위한 인터페이스 확인 후 월드 순회
void USaveGameSubsystem::SaveWorldActors()
{
CurrentSaveGame->SavedActors.Empty();
for (FActorIterator It(GetWorld()); It; ++It)
{
AActor* Actor = *It;
// ISavableInterface를 구현한 액터만 저장 대상
if (!Actor->Implements<USavableInterface>())
{
continue;
}
FActorSaveData ActorData;
ActorData.ActorName = Actor->GetFName(); // 식별자로 FName 사용
// FMemoryWriter → FObjectAndNameAsStringProxyArchive 래핑
FMemoryWriter MemWriter(ActorData.ByteData);
FObjectAndNameAsStringProxyArchive Ar(MemWriter, true);
Ar.ArIsSaveGame = true; // SaveGame 지정자 변수만 직렬화
Actor->Serialize(Ar); // 액터의 SaveGame UPROPERTY 직렬화 실행
CurrentSaveGame->SavedActors.Add(ActorData);
}
UGameplayStatics::SaveGameToSlot(CurrentSaveGame, SaveSlotName, 0);
}
// 로드 시 저장된 바이트 데이터를 액터에 복원
void USaveGameSubsystem::LoadWorldActors()
{
for (FActorIterator It(GetWorld()); It; ++It)
{
AActor* Actor = *It;
if (!Actor->Implements<USavableInterface>())
{
continue;
}
// 저장 데이터에서 이름이 일치하는 항목 탐색
for (const FActorSaveData& ActorData : CurrentSaveGame->SavedActors)
{
if (ActorData.ActorName == Actor->GetFName())
{
Actor->SetActorTransform(ActorData.Transform);
// FMemoryReader로 바이트 배열에서 복원
FMemoryReader MemReader(ActorData.ByteData);
FObjectAndNameAsStringProxyArchive Ar(MemReader, true);
Ar.ArIsSaveGame = true;
Actor->Serialize(Ar); // 역직렬화로 SaveGame UPROPERTY 복원
// 인터페이스를 통해 로드 완료 이벤트 통보
ISavableInterface::Execute_OnActorLoaded(Actor);
break;
}
}
}
}
위의 코드에서는 FActorIterator로 월드 전체 액터를 순회하지만, 인터페이스 구현 여부로 저장 대상을 좁힙니다. FObjectAndNameAsStringProxyArchive를 사용함으로써 오브젝트 포인터 타입 변수도 재시작 후에 올바르게 역직렬화됩니다.
7. GameInstanceSubsystem을 통한 세이브 시스템 관리
세이브 로직을 어디에 배치할지는 설계 초기에 결정해야 합니다. 가장 권장되는 방식은 UGameInstanceSubsystem을 활용하는 것입니다. 서브시스템은 게임 인스턴스와 수명이 같기 때문에 레벨 전환에도 세이브 데이터가 유지되며, 블루프린트와 C++ 양쪽에서 접근하기 편리합니다.
아래는 세이브 서브시스템의 헤더 정의 예시입니다.
// SaveGameSubsystem.h
#pragma once
#include "CoreMinimal.h"
#include "Subsystems/GameInstanceSubsystem.h"
#include "SaveGameSubsystem.generated.h"
class UMySaveGame;
UCLASS()
class MYGAME_API USaveGameSubsystem : public UGameInstanceSubsystem
{
GENERATED_BODY()
public:
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
UFUNCTION(BlueprintCallable, Category = "SaveGame")
void SaveGame();
UFUNCTION(BlueprintCallable, Category = "SaveGame")
void LoadGame();
UFUNCTION(BlueprintCallable, Category = "SaveGame")
bool HasSaveData() const;
protected:
UPROPERTY()
TObjectPtr<UMySaveGame> CurrentSaveGame; // 현재 로드된 세이브 게임 오브젝트
FString SaveSlotName = TEXT("SaveSlot_Default");
int32 UserIndex = 0;
};
Initialize 함수에서 슬롯이 이미 존재하면 로드하고, 없으면 새 인스턴스를 생성하는 초기화 로직을 두면 게임 시작 시 세이브 데이터를 자동으로 준비할 수 있습니다.
// SaveGameSubsystem.cpp
void USaveGameSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
// 기존 세이브 파일이 있으면 즉시 로드, 없으면 새 인스턴스 생성
if (UGameplayStatics::DoesSaveGameExist(SaveSlotName, UserIndex))
{
CurrentSaveGame = Cast<UMySaveGame>(
UGameplayStatics::LoadGameFromSlot(SaveSlotName, UserIndex)
);
}
else
{
CurrentSaveGame = CastChecked<UMySaveGame>(
UGameplayStatics::CreateSaveGameObject(UMySaveGame::StaticClass())
);
}
}
위 코드에서는 Initialize를 통해 서브시스템이 생성될 때 세이브 데이터가 자동으로 준비됩니다. 이후 어느 레벨, 어느 블루프린트에서도 GetGameInstance()->GetSubsystem<USaveGameSubsystem>()으로 접근할 수 있습니다.
8. 비동기 저장과 로드
SaveGameToSlot은 메인 스레드를 블로킹하는 동기 함수입니다. 저장 데이터가 크거나 디스크 I/O가 느린 환경(예: 콘솔 플랫폼 HDD)에서는 프레임 드랍을 유발할 수 있습니다. 이를 방지하기 위해 UE5는 비동기 저장/로드 API를 제공합니다.
8-1. 비동기 저장: AsyncSaveGameToSlot
void USaveGameSubsystem::SaveGameAsync()
{
if (!CurrentSaveGame)
{
return;
}
// 저장 완료 시 호출될 델리게이트 설정
FAsyncSaveGameToSlotDelegate SaveDelegate;
SaveDelegate.BindUObject(this, &USaveGameSubsystem::OnSaveCompleted);
// 직렬화는 게임 스레드에서, 파일 쓰기는 워커 스레드에서 실행
UGameplayStatics::AsyncSaveGameToSlot(
CurrentSaveGame, SaveSlotName, UserIndex, SaveDelegate
);
}
// 저장 완료 콜백 — 게임 스레드에서 호출됨
void USaveGameSubsystem::OnSaveCompleted(
const FString& SlotName, const int32 InUserIndex, bool bSuccess)
{
if (bSuccess)
{
UE_LOG(LogTemp, Log, TEXT("비동기 저장 완료: %s"), *SlotName);
// UI 알림, 자동저장 아이콘 숨김 등의 처리
}
else
{
UE_LOG(LogTemp, Error, TEXT("비동기 저장 실패: %s"), *SlotName);
}
}
8-2. 비동기 로드: AsyncLoadGameFromSlot
void USaveGameSubsystem::LoadGameAsync()
{
FAsyncLoadGameFromSlotDelegate LoadDelegate;
LoadDelegate.BindUObject(this, &USaveGameSubsystem::OnLoadCompleted);
UGameplayStatics::AsyncLoadGameFromSlot(SaveSlotName, UserIndex, LoadDelegate);
}
// 로드 완료 콜백 — 게임 스레드에서 호출됨
void USaveGameSubsystem::OnLoadCompleted(
const FString& SlotName, const int32 InUserIndex, USaveGame* LoadedGame)
{
CurrentSaveGame = Cast<UMySaveGame>(LoadedGame);
if (!CurrentSaveGame)
{
UE_LOG(LogTemp, Warning, TEXT("로드 실패 또는 파일 없음: %s"), *SlotName);
return;
}
// 플레이어 및 월드 상태 복원 처리 호출
RestorePlayerData();
LoadWorldActors();
}
위의 코드에서는 AsyncSaveGameToSlot이 직렬화를 게임 스레드에서 수행하고, 플랫폼별 파일 쓰기만 워커 스레드로 분리합니다. 콜백 함수는 다시 게임 스레드에서 호출되므로 UObject 접근이 안전합니다.
9. 일반 C++과의 비교
일반 C++에서 직렬화를 구현하려면 fstream, ofstream 등을 직접 다루거나, cereal, nlohmann/json 같은 서드파티 라이브러리를 사용해야 합니다.
// 일반 C++ 방식 예시
#include <fstream>
struct PlayerData { int health; float x, y, z; };
void SaveToFile(const PlayerData& data, const std::string& path)
{
std::ofstream out(path, std::ios::binary);
out.write(reinterpret_cast<const char*>(&data), sizeof(PlayerData));
}
void LoadFromFile(PlayerData& data, const std::string& path)
{
std::ifstream in(path, std::ios::binary);
in.read(reinterpret_cast<char*>(&data), sizeof(PlayerData));
}
위와 같은 방식은 단순 POD 구조에서는 동작하지만, 포인터, 동적 배열, 상속 계층이 포함된 데이터에서는 별도의 처리가 필요합니다. 또한 플랫폼별 경로 처리, 파일 권한, 쓰기 실패 복구 등을 모두 직접 구현해야 합니다.
언리얼 엔진의 USaveGame 시스템은 이 모든 복잡성을 FArchive 기반 추상화로 해결합니다. << 연산자 오버로딩을 통해 읽기와 쓰기를 동일한 코드 경로에서 처리하고, UPROPERTY 리플렉션을 이용해 직렬화 대상 변수를 자동으로 수집합니다. 플랫폼별 저장 경로(PC의 Saved/SaveGames/, 콘솔의 시스템 스토리지 등)도 엔진이 자동으로 처리합니다.
10. 유니티 엔진과의 비교
유니티에서 게임 데이터를 저장하는 가장 일반적인 방법은 PlayerPrefs와 JsonUtility입니다.
// 유니티 PlayerPrefs 방식
PlayerPrefs.SetInt("PlayerHealth", 100);
PlayerPrefs.SetFloat("PosX", transform.position.x);
PlayerPrefs.Save();
// 불러오기
int health = PlayerPrefs.GetInt("PlayerHealth", 100);
PlayerPrefs는 키-값 쌍으로 단순 데이터를 저장하기에는 간편하지만, 복잡한 구조체나 배열 데이터를 다루기 어렵고, 저장 경로가 레지스트리(Windows)나 plist(macOS, iOS) 같은 시스템 종속적인 위치에 기록됩니다.
더 복잡한 데이터는 JsonUtility로 직렬화한 뒤 File.WriteAllText로 파일에 쓰는 방식을 사용하지만, 이 역시 직렬화 대상 필드를 [System.Serializable] 어트리뷰트로 수동 관리해야 하며, 파일 입출력 코드를 직접 작성해야 합니다.
언리얼의 USaveGame 시스템은 유니티와 비교했을 때 다음과 같은 강점이 있습니다. UPROPERTY(SaveGame) 지정자로 저장 대상을 코드 레벨에서 명확히 선언할 수 있고, FObjectAndNameAsStringProxyArchive를 통해 오브젝트 포인터 직렬화까지 기본 지원합니다. 또한 비동기 저장/로드 API가 엔진 레벨에서 제공되므로 스레딩 코드를 직접 작성할 필요가 없습니다.
11. 주의사항
11-1. 슬롯 이름 관리
슬롯 이름은 저장 파일의 파일명이 됩니다. 하드코딩된 문자열을 코드 곳곳에 흩어놓으면 오타나 불일치로 인해 세이브 파일을 찾지 못하는 버그가 발생합니다. 슬롯 이름은 반드시 한 곳에서 관리하고 상수 또는 설정 값으로 참조하도록 설계하셔야 합니다.
// 권장: 상수로 중앙 관리
namespace SaveGameConstants
{
static const FString DefaultSlot = TEXT("SaveSlot_Default");
static const FString AutoSaveSlot = TEXT("SaveSlot_Auto");
static const FString QuickSaveSlot = TEXT("SaveSlot_Quick");
}
// 권장하지 않는 방식: 문자열 하드코딩 분산
UGameplayStatics::SaveGameToSlot(SaveGame, TEXT("MySave"), 0); // A.cpp
UGameplayStatics::LoadGameFromSlot(TEXT("MySave_Default"), 0); // B.cpp (오타)
11-2. 동기 저장 중 프레임 드랍
SaveGameToSlot은 메인 스레드를 블로킹합니다. 저장 데이터가 수십 KB를 넘거나 콘솔 플랫폼처럼 I/O 지연이 큰 환경에서는 눈에 띄는 히칭(hitching)이 발생할 수 있습니다. 체크포인트 통과나 레벨 전환처럼 자연스러운 멈춤이 허용되는 시점에는 동기 저장을 쓰더라도 무방하지만, 인게임 중 주기적인 자동저장에는 반드시 AsyncSaveGameToSlot을 사용하셔야 합니다.
11-3. 비동기 저장 중 데이터 변경
AsyncSaveGameToSlot은 직렬화를 게임 스레드에서 수행하지만, 파일 쓰기는 워커 스레드에서 이루어집니다. 저장 콜백이 완료되기 전에 CurrentSaveGame 오브젝트를 수정하거나 다시 저장을 시도하면 데이터 불일치가 발생할 수 있습니다. 저장 중 상태를 bIsSaving 플래그로 추적하고, 저장이 완료된 후에만 재저장을 허용하도록 처리하셔야 합니다.
// 저장 중복 호출 방지 예시
void USaveGameSubsystem::SaveGameAsync()
{
if (bIsSaving)
{
UE_LOG(LogTemp, Warning, TEXT("이미 저장 중입니다. 요청을 무시합니다."));
return;
}
bIsSaving = true;
FAsyncSaveGameToSlotDelegate Delegate;
Delegate.BindLambda([this](const FString& Slot, int32 Index, bool bSuccess)
{
bIsSaving = false; // 완료 후 플래그 해제
});
UGameplayStatics::AsyncSaveGameToSlot(CurrentSaveGame, SaveSlotName, UserIndex, Delegate);
}
11-4. 액터 이름 기반 식별의 한계
Actor->GetFName()을 세이브 데이터의 식별자로 사용하는 방식은 레벨에 사전 배치된 액터에는 적합합니다. 그러나 런타임에 동적으로 스폰된 액터는 매 실행마다 이름이 달라질 수 있습니다. 동적 스폰 액터를 저장해야 한다면 FGuid를 SaveGame UPROPERTY로 선언하고, BeginPlay에서 고유 ID를 생성하여 식별자로 사용하는 방식을 권장합니다.
11-5. UObject 포인터 직렬화
USaveGame 멤버 변수에 다른 UObject*를 직접 저장하면 포인터 주소 자체가 직렬화되어 재시작 후 무효한 주소를 참조하게 됩니다. 오브젝트 참조를 저장해야 한다면, 에셋 경로(FSoftObjectPath) 또는 고유 ID로 변환하여 저장하고 로드 시 다시 검색하는 방식을 사용하셔야 합니다. FObjectAndNameAsStringProxyArchive를 사용하는 경우에는 오브젝트 참조를 문자열 경로로 변환하여 이 문제를 완화할 수 있습니다.
11-6. 세이브 파일 저장 경로
에디터에서 테스트할 때 세이브 파일은 [프로젝트 루트]/Saved/SaveGames/ 폴더에 .sav 확장자로 생성됩니다. 패키징된 빌드에서는 플랫폼에 따라 경로가 달라집니다. Windows의 경우 %AppData%\Local\[게임명]\Saved\SaveGames\에 저장되며, 콘솔 플랫폼은 각 시스템의 사용자 데이터 영역을 사용합니다. 세이브 파일을 직접 조작하거나 디버깅할 때는 이 경로를 확인하셔야 합니다.
USaveGame 시스템은 언리얼 엔진이 직렬화의 복잡한 부분을 상당 부분 추상화해주는 강력한 도구입니다. 단순한 플레이어 진행 상황 저장부터 레벨 전체 오브젝트 상태 복원까지 폭넓게 활용할 수 있으며, UGameInstanceSubsystem과 결합하면 레벨 전환에도 안전한 세이브 관리 시스템을 구축할 수 있습니다. 인게임 자동저장이나 대용량 데이터 처리가 필요한 경우에는 반드시 비동기 API를 사용하여 프레임 드랍 없는 안정적인 사용자 경험을 제공하시기 바랍니다.
'언리얼 엔진 5' 카테고리의 다른 글
| [UE5] UPrimitiveComponent 의 바운딩 박스 (1) | 2026.03.18 |
|---|---|
| [UE5] 언리얼 인터페이스(UInterface) (0) | 2026.03.15 |
| [UE5] Enhanced Input System (0) | 2026.03.09 |
| [UE5] GPU 최적화 (0) | 2026.03.08 |
| [UE5] Render Thread 최적화 (0) | 2026.03.05 |