Unreal Engine Module - Store game settings in the cloud - Use the Online Subsystem to store game options
Unwrap the Subsystem
In this section, you will learn how to implement Cloud Save to save and fetch game options for music and SFX volumes. In the Byte Wars, there is a Game Instance Subsystem defined in the CloudSaveSubsystem
class. This subsystem act as a wrapper to set and get any Player Record from AccelByte Cloud Save. Player Record is one of the record types that available on AccelByte Cloud Save.
Here is the definition of record types supported on AccelByte Cloud Save:
- Player Record is a record type to save player data, such as game options and preferences.
- Game Record is a record type to save game data, such as game announcements, event configurations, etc.
In the next section, you will set up similar functionalities to the CloudSaveSubsystem
class. You will use the starter subsystem namely the CloudSaveSubsystem_Starter
class to configure those functionalities.
What's in the Starter Pack
As mentioned in the previous section, you will configure the CloudSaveSubsystem_Starter
subsystem class to store players' game options in the AccelByte Cloud Save. You can find the files for the CloudSaveSubsystem_Starter
in the following directories.
- Header file can be found in
/Source/AccelByteWars/TutorialModules/Storage/CloudSaveEssentials/CloudSaveSubsystem_Starter.h
. - CPP file can be found in
/Source/AccelByteWars/TutorialModules/Storage/CloudSaveEssentials/CloudSaveSubsystem_Starter.cpp
.
The CloudSaveSubsystem_Starter
class has several functionalities supplied for you. If you open the CloudSaveSubsystem_Starter
class CPP file, you will find a function called Initialize()
. In this function, we have defined a 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 the 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;
}
UTutorialModuleDataAsset* TutorialModule = UTutorialModuleUtility::GetTutorialModuleDataAsset(FPrimaryAssetId{ "TutorialModule:CLOUDSAVEESSENTIALS" }, this, true);
if (TutorialModule && TutorialModule->IsActiveAndDependenciesChecked() && TutorialModule->IsStarterMode())
{
BindDelegates();
}
}
The CloudSaveInterface
is a variable of type FOnlineCloudSaveAccelBytePtr
interface. The FOnlineCloudSaveAccelBytePtr
is part of AccelByte OSS you can use this interface to access AccelByte Cloud Save functionalities. In the next section, you will use the CloudSaveInterface
to set and get Player Record to store and fetch game options.
Implement Cloud Save with AccelByte OSS
In this section you will implement Cloud Save using AccelByte OSS to store and fetch game options from and to Player Record.
Open the
CloudSaveSubsystem_Starter
class header file and declare the following functions. 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. We also declared corresponding response function for each action.public:
void SetPlayerRecord(const APlayerController* PC, const FString& RecordKey, const FJsonObject& RecordData, const FOnSetCloudSaveRecordComplete& OnSetRecordComplete);
void GetPlayerRecord(const APlayerController* PC, const FString& RecordKey, const FOnGetCloudSaveRecordComplete& OnGetRecordComplete);
void DeletePlayerRecord(const APlayerController* PC, const FString& RecordKey, const FOnDeleteCloudSaveRecordComplete& OnDeleteRecordComplete);
private:
void OnSetPlayerRecordComplete(int32 LocalUserNum, const FOnlineError& Result, const FString& Key, const FOnSetCloudSaveRecordComplete OnSetRecordComplete);
void OnGetPlayerRecordComplete(int32 LocalUserNum, const FOnlineError& Result, const FString& Key, const FAccelByteModelsUserRecord& UserRecord, const FOnGetCloudSaveRecordComplete OnGetRecordComplete);
void OnDeletePlayerRecordComplete(int32 LocalUserNum, const FOnlineError& Result, const FString& Key, const FOnDeleteCloudSaveRecordComplete OnDeleteRecordComplete);Now, let's create the function definitions starting with
SetPlayerRecord()
. Open theCloudSaveSubsystem_Starter
class CPP file and add the following code. This will send a request to set Player Record data with the JSON data provided, under the record key provided. When the request is completed, theOnSetPlayerRecordComplete()
method will be called.void UCloudSaveSubsystem_Starter::SetPlayerRecord(const APlayerController* PC, 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 ULocalPlayer* LocalPlayer = PC->GetLocalPlayer();
ensure(LocalPlayer != nullptr);
int32 LocalUserNum = LocalPlayer->GetControllerId();
// Create Player Record or update it if it already exists.
OnSetPlayerRecordCompletedDelegateHandle = CloudSaveInterface->AddOnReplaceUserRecordCompletedDelegate_Handle(LocalUserNum, FOnReplaceUserRecordCompletedDelegate::CreateUObject(this, &ThisClass::OnSetPlayerRecordComplete, OnSetRecordComplete));
CloudSaveInterface->ReplaceUserRecord(LocalUserNum, RecordKey, RecordData);
}Next, let's define the
OnSetPlayerRecordComplete()
function. In the CPP file, add the following code. 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 in later section.void UCloudSaveSubsystem_Starter::OnSetPlayerRecordComplete(int32 LocalUserNum, const FOnlineError& Result, const FString& Key, const FOnSetCloudSaveRecordComplete OnSetRecordComplete)
{
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());
}
CloudSaveInterface->ClearOnReplaceUserRecordCompletedDelegate_Handle(LocalUserNum, OnSetPlayerRecordCompletedDelegateHandle);
OnSetRecordComplete.ExecuteIfBound(Result.bSucceeded);
}Now, create the function definition for the
GetPlayerRecord()
with the following code. This will send a request to retrieve the stored JSON data for the Player Record under the record key provided. When the request is completed, theOnGetPlayerRecordComplete()
will be called.void UCloudSaveSubsystem_Starter::GetPlayerRecord(const APlayerController* PC, const FString& RecordKey, const FOnGetCloudSaveRecordComplete& OnGetRecordComplete)
{
if (!ensure(CloudSaveInterface.IsValid()))
{
UE_LOG_CLOUDSAVE_ESSENTIALS(Warning, TEXT("Cloud Save interface is not valid."));
return;
}
const ULocalPlayer* LocalPlayer = PC->GetLocalPlayer();
ensure(LocalPlayer != nullptr);
int32 LocalUserNum = LocalPlayer->GetControllerId();
OnGetPlayerRecordCompletedDelegateHandle = CloudSaveInterface->AddOnGetUserRecordCompletedDelegate_Handle(LocalUserNum, FOnGetUserRecordCompletedDelegate::CreateUObject(this, &ThisClass::OnGetPlayerRecordComplete, OnGetRecordComplete));
CloudSaveInterface->GetUserRecord(LocalUserNum, RecordKey);
}Next, define the
OnGetPlayerRecordComplete()
function with the following code. Similar to theOnSetPlayerRecordComplete()
function, this will notify the player if the record retrieval was successful or not. Additionally, we will send the record data through the delegate for the game code to read and use. You will use this delegate in later section.void UCloudSaveSubsystem_Starter::OnGetPlayerRecordComplete(int32 LocalUserNum, const FOnlineError& Result, const FString& Key, const FAccelByteModelsUserRecord& UserRecord, const FOnGetCloudSaveRecordComplete OnGetRecordComplete)
{
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());
}
CloudSaveInterface->ClearOnGetUserRecordCompletedDelegate_Handle(LocalUserNum, OnGetPlayerRecordCompletedDelegateHandle);
OnGetRecordComplete.ExecuteIfBound(Result.bSucceeded, RecordResult);
}Now we can define the
DeletePlayerRecord()
function with the following code. This will send a request to delete the Player Record associated with the record key provided. When the request is completed,OnDeletePlayerRecordComplete()
will be called.void UCloudSaveSubsystem_Starter::DeletePlayerRecord(const APlayerController* PC, const FString& RecordKey, const FOnDeleteCloudSaveRecordComplete& OnDeleteRecordComplete)
{
if (!ensure(CloudSaveInterface.IsValid()))
{
UE_LOG_CLOUDSAVE_ESSENTIALS(Warning, TEXT("Cloud Save interface is not valid."));
return;
}
const ULocalPlayer* LocalPlayer = PC->GetLocalPlayer();
ensure(LocalPlayer != nullptr);
int32 LocalUserNum = LocalPlayer->GetControllerId();
OnDeletePlayerRecordCompletedDelegateHandle = CloudSaveInterface->AddOnDeleteUserRecordCompletedDelegate_Handle(LocalUserNum, FOnDeleteUserRecordCompletedDelegate::CreateUObject(this, &ThisClass::OnDeletePlayerRecordComplete, OnDeleteRecordComplete));
CloudSaveInterface->DeleteUserRecord(LocalUserNum, RecordKey);
}Finally, we can define the
OnDeletePlayerRecordComplete()
function with the following code. 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, const FOnDeleteCloudSaveRecordComplete OnDeleteRecordComplete)
{
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());
}
CloudSaveInterface->ClearOnDeleteUserRecordCompletedDelegate_Handle(LocalUserNum, OnDeletePlayerRecordCompletedDelegateHandle);
OnDeleteRecordComplete.ExecuteIfBound(Result.bSucceeded);
}Congratulations! Your
CloudSaveSubsystem_Starter
is completed. Build the Byte Wars project and make sure there is no compile error. In the next section, you will use this subsystem to set and get the game options from Cloud Save.
Resources
- The files used in this tutorial section are available in the Byte Wars GitHub repository.