Skip to main content

Unreal Engine Module - Store game settings in the cloud - Use the Online Subsystem to store game options

Last updated on January 13, 2024

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.

  1. 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);
  2. Now, let's create the function definitions starting with SetPlayerRecord(). Open the CloudSaveSubsystem_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, the OnSetPlayerRecordComplete() 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);
    }
  3. Next, let's define the OnSetPlayerRecordComplete() function. In the CPP file, add the following code. This will execute the OnSetRecordComplete 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);
    }
  4. 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, the OnGetPlayerRecordComplete() 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);
    }
  5. Next, define the OnGetPlayerRecordComplete() function with the following code. Similar to the OnSetPlayerRecordComplete() 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);
    }
  6. 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);
    }
  7. Finally, we can define the OnDeletePlayerRecordComplete() function with the following code. This will execute the OnDeleteRecordComplete 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);
    }
  8. 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