'게임 저장'의 의미는 게임에 따라 크게 다를 수 있지만, 플레이어가 게임을 종료했다가 중단한 시점에서부터 나중에 다시 시작할 수 있도록 만드는 일반적인 개념은 대부분의 최신 게임에 적용되어 있습니다. 제작하려는 게임의 타입에 따라 플레이어가 도달한 마지막 체크포인트나 플레이어가 발견한 아이템 등 몇 가지 기본적인 정보만 필요할 수도 있습니다. 또는 다른 인게임 캐릭터와 플레이어의 소셜 상호작용에 대한 상세 목록이나 다양한 퀘스트, 미션 목표, 서브플롯의 현재 상태 등 훨씬 더 자세한 정보가 필요할 수도 있습니다. 언리얼 엔진 4(UE4)는 여러 플레이 세션 전반에서 유지해야 하는 모든 정보를 포함하여 게임에 필요한 사항을 충족하기 위해 생성하는 하나 이상의 커스텀 SaveGame 클래스를 중심으로 작동하는 저장 및 로딩 시스템을 제공합니다. 이 시스템은 저장된 게임 파일을 여러 개 유지하고 해당 파일에 서로 다른 SaveGame 클래스를 저장할 수 있도록 지원합니다. 이는 전역으로 잠금해제된 기능을 특정 플레이별 게임 데이터에서 분리하는 데 유용합니다.
SaveGame 오브젝트 생성
새 SaveGame 오브젝트를 생성하려면, 새 블루프린트 클래스 생성합니다. 부모 클래스 선택(Pick Parent Class) 대화창이 표시되면, 커스텀 클래스(Custom Classes) 드롭다운을 펼치고 SaveGame 을 선택합니다. 검색창을 사용하여 바로 SaveGame으로 넘어갈 수도 있습니다. 새 블루프린트를 MySaveGame으로 명명합니다.
새 SaveGame 오브젝트 블루프린트에서 저장하려는 정보의 변수를 생성합니다.
이 예시에는 SaveSlotName과 UserIndex의 디폴트값을 저장하는 데 사용할 변수도 선언되어 있으므로 이 SaveGame 오브젝트에 저장하는 각 클래스가 따로 이 변수들을 설정하지 않아도 됩니다. 이 단계는 선택사항이며, 디폴트값을 변경하지 않으면 하나의 세이브 슬롯을 덮어쓰게 됩니다.
블루프린트가 컴파일된 후 변수의 디폴트값을 설정할 수 있습니다.
SaveGame 오브젝트 생성
USaveGame
클래스가 Kismet/GameplayStatics.h
에 선언된 저장 및 로딩 함수의 타깃으로 사용할 수 있는 오브젝트를 설정합니다.
C++ 클래스 마법사을 사용해 USaveGame
기반의 새 클래스를 생성할 수 있습니다.
이 예시에서 새 USaveGame
클래스의 이름은 UMySaveGame
입니다. 사용하려면 게임 모듈의 헤더 파일에서 다른 #include
지시문 뒤에 다음 줄을 추가합니다.
MyProject.h
#include "MySaveGame.h"
#include "Kismet/GameplayStatics.h"
헤더
SaveGame
오브젝트의 헤더 파일에서 SaveGame
에 저장할 모든 변수를 선언할 수 있습니다.
UPROPERTY(VisibleAnywhere, Category = Basic)
FString PlayerName;
이 예시에는 SaveSlotName
과 UserIndex
의 디폴트값을 저장하는 데 사용할 변수가 선언되어 있어
이 SaveGame
오브젝트에 저장하는 각 클래스가 따로 이 변수들을 설정하지 않아도 됩니다. 이 단계는 선택사항이며, 디폴트값을 변경하지 않으면 하나의 세이브 슬롯을 덮어쓰게 됩니다.
MySaveGame.h
#pragma once
#include "GameFramework/SaveGame.h"
#include "MySaveGame.generated.h"
/**
*
*/
UCLASS()
class [PROJECTNAME]_API UMySaveGame : public USaveGame
{
GENERATED_BODY()
public:
UPROPERTY(VisibleAnywhere, Category = Basic)
FString PlayerName;
UPROPERTY(VisibleAnywhere, Category = Basic)
FString SaveSlotName;
UPROPERTY(VisibleAnywhere, Category = Basic)
uint32 UserIndex;
UMySaveGame();
};
소스
일반적으로 특정한 저장 시스템에 추가 기능을 설정하려는 것이 아니라면 SaveGame
오브젝트의 소스 파일에는 특별히 코드가 필요하지
않습니다.
이 예시에서는 다른 게임플레이 클래스에서 읽고 사용할 수 있도록 클래스 생성자에서 SaveSlotName
과 UserIndex
의 값을 정의합니다.
MySaveGame.cpp
// Copyright 1998-2018 Epic Games, Inc. All Rights Reserved.
#include "[ProjectName].h"
#include "MySaveGame.h"
UMySaveGame::UMySaveGame()
{
SaveSlotName = TEXT("TestSaveSlot");
UserIndex = 0;
}
게임 저장
SaveGame 클래스를 생성한 후, 게임 데이터를 저장하기 위한 변수로 채울 수 있습니다. 예를 들어, 플레이어의 점수를 저장하기 위한 integer 변수를 생성하거나 플레이어의 이름을 저장하기 위한 string 변수를 생성할 수 있습니다. 게임을 저장할 때, 현재 게임 월드의 정보를 SaveGame 오브젝트로 전송하고, 게임을 로드할 때는 SaveGame 오브젝트에서 캐릭터, 플레이어 컨트롤러, 게임 모드 등의 게임 오브젝트로 정보를 복사합니다.
먼저, Create Save Game Object 노드를 사용하여 SaveGame 클래스를 기반으로 오브젝트 하나를 생성합니다. Save Game Class 입력 핀의 드롭다운을 생성한 클래스로 설정해야 합니다. 이 예시에서는 MySaveGame 입니다. Create Save Game Object 노드는 자동으로 출력 핀 타입을 변경해 Save Game Class 입력 핀으로 지정한 타입에 일치시킵니다. 이렇게 하면 Cast To 노드 없이 바로 사용할 수 있습니다. 방금 생성한 오브젝트를 나중에 쉽게 재사용할 수 있도록 변수로 승격(Promote to Variable) 을 사용하여 결과 오브젝트를 변수에 저장하는 것이 좋습니다.
Save Game Instance 가 커스텀 SaveGame 오브젝트를 가지고 있으므로 이 오브젝트로 정보를 전송할 수 있습니다. 예를 들어, 플레이어 이름(Player Name) 필드를 'PlayerOne'으로 설정할 수 있습니다. 저장된 게임 파일에 저장할 모든 데이터가 담길 때까지 SaveGame 오브젝트의 필드를 계속 설정합니다.
SaveGame 오브젝트가 모두 채워지면 ASync Save Game To Slot 노드를 사용해 게임 저장을 완료합니다. 파일 이름과 사용자 ID도 제공해야 합니다. 이 예시에서는 이전에 생성한 디폴트값을 파일 이름과 사용자 ID로 사용합니다. 즉시 맨 위 핀부터 실행되고, 게임 저장 작업이 완료되면 이어서 두 번째 핀부터 실행됩니다. 출력 핀은 두 번째 핀이 실행될 때까지 유효하지 않습니다.
Async Save Game To Slot 을 사용하면 대량의 데이터를 저장해도 멈춤 현상을 방지할 수 있으므로 게임을 저장하는 데 권장되는 방법입니다. 그러나 게임 저장 데이터가 작거나 메뉴 또는 일시정지 화면에서 저장하는 경우, 아래에 표시된 Save Game To Slot 노드를 대신 사용하여 게임을 저장할 수 있습니다.
다음 스크린샷에서는 MySaveGame 클래스를 사용해 게임을 저장하는 전체 블루프린트 프로세스를 보여줍니다.
위 이미지를 클릭하면 확대됩니다.
먼저, UGameplayStatics
라이브러리의 CreateSaveGameObject
를 호출하여 새 UMySaveGame
오브젝트를 가져옵니다. 오브젝트를 가져오면 저장할 데이터로 해당 오브젝트를 채울 수 있습니다. 마지막으로, SaveGameToSlot
또는 AsyncSaveGameToSlot
을 호출하여 디바이스에 데이터를 작성합니다.
비동기식 저장
AsyncSaveGameToSlot
을 사용하여 게임을 저장하는 것이 좋습니다. 비동기식 실행 시 갑작스러운 프레임 레이트 멈춤 현상을 방지하며 플레이어의 눈에 덜 띄도록 만들 수 있고, 특정 플랫폼에서의 인증 문제를 방지할 수 있습니다. 저장 프로세스가 완료되면 슬롯 이름, 사용자 인덱스, 성공 또는 실패를 나타내는 부울
과 함께 FAsyncSaveGameToSlotDelegate
타입의 델리게이트가 호출됩니다.
if (UMySaveGame* SaveGameInstance = Cast<UMySaveGame>(UGameplayStatics::CreateSaveGameObject(UMySaveGame::StaticClass())))
{
// (선택적) 델리게이트를 설정합니다.
FAsyncSaveGameToSlotDelegate SavedDelegate;
// USomeUObjectClass::SaveGameDelegateFunction은 const FString& SlotName, const int32 UserIndex, bool bSuccess 파라미터를 갖는 void 함수입니다.
SavedDelegate.BindUObject(SomeUObjectPointer, &USomeUObjectClass::SaveGameDelegateFunction);
// SaveGame 오브젝트에 데이터를 설정합니다.
SaveGameInstance->PlayerName = TEXT("PlayerOne");
// 비동기 저장 프로세스를 시작합니다.
UGameplayStatics::AsyncSaveGameToSlot(SaveGameInstance, SlotNameString, UserIndexInt32, SavedDelegate);
}
동기식 저장
작은 SaveGame 포맷이나 일시정지 상태 또는 메뉴에서의 게임 저장에는 SaveGameToSlot
이면 충분합니다. 즉시 게임을 저장하고 성공 또는 실패를 나타내는 부울
을 반환하므로 간단히 사용할 수 있습니다. 대량의 데이터를 사용하는 경우 또는 플레이어가 게임 월드와 활발하게 상호작용하고 있는 도중에 게임을 자동 저장하는 경우에는 AsyncSaveGameToSlot
을 사용하는 것이 낫습니다.
if (UMySaveGame* SaveGameInstance = Cast<UMySaveGame>(UGameplayStatics::CreateSaveGameObject(UMySaveGame::StaticClass())))
{
// SaveGame 오브젝트에 데이터를 설정합니다.
SaveGameInstance->PlayerName = TEXT("PlayerOne");
// 즉시 데이터를 저장합니다.
if (UGameplayStatics::SaveGameToSlot(SaveGameInstance, SlotNameString, UserIndexInt32))
{
// 저장에 성공했습니다.
}
}
바이너리 저장
SaveGameToMemory
함수를 사용해 SaveGame 오브젝트를 메모리로 전송할 수 있습니다. 이 함수는 동기식 작업만 제공하지만, 드라이브에 저장하는 것보다 빠릅니다. 호출자는 데이터가 저장될 TArray<uint8>&
버퍼에 대한 레퍼런스를 제공합니다. 성공 시, 함수가 true를 반환합니다.
TArray<uint8> OutSaveData;
if (UGameplayStatics::SaveGameToMemory(SaveGameObject, OutSaveData))
{
// 작업이 성공했으며, 이제 SaveGame 오브젝트의 바이너리 표현이 OutSaveData에 저장되어 있습니다.
}
SaveGameToSlot
함수와 유사하게, const TArray<uint8>&
버퍼, 슬롯 이름, 사용자 ID 정보로 SaveDataToSlot
을 호출하여 바이너리 데이터를 파일로 직접 저장할 수도 있습니다. SaveGameToMemory
와 마찬가지로, 이 함수는 동기식 작업만 제공하며, 성공 또는 실패를 나타내는 부울
을 반환합니다.
if (UGameplayStatics::SaveDataToSlot(InSaveData, SlotNameString, UserIndexInt32))
{
// 작업이 성공했으며, 제공한 슬롯 이름과 사용자 ID에 의해 정의된 저장 파일에 InSaveData가 작성되었습니다.
}
개발 플랫폼에서 저장된 게임 파일의 확장자는 .sav
이며 프로젝트의 Saved\SaveGames
폴더에 표시됩니다. 다른 플랫폼, 특히 콘솔에서는 해당 파일 시스템에 따라 달라집니다.
게임 로딩
저장된 게임을 로드하려면 게임을 저장할 때 사용했던 세이브 슬롯 이름과 사용자 ID를 제공해야 합니다. 지정한 SaveGame이 존재하는 경우, 엔진이 여기에 포함된 데이터로 SaveGame 오브젝트를 채우고 베이스 SaveGame(USaveGame
클래스) 오브젝트로 반환합니다. 그런 다음 해당 오브젝트를 커스텀 SaveGame 클래스로 형변환하고 데이터에 액세스할 수 있습니다. SaveGame 타입에 포함된 데이터의 종류에 따라 오브젝트의 사본을 유지할 수도 있고, 아니면 데이터만 사용하고 오브젝트를 버릴 수도 있습니다.
저장과 마찬가지로, 동기식 또는 비동기식 로드도 수행할 수 있습니다. 데이터의 양이 많거나, 로딩 시간 동안 로딩 화면 또는 애니메이션을 사용하고 싶은 경우, 비동기 방식을 권장합니다. 로딩이 빠른 소량의 데이터에는 동기식 방식을 사용할 수 있습니다.
동기식으로 로드하려면 Load Game From Slot 을 사용합니다. 이 노드는 간단히 사용할 수 있으며 제공한 슬롯 이름과 사용자 ID가 유효한 SaveGame 파일을 식별하는 경우, 유효한 SaveGame 오브젝트를 반환합니다. 로드 작업이 진행되는 동안에는 게임이 중단됩니다.
Async Load Game From Slot 도 거의 동일한 방식으로 작동하지만, 실행 출력 핀이 두 개입니다. 첫 번째 핀은 로드 작업이 시작될 때 실행되며, 두 번째 핀은 작업이 완료되면 실행됩니다. 변수 출력 핀은 두 번째 핀이 실행될 때까지 유효하지 않습니다. Success 핀은 로드 작업이 성공했는지 여부를 나타내지만, 반환된 오브젝트를 Is Valid 노드로 전달하거나 로드 프로세스에서 발생하는 모든 오류에 대해 Cast To 노드의 실패를 캐치 올로 처리할 수도 있습니다.
비동기식 로딩
AsyncLoadGameFromSlot
을 사용하여 비동기식으로 로딩할 경우, 시스템이 로드하는 데이터를 수신하기 위해 콜백 델리게이트를 제공해야 합니다.
// 델리게이트를 설정합니다.
FAsyncLoadGameFromSlotDelegate LoadedDelegate;
// USomeUObjectClass::LoadGameDelegateFunction은 const FString& SlotName, const int32 UserIndex, USaveGame* LoadedGameData 파라미터를 갖는 void 함수입니다.
LoadedDelegate.BindUObject(SomeUObjectPointer, &USomeUObjectClass::LoadGameDelegateFunction);
UGameplayStatics::AsyncLoadGameFromSlot(SlotName, 0, LoadedDelegate);
동기식 로딩
LoadGameFromSlot
함수는 성공하면 USaveGame
오브젝트를 생성하고 반환합니다.
// USaveGame 오브젝트를 얻고 UMySaveGame으로 형변환합니다.
if (UMySaveGame* LoadedGame = Cast<UMySaveGame>(UGameplayStatics::LoadGameFromSlot(SlotName, 0)))
{
// 작업이 성공했으며, 이전에 저장한 데이터가 이제 LoadedGame에 포함되어 있습니다.
UE_LOG(LogTemp, Warning, TEXT("LOADED: %s"), *LoadedGame->PlayerName);
}
바이너리 로딩
LoadDataFromSlot
을 사용해 바이너리 raw 포맷의 파일에서 SaveGame 데이터를 로드할 수 있습니다. 이 함수는 LoadGameFromSlot
과 매우 유사하지만, SaveGame 오브젝트를 생성하지 않습니다. 이 로딩 타입에는 동기식 작업만 사용할 수 있습니다.
TArray<uint8> OutSaveData;
if (UGameplayStatics::LoadDataFromSlot(OutSaveData, SlotNameString, UserIndexInt32))
{
// 작업이 성공했으며, 이제 SaveGame 오브젝트의 바이너리 표현이 OutSaveData에 저장되어 있습니다.
}
LoadGameFromMemory
를 호출해 이 바이너리 데이터를 SaveGame 오브젝트로 변환할 수도 있습니다. 이는 동기식 호출이며, 성공 즉시 새 USaveGame
오브젝트를 반환하고 실패 시 null 포인터를 반환합니다.
if (UMySaveGame* SaveGameInstance = Cast<UMySaveGame>(LoadGameFromMemory(InSaveData)))
{
// 작업이 성공했으며, SaveGameInstance가 예상했던 타입(UMySaveGame)으로 형변환될 수 있었습니다.
}