クラウドセーブに OSS を使用する - クラウドセーブ - (Unreal Engine モジュール)
Unwrap the Subsystem
In this tutorial, you will learn how to implement AccelByte Gaming Services (AGS) Cloud Save to save and fetch game options for music and sound effects (SFX) volumes. In Byte Wars, there is a Game Instance Subsystem defined in the CloudSaveSubsystem
class. This subsystem acts as a wrapper to set and get any Player Record from AGS Cloud Save. Player Record is one of the record types that are available in AGS.
Here is the definition of record types supported by AGS:
- Player Record: a record type to save player data, such as game options and preferences.
- Game Record: a record type to save game data, such as game announcements, event configurations, etc.
You will set up a subsystem called CloudSaveSubsystem_Starter
, which is the starter version of the CloudSaveSubsystem
class.
What's in the Starter Pack
The CloudSaveSubsystem_Starter
is available in the Resources section and consists of the following:
- Header file:
/Source/AccelByteWars/TutorialModules/Storage/CloudSaveEssentials/CloudSaveSubsystem_Starter.h
- CPP file:
/Source/AccelByteWars/TutorialModules/Storage/CloudSaveEssentials/CloudSaveSubsystem_Starter.cpp
The CloudSaveSubsystem_Starter
class has several functionalities provided. If you open the CloudSaveSubsystem_Starter
class CPP file, you will find a function called Initialize()
. In this function is a defined variable called CloudSaveInterface
.
void UCloudSaveSubsystem_Starter::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
// Get Online Subsystem and make sure it's valid.
const FOnlineSubsystemAccelByte* Subsystem = static_cast<const FOnlineSubsystemAccelByte*>(Online::GetSubsystem(GetWorld()));
if (!ensure(Subsystem))
{
UE_LOG_CLOUDSAVE_ESSENTIALS(Warning, TEXT("The online subsystem is invalid. Please make sure OnlineSubsystemAccelByte is enabled and DefaultPlatformService under [OnlineSubsystem] in the Engine.ini set to AccelByte."));
return;
}
// Grab the reference of AccelByte Identity Interface and make sure it's valid.
CloudSaveInterface = StaticCastSharedPtr<FOnlineCloudSaveAccelByte>(Subsystem->GetCloudSaveInterface());
if (!ensure(CloudSaveInterface.IsValid()))
{
UE_LOG_CLOUDSAVE_ESSENTIALS(Warning, TEXT("Cloud Save interface is not valid."));
return;
}
// ...
}
The CloudSaveInterface
is a variable of type FOnlineCloudSaveAccelBytePtr
interface. The FOnlineCloudSaveAccelBytePtr
is part of AGS Online Subsystem (OSS), and you can use this interface to access AGS Cloud Save functionalities.
Implement Cloud Save with AGS OSS
In this section, you will implement Cloud Save using AGS OSS to store game options to and fetch game options from a Player Record.
Open the
CloudSaveSubsystem_Starter
class Header file and declare the functions below. Each of these functions correspond to an action performed on a Player Record.SetPlayerRecord()
will create or update an existing record.GetPlayerRecord()
will retrieve the data associated with the record key provided.DeletePlayerRecord()
will remove the stored record. You also declare corresponding response functions for each action.public:
void SetPlayerRecord(
const APlayerController* PlayerController,
const FString& RecordKey,
const FJsonObject& RecordData,
const FOnSetCloudSaveRecordComplete& OnSetRecordComplete);
void GetPlayerRecord(
const APlayerController* PlayerController,
const FString& RecordKey,
const FOnGetCloudSaveRecordComplete& OnGetRecordComplete);
void DeletePlayerRecord(
const APlayerController* PlayerController,
const FString& RecordKey,
const FOnDeleteCloudSaveRecordComplete& OnDeleteRecordComplete);private:
// ...
void OnSetPlayerRecordComplete(
int32 LocalUserNum,
const FOnlineError& Result,
const FString& Key);
void OnGetPlayerRecordComplete(
int32 LocalUserNum,
const FOnlineError& Result,
const FString& Key,
const FAccelByteModelsUserRecord& UserRecord);
void OnDeletePlayerRecordComplete(
int32 LocalUserNum,
const FOnlineError& Result,
const FString& Key);Create the function definitions starting with
SetPlayerRecord()
. Open theCloudSaveSubsystem_Starter
class CPP file and add the code below. This will send a request to set the Player Record data with the JSON data provided under the record key provided. When the request completes, theOnSetPlayerRecordComplete()
method will be called.void UCloudSaveSubsystem_Starter::SetPlayerRecord(
const APlayerController* PlayerController,
const FString& RecordKey,
const FJsonObject& RecordData,
const FOnSetCloudSaveRecordComplete& OnSetRecordComplete)
{
if (!ensure(CloudSaveInterface.IsValid()))
{
UE_LOG_CLOUDSAVE_ESSENTIALS(Warning, TEXT("Cloud Save interface is not valid."));
return;
}
const int32 LocalUserNum = GetLocalUserIndex(PlayerController);
SetPlayerRecordParams.Add(RecordKey, OnSetRecordComplete);
CloudSaveInterface->ReplaceUserRecord(LocalUserNum, RecordKey, RecordData);
}Define the
OnSetPlayerRecordComplete()
function. In the CPP file, add the code below. This will execute theOnSetRecordComplete
delegate. This delegate is used to inform the player if the record was saved successfully, or if there was an error. You will use this delegate later.void UCloudSaveSubsystem_Starter::OnSetPlayerRecordComplete(
int32 LocalUserNum,
const FOnlineError& Result,
const FString& Key)
{
if (Result.bSucceeded)
{
UE_LOG_CLOUDSAVE_ESSENTIALS(Log, TEXT("Success to set player record."));
}
else
{
UE_LOG_CLOUDSAVE_ESSENTIALS(Log, TEXT("Failed to set player record. Message: %s"), *Result.ErrorMessage.ToString());
}
for (TTuple<FString, FOnSetCloudSaveRecordComplete>& Param : SetPlayerRecordParams)
{
if (Param.Key.Equals(Key))
{
Param.Value.ExecuteIfBound(Result.bSucceeded);
}
}
SetPlayerRecordParams.Remove(Key);
}Create the function definition for the
GetPlayerRecord()
with the code below. This will send a request to retrieve the stored JSON data for the Player Record under the record key provided. When the request completes, theOnGetPlayerRecordComplete()
will be called.void UCloudSaveSubsystem_Starter::GetPlayerRecord(
const APlayerController* PlayerController,
const FString& RecordKey,
const FOnGetCloudSaveRecordComplete& OnGetRecordComplete)
{
if (!ensure(CloudSaveInterface.IsValid()))
{
UE_LOG_CLOUDSAVE_ESSENTIALS(Warning, TEXT("Cloud Save interface is not valid."));
return;
}
const int32 LocalUserNum = GetLocalUserIndex(PlayerController);
GetPlayerRecordParams.Add(RecordKey, OnGetRecordComplete);
CloudSaveInterface->GetUserRecord(LocalUserNum, RecordKey);
}Define the
OnGetPlayerRecordComplete()
function with the code below. Similar to theOnSetPlayerRecordComplete()
function, this will notify the player if the record retrieval was successful or not. Additionally, it will send the record data through the delegate for the game code to read and use. You will use this delegate later.void UCloudSaveSubsystem_Starter::OnGetPlayerRecordComplete(
int32 LocalUserNum,
const FOnlineError& Result,
const FString& Key,
const FAccelByteModelsUserRecord& UserRecord)
{
FJsonObject RecordResult;
if (Result.bSucceeded)
{
RecordResult = UserRecord.Value.JsonObject.ToSharedRef().Get();
UE_LOG_CLOUDSAVE_ESSENTIALS(Log, TEXT("Success to get player record."));
}
else
{
UE_LOG_CLOUDSAVE_ESSENTIALS(Log, TEXT("Failed to get player record. Message: %s"), *Result.ErrorMessage.ToString());
}
for (TTuple<FString, FOnGetCloudSaveRecordComplete>& Param : GetPlayerRecordParams)
{
if (Param.Key.Equals(Key))
{
Param.Value.ExecuteIfBound(Result.bSucceeded, RecordResult);
}
}
GetPlayerRecordParams.Remove(Key);
}Define the
DeletePlayerRecord()
function with the code below. This will send a request to delete the Player Record associated with the record key provided. When the request completes,OnDeletePlayerRecordComplete()
will be called.void UCloudSaveSubsystem_Starter::DeletePlayerRecord(
const APlayerController* PlayerController,
const FString& RecordKey,
const FOnDeleteCloudSaveRecordComplete& OnDeleteRecordComplete)
{
if (!ensure(CloudSaveInterface.IsValid()))
{
UE_LOG_CLOUDSAVE_ESSENTIALS(Warning, TEXT("Cloud Save interface is not valid."));
return;
}
const int32 LocalUserNum = GetLocalUserIndex(PlayerController);
DeletePlayerRecordParams.Add(RecordKey, OnDeleteRecordComplete);
CloudSaveInterface->DeleteUserRecord(LocalUserNum, RecordKey);
}Define the
OnDeletePlayerRecordComplete()
function with the code below. This will execute theOnDeleteRecordComplete
delegate to inform the player that the deletion was successful.void UCloudSaveSubsystem_Starter::OnDeletePlayerRecordComplete(
int32 LocalUserNum,
const FOnlineError& Result,
const FString& Key)
{
if (Result.bSucceeded)
{
UE_LOG_CLOUDSAVE_ESSENTIALS(Log, TEXT("Success to delete player record."));
}
else
{
UE_LOG_CLOUDSAVE_ESSENTIALS(Log, TEXT("Failed to delete player record. Message: %s"), *Result.ErrorMessage.ToString());
}
for (TTuple<FString, FOnDeleteCloudSaveRecordComplete>& Param : DeletePlayerRecordParams)
{
if (Param.Key.Equals(Key))
{
Param.Value.ExecuteIfBound(Result.bSucceeded);
}
}
DeletePlayerRecordParams.Remove(Key);
}Finally, you need to bind and unbind the callback functions when the subsystem is initialized or deinitialized. To do this, add the code below to the existing
BindDelegates
andUnbindDelegates
function.void UCloudSaveSubsystem_Starter::BindDelegates()
{
// ...
CloudSaveInterface->OnReplaceUserRecordCompletedDelegates->AddUObject(this, &ThisClass::OnSetPlayerRecordComplete);
CloudSaveInterface->OnGetUserRecordCompletedDelegates->AddUObject(this, &ThisClass::OnGetPlayerRecordComplete);
CloudSaveInterface->OnDeleteUserRecordCompletedDelegates->AddUObject(this, &ThisClass::OnDeletePlayerRecordComplete);
}void UCloudSaveSubsystem_Starter::UnbindDelegates()
{
// ...
CloudSaveInterface->OnReplaceUserRecordCompletedDelegates->RemoveAll(this);
CloudSaveInterface->OnGetUserRecordCompletedDelegates->RemoveAll(this);
CloudSaveInterface->OnDeleteUserRecordCompletedDelegates->RemoveAll(this);
}Build the Byte Wars project and make sure there are no compile errors.
Resources
- The files used in this tutorial section are available in the Byte Wars GitHub repository.