개요

  • 스테이지, 최근 저장 위치, 인벤토리 정보를 관리하기 위한 세이브 & 로드 시스템의 설계 및 구현 과정을 설명한다.

설계

세이브 & 로드 시스템

세이브 & 로드 시스템은 대부분의 게임에서 기본적으로 제공되는 기능이다. 게임에 따라 저장해야 할 데이터와 불러와야 할 데이터의 종류는 다양하다.

이번에 만든 것은 조건 확인 시스템에 사용되는 스테이지 정보와 인벤토리 내부의 아이템 정보, 그리고 리스폰 장소를 관리하는 세이브 & 로드 시스템이다.

로드는 게임 시작 시점이나 사망 후 리스폰 시 작동하며, 세이브는 세이브 포인트와 상호작용 시 작동하도록 설계했다.

언리얼 엔진의 세이브 시스템 - USaveGame

언리얼 엔진은 USaveGame클래스를 활용한 세이브 기능을 제공한다. 해당 클래스를 이해하기 위한 핵심 사항은 다음과 같다.

  • USaveGame은 데이터 컨테이너의 역할을, UGamePlayStatics는 실질적인 세이브 & 로드 동작을 수행한다.
  • 로컬 플레이어 별 세이브 데이터 관리를 위하여, USaveGame을 상속받은 ULocalPlayerSaveGame이 존재한다.
  • 세이브 시스템은 Create, Save, Load의 3가지 동작이 중심이 된다.
  • 슬롯은 아래 Batman : Arkham City 세이브 화면에 보이는 것 처럼 같이 세이브 데이터를 구분하는 이름이다.

    Untitled

세이브 과정

Untitled

  1. 각 요소는 본인이 준비되었을 때, 세이브 & 로드 서브시스템의 대리자에 세이브 함수를 등록한다.
    • 이 대리자는 IVSaveGame* 을 인자로 전달한다.
  2. 플레이어가 세이브 포인트와 상호작용하면, 세이브 포인트는 서브시스템의 전체 저장 함수를 호출한다.
  3. 세이브 & 로드 서브시스템의 대리자가 실행되며 각 요소에서의 데이터 기록이 진행된다.
    • GameState는 스테이지 정보를 기록한다.
    • InventoryComponent는 보유한 4개 인벤토리 슬롯의 모든 아이템 정보를 기록한다.
    • 세이브 포인트는 상호작용한 플레이어의 위치를 세이브 파일에 기록한다.
  4. 기록이 완료되었다면, 해당 IVSaveGame 파일을 슬롯에 저장한다.

로드 작업

Untitled

로드는 세이브와 달리 일괄적으로 처리되지 않고, 각 요소가 필요한 시점에 개별적으로 데이터를 가져간다.

  • 세이브 & 로드 서브시스템은 초기화 시점에 IVSaveGame을 로드한다.
    • 각 요소들은 이 서브시스템의 Get함수들을 통해 필요한 데이터를 사용할 수 있다.
  • GameStateBeginPlay()에서 스테이지 정보를 가져온다.
  • GameMode는 플레이어 리스폰 시, 마지막으로 저장된 위치를 가져와 해당 위치에 새 캐릭터를 스폰한다.
  • InventoryComponentBeginPlay()에서 인벤토리 정보를 가져온다.
    • 이 컴포넌트는 리스폰 마다 새로 생성되므로, 항상 초기화가 완료된 이후 데이터 로드가 필요하다.
    • 공용 슬롯은 데이터 복사만 하면 되지만, 방어구, 무기는 장착, 퀵슬롯은 퀵슬롯 위젯 업데이트 과정이 추가로 필요하다.
  • HUD에 장착된 InventoryWidgetQuickSlotWidget은 캐릭터와 인벤토리가 모두 준비된 이후, 새로 바인딩을 진행하고 로드된 데이터를 반영한다.

구현

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기반이기에 세이브 데이터를 필요로 하는 다른 요소들보다 먼저 생성되며, 안정적으로 데이터를 제공할 수 있다.

최종 결과