세이브 & 로드 시스템
USaveGame을 사용한 세이브 & 로드 구조
개요
- 스테이지, 최근 저장 위치, 인벤토리 정보를 관리하기 위한 세이브 & 로드 시스템의 설계 및 구현 과정을 설명한다.
설계
세이브 & 로드 시스템
세이브 & 로드 시스템은 대부분의 게임에서 기본적으로 제공되는 기능이다. 게임에 따라 저장해야 할 데이터와 불러와야 할 데이터의 종류는 다양하다.
이번에 만든 것은 조건 확인 시스템에 사용되는 스테이지 정보와 인벤토리 내부의 아이템 정보, 그리고 리스폰 장소를 관리하는 세이브 & 로드 시스템이다.
로드는 게임 시작 시점이나 사망 후 리스폰 시 작동하며, 세이브는 세이브 포인트와 상호작용 시 작동하도록 설계했다.
언리얼 엔진의 세이브 시스템 - USaveGame
언리얼 엔진은 USaveGame
클래스를 활용한 세이브 기능을 제공한다. 해당 클래스를 이해하기 위한 핵심 사항은 다음과 같다.
USaveGame
은 데이터 컨테이너의 역할을,UGamePlayStatics
는 실질적인 세이브 & 로드 동작을 수행한다.- 로컬 플레이어 별 세이브 데이터 관리를 위하여,
USaveGame
을 상속받은ULocalPlayerSaveGame
이 존재한다. - 세이브 시스템은 Create, Save, Load의 3가지 동작이 중심이 된다.
-
슬롯은 아래 Batman : Arkham City 세이브 화면에 보이는 것 처럼 같이 세이브 데이터를 구분하는 이름이다.
세이브 과정
- 각 요소는 본인이 준비되었을 때, 세이브 & 로드 서브시스템의 대리자에 세이브 함수를 등록한다.
- 이 대리자는
IVSaveGame*
을 인자로 전달한다.
- 이 대리자는
- 플레이어가 세이브 포인트와 상호작용하면, 세이브 포인트는 서브시스템의 전체 저장 함수를 호출한다.
- 세이브 & 로드 서브시스템의 대리자가 실행되며 각 요소에서의 데이터 기록이 진행된다.
GameState
는 스테이지 정보를 기록한다.InventoryComponent
는 보유한 4개 인벤토리 슬롯의 모든 아이템 정보를 기록한다.- 세이브 포인트는 상호작용한 플레이어의 위치를 세이브 파일에 기록한다.
- 기록이 완료되었다면, 해당
IVSaveGame
파일을 슬롯에 저장한다.
로드 작업
로드는 세이브와 달리 일괄적으로 처리되지 않고, 각 요소가 필요한 시점에 개별적으로 데이터를 가져간다.
- 세이브 & 로드 서브시스템은 초기화 시점에
IVSaveGame
을 로드한다.- 각 요소들은 이 서브시스템의 Get함수들을 통해 필요한 데이터를 사용할 수 있다.
GameState
는BeginPlay()
에서 스테이지 정보를 가져온다.GameMode
는 플레이어 리스폰 시, 마지막으로 저장된 위치를 가져와 해당 위치에 새 캐릭터를 스폰한다.InventoryComponent
는BeginPlay()
에서 인벤토리 정보를 가져온다.- 이 컴포넌트는 리스폰 마다 새로 생성되므로, 항상 초기화가 완료된 이후 데이터 로드가 필요하다.
- 공용 슬롯은 데이터 복사만 하면 되지만, 방어구, 무기는 장착, 퀵슬롯은 퀵슬롯 위젯 업데이트 과정이 추가로 필요하다.
- HUD에 장착된
InventoryWidget
과QuickSlotWidget
은 캐릭터와 인벤토리가 모두 준비된 이후, 새로 바인딩을 진행하고 로드된 데이터를 반영한다.
구현
IVSaveGame
게임의 데이터를 저장하는 IVSaveGame
클래스이다. 해당 데이터들에는 UPROPERTY()를 붙여야한다. 생성자에서 기본 데이터를 지정할 수 있으며, 생성자의 데이터는 저장된 데이터를 덮어쓰지 않는다.
// UIVSaveGame.h
UCLASS()
class IVAN_API UIVSaveGame : public USaveGame
{
GENERATED_BODY()
public:
/* 생성자 */
UIVSaveGame();
/* 스테이지 상태 */
UPROPERTY(VisibleAnywhere, Category = "Stage")
EStageState SavedStageState;
/* 플레이어 위치 */
UPROPERTY(VisibleAnywhere, Category = "Transform")
FTransform SavedTransform;
/* 유저 인벤토리 */
UPROPERTY(VisibleAnywhere, Category = "Transform")
TArray<FItemBaseInfo> SavedInventorySlots;
/* 퀵 슬롯 */
UPROPERTY(VisibleAnywhere, Category = "Transform")
TArray<FItemBaseInfo> SavedQuickSlots;
/* 장비 슬롯 */
UPROPERTY(VisibleAnywhere, Category = "Transform")
TArray<FItemBaseInfo> SavedEquipSlots;
/* 무기 슬롯 */
UPROPERTY(VisibleAnywhere, Category = "Transform")
TArray<FItemBaseInfo> SavedWeaponSlots;
};
세이브 & 로드 매니저(IVSaveManagerSubsystem)
앞서 IVSaveGame
은 데이터 컨테이너처럼 사용된다고 말했었다. 이 세이브 & 로드 매니저는 초기화 단계에서 세이브 파일을 로드하거나 생성하고, 다른 요소들에게 해당 데이터를 제공하는 역할을 수행한다.
// UIVSaveManagerSubsystem.cpp
void UIVSaveManagerSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
// 게임 저장 파일 초기화
USaveGame* SaveGame = UGameplayStatics::LoadGameFromSlot(TEXT("DefaultSlot"), 0);
if (SaveGame)
{
SaveGameFile = Cast<UIVSaveGame>(SaveGame); // 세이브 파일이 존재하면 로드
}
else
{
SaveGameFile = Cast<UIVSaveGame>(UGameplayStatics::CreateSaveGameObject(UIVSaveGame::StaticClass())); // 세이브 파일이 존재하지 않으면 새로 생성
}
}
void UIVSaveManagerSubsystem::RequestSaveGame()
{
if (SaveGameFile)
{
OnSaveRequested.Broadcast(SaveGameFile); // 각 요소들이 직접 데이터를 덮어쓰도록 요청한다
UGameplayStatics::SaveGameToSlot(SaveGameFile, TEXT("DefaultSlot"), 0); // 이후 실제로 저장
}
}
매니저는 IVSaveGame*
을 전달하는 세이브 대리자를 가진다. 대리자 호출 시, 이를 구독하는 각 요소들은 본인 클래스 내부에서 세이브 파일에 데이터를 기록한다.
매니저는 GameInstanceSubsystem
기반이기에 세이브 데이터를 필요로 하는 다른 요소들보다 먼저 생성되며, 안정적으로 데이터를 제공할 수 있다.