Skip to main content

Implement feature flags using Cloud Save

Last updated on March 9, 2024

Overview

AccelByte Gaming Services (AGS) Cloud Save allows you to flag certain features to be enabled or disabled remotely without the need for a game update. This guide will walk you through implementing these feature flags into your game so you can turn them on and off in the AGS Admin Portal.

Prerequisites

You will need to have installed AGS Online Subsystem (OSS) version 0.12.0 and AGS Game SDK Unreal 24.7.0 or later.

You will also need to set up in-game configurations in game records.

This guide will use Byte Wars with Unreal Engine v5.1, so be sure to be sure to clone and install the necessary files if you want to follow along. The Unreal Engine Byte Wars GitHub can be found here, and the tutorial for getting Cloud Save up and running in Byte Wars Unreal is here.

Implement game record cloud save

This section will walk you through implementing game record cloud saves.

Create the game configuration flag in your project's .ini file. In this example, EnableMatchmaking and EnableStore flags are created and set to true in the DefaultEngine.ini file.

    [GameConfig]
EnableMatchmaking=true
EnableStore=true

If you're following the Byte Wars module, game records haven't been implemented yet. Follow these steps to do so.

Add the following code into CloudSaveSubsystem.h:

public:
void GetGameRecord(const APlayerController* PlayerController, const FString& RecordKey, const FOnGetCloudSaveRecordComplete& OnGetRecordComplete);


private:
void OnGetGameRecordComplete(int32 LocalUserNum, const FOnlineError& Result, const FString& Key, const FAccelByteModelsGameRecord& GameRecord, const FOnGetCloudSaveRecordComplete OnGetRecordComplete);


FDelegateHandle OnGetGameRecordCompletedDelegateHandle;

Add this code CloudSaveSubsystem.cpp at the UCloudSaveSubsystem::GetGameRecord declaration:

void UCloudSaveSubsystem::GetGameRecord(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 ULocalPlayer* LocalPlayer = PlayerController->GetLocalPlayer();
ensure(LocalPlayer != nullptr);
int32 LocalUserNum = LocalPlayer->GetControllerId();


OnGetGameRecordCompletedDelegateHandle = CloudSaveInterface->AddOnGetGameRecordCompletedDelegate_Handle(LocalUserNum, FOnGetGameRecordCompletedDelegate::CreateUObject(this, &ThisClass::OnGetGameRecordComplete, OnGetRecordComplete));
CloudSaveInterface->GetGameRecord(LocalUserNum, RecordKey);
}

Add this code into CloudSaveSubsystem.cpp at the UCloudSaveSubsystem::OnGetGameRecordComplete declaration:

void UCloudSaveSubsystem::OnGetGameRecordComplete(int32 LocalUserNum, const FOnlineError& Result, const FString& Key, const FAccelByteModelsGameRecord& GameRecord, const FOnGetCloudSaveRecordComplete OnGetRecordComplete)
{
FJsonObject RecordResult;


if (Result.bSucceeded)
{
RecordResult = GameRecord.Value.JsonObject.ToSharedRef().Get();
UE_LOG_CLOUDSAVE_ESSENTIALS(Log, TEXT("Success to get game record."));
}
else
{
UE_LOG_CLOUDSAVE_ESSENTIALS(Log, TEXT("Failed to get game record. Message: %s"), *Result.ErrorMessage.ToString());
}


CloudSaveInterface->ClearOnGetGameRecordCompletedDelegate_Handle(LocalUserNum, OnGetGameRecordCompletedDelegateHandle);
OnGetRecordComplete.ExecuteIfBound(Result.bSucceeded, RecordResult);
}

Create the defined strings below for our game config flag to use later when we retrieve it from Cloud Save.

#define GAME_CONFIG_KEY FString(TEXT("GameConfig"))
#define ENABLE_MATCHMAKING_KEY FString(TEXT("enableMatchmaking"))
#define ENABLE_STORE_KEY FString(TEXT("enableStore"))

Implement game configuration

Next, you will implement the game configuration. You can get the game configuration value you created before from DefaultEngine.ini using this code:

bool bEnableMatchmaking;
GConfig->GetBool(TEXT("GameConfig"), TEXT("EnableMatchmaking"), bEnableMatchmaking, GEngineIni);


bool bEnableStore;
GConfig->GetBool(TEXT("GameConfig"), TEXT("EnableStore"), bEnableStore, GEngineIni);

You can also save the game configuration value in variable instead of always reading it from the DefaultEngine.ini file.

Add this code to AccelByteWarsGameInstance.h:

public:
void SetEnableMatchmaking(bool InValue);
void SetEnableStore(bool InValue);


bool GetEnableMatchmaking();
bool GetEnableStore();


private:
bool bEnableMatchmaking = false;
bool bEnableStore = false;

Then, add this code to AccelByteWarsGameInstance.cpp:

void UAccelByteWarsGameInstance::SetEnableMatchmaking(bool InValue)
{
bEnableMatchmaking = InValue;
}


void UAccelByteWarsGameInstance::SetEnableStore(bool InValue)
{
bEnableStore = InValue;
}


bool UAccelByteWarsGameInstance::GetEnableMatchmaking()
{
return bEnableMatchmaking;
}


bool UAccelByteWarsGameInstance::GetEnableStore()
{
return bEnableStore;
}

Now, you can save the game configuration value from the DefaultEngine.ini file to those new values we created in the game instance. If you are using Byte Wars, you can add this into LoginWidget.cpp in ULoginWidget::NativeOnActivated().

bool bEnableMatchmaking;
GConfig->GetBool(TEXT("GameConfig"), TEXT("EnableMatchmaking"), bEnableMatchmaking, GEngineIni);
GameInstance->SetEnableMatchmaking(bEnableMatchmaking);


bool bEnableStore;
GConfig->GetBool(TEXT("GameConfig"), TEXT("EnableStore"), bEnableStore, GEngineIni);
GameInstance->SetEnableStore(bEnableStore);

To get the game records that we created in the AGS Admin Portal, we can call UCloudSaveSubsystem::GetGameRecord, but make sure to call it after the player has logged in in to AGS.

In Byte Wars, we can add this into the LoginWidget class. Add this to LoginWidget.h.

#include "Storage/CloudSaveEssentials/CloudSaveSubsystem.h"


protected:
void OnLoadGameConfigFromCloud(const APlayerController* PlayerController, FSimpleDelegate OnComplete);


private:
UCloudSaveSubsystem* CloudSaveSubsystem;

In LoginWidget.cpp, call the GetGameRecord from CloudSaveSubsystem that you created earlier:

void ULoginWidget::OnLoadGameConfigFromCloud(const APlayerController* PlayerController, FSimpleDelegate OnComplete)
{
if (!PlayerController)
{
UE_LOG_CLOUDSAVE_ESSENTIALS(Warning, TEXT("Cannot get game config from Cloud Save. Player Controller is null."));
return;
}


// Get game config from Cloud Save.
CloudSaveSubsystem = GameInstance->GetSubsystem<UCloudSaveSubsystem>();
CloudSaveSubsystem->GetGameRecord(
PlayerController,
GAME_CONFIG_KEY, //FString with value of GameConfig
FOnGetCloudSaveRecordComplete::CreateWeakLambda(this, [this, OnComplete](bool bWasSuccessful, FJsonObject& Result)
{
UE_LOG_CLOUDSAVE_ESSENTIALS(Warning, TEXT("Get game config from Cloud Save was successful: %s"), bWasSuccessful ? TEXT("True") : TEXT("False"));


// Update the local game options based on the Cloud Save record.
if (bWasSuccessful)
{
// Get game config from DefaultEngine.ini
bool bEnableMatchmaking = Result.GetBoolField(ENABLE_MATCHMAKING_KEY); // FString with value of enableMatchmaking
GConfig->SetBool(TEXT("GameConfig"), TEXT("EnableMatchmaking"), bEnableMatchmaking, GEngineIni);
GameInstance->SetEnableMatchmaking(bEnableMatchmaking);


bool bEnableStore = Result.GetBoolField(ENABLE_STORE_KEY); //FString with value of enableStore
GConfig->SetBool(TEXT("GameConfig"), TEXT("EnableStore"), bEnableStore, GEngineIni);
GameInstance->SetEnableStore(bEnableStore);
}


OnComplete.ExecuteIfBound();
})
);
}

You can retrieve your game configuration value using this code:

bool bEnableMatchmaking = Result.GetBoolField(ENABLE_MATCHMAKING_KEY);
bool bEnableStore = Result.GetBoolField(ENABLE_STORE_KEY);

Then, you can save it into DefaultEngine.ini. This is only a runtime cache, so the real file will not be changed.

GConfig->SetBool(TEXT("GameConfig"), TEXT("EnableMatchmaking"), bEnableMatchmaking, GEngineIni);
GConfig->SetBool(TEXT("GameConfig"), TEXT("EnableStore"), bEnableStore, GEngineIni);

Save it into our game instance for later use.

GameInstance->SetEnableMatchmaking(bEnableMatchmaking);
GameInstance->SetEnableStore(bEnableStore);

Add a freeform push notification

You need to create a listener to a lobby notification that will notify about game configuration changes. First, you will create a topic for it in the AGS Admin Portal. In this example, you will create a GAMECONFIG_CHANGE topic. In the AGS Admin Portal open your game namespace, go to Game Management > Push Notifications > Topics. Click + New Topic. Fill in the topic name as GAMECONFIG_CHANGE and add the descriptions.

In the game project, you need to add a listener for the notification. First, you need to get the ApiClient. Add this code to AuthEssentialsSubsystem.h:

public:
AccelByte::FApiClientPtr GetApiClient();


private:
AccelByte::FApiClientPtr ApiClient;

Add this code to the AuthEssentialsSubsystem.cpp:

AccelByte::FApiClientPtr UAuthEssentialsSubsystem::GetApiClient()
{
if (!ApiClient.IsValid())
{
UE_LOG_AUTH_ESSENTIALS(Warning, TEXT("Api Client not valid"));
return nullptr;
}
return ApiClient;
}

Set the ApiClient at the UAuthEssentialsSubsystem::OnLoginComplete when login is successful:

if (bLoginWasSuccessful)
{
UE_LOG_AUTH_ESSENTIALS(Log, TEXT("Login user successful."));
ApiClient = IdentityInterface->GetApiClient(LocalUserNum);


if (!ApiClient.IsValid())
{
UE_LOG_AUTH_ESSENTIALS(Warning, TEXT("Cannot get the Api Client"));
}
}
else

Then, in LoginWidget.h, create a function delegate for the notification:

void OnMessageNotif(const FAccelByteModelsNotificationMessage& InMessage);

And the declaration for it on the LoginWidget.cpp.

void ULoginWidget::OnMessageNotif(const FAccelByteModelsNotificationMessage& InMessage)
{
if (InMessage.Topic == "GAMECONFIG_CHANGE")
{
// do hide the corresponding button
}
}

Set the delegate function after user login is complete in the LoginWidget.cpp at ULoginWidget::OnLoginComplete:

const AccelByte::FApiClientPtr ApiClient = AuthSubsystem->GetApiClient();


// listen to Message Notif Lobby
const AccelByte::Api::Lobby::FMessageNotif Delegate = AccelByte::Api::Lobby::FMessageNotif::CreateUObject(this, &ULoginWidget::OnMessageNotif);
if (!ApiClient.IsValid())
{
UE_LOG_AUTH_ESSENTIALS(Warning, TEXT("Cannot get the Api Client"));
return;
}
ApiClient->Lobby.SetMessageNotifDelegate(Delegate);

For sending the push notification, you can go to the AGS Admin Portal under Game Management > Push Notifications > Templates and click Send Freeform.

Send Freeform Push Notification on Admin Portal

Handle the UI button

In the Byte Wars project, there is nothing handling the UI button on the main menu. You need to add it first, so go back to AccelByteWarsGameInstance.h and add this code:

public:
void SetPlayOnlineButton(UUserWidget* InButton);
void SetStoreButton(UUserWidget* InButton);


void SetPlayOnlineButtonVisibility(ESlateVisibility InValue);
void SetStoreButtonVisibility(ESlateVisibility InValue);

private:
UUserWidget* ButtonPlayOnline;
UUserWidget* ButtonStore;

Then, create the declaration of that function in AccelByteWarsGameInstance.cpp.

void UAccelByteWarsGameInstance::SetPlayOnlineButton(UUserWidget* InButton)
{
ButtonPlayOnline = InButton;
}


void UAccelByteWarsGameInstance::SetStoreButton(UUserWidget* InButton)
{
ButtonStore = InButton;
}


void UAccelByteWarsGameInstance::SetPlayOnlineButtonVisibility(ESlateVisibility InValue)
{
if (!ButtonPlayOnline)
{
GAMEINSTANCE_LOG("Failed to Set Play Online Button Visibility, the button is not set");
return;
}
ButtonPlayOnline->SetVisibility(InValue);
}


void UAccelByteWarsGameInstance::SetStoreButtonVisibility(ESlateVisibility InValue)
{
if (!ButtonPlayOnline)
{
GAMEINSTANCE_LOG("Failed to Set Store Button Visibility, the button is not set");
return;
}
ButtonStore->SetVisibility(InValue);
}

After that, set the Play Online and Store buttons by adding this code into UAccelByteWarsActivatableWidget::GenerateEntryButton at AccelByteWarsActivatableWidget.cpp:

if (EntryWidgetClassName == "W_PlayOnline_C")
{
GameInstance->SetPlayOnlineButton(Button.Get());
}


if (EntryWidgetClassName == "W_Store_C")
{
GameInstance->SetStoreButton(Button.Get());
}

In LoginWidget.h, create a function delegate to handle the game configuration update that will hide or unhide the corresponding button based on the config:

public:
void UpdateConfig();


private:
FSimpleDelegate OnGetGameRecordComplete;

Add the declaration for it in LoginWidget.cpp.

void ULoginWidget::UpdateConfig()
{
if (GameInstance->GetEnableMatchmaking())
{
GameInstance->SetPlayOnlineButtonVisibility(ESlateVisibility::Visible);
}
else
{
GameInstance->SetPlayOnlineButtonVisibility(ESlateVisibility::Collapsed);
}


if (GameInstance->GetEnableStore())
{
GameInstance->SetStoreButtonVisibility(ESlateVisibility::Visible);
}
else
{
GameInstance->SetStoreButtonVisibility(ESlateVisibility::Collapsed);
}
}

Now, you can bind OnGetGameRecordComplete with UpdateConfig. Add this code in ULoginWidget::NativeOnActivated:

OnGetGameRecordComplete.BindUObject(this, &ULoginWidget::UpdateConfig);

Then, you put it all together by calling OnLoadGameConfigFromCloud function that you added before into ULoginWidget::OnLoginComplete at LoginWidget.cpp. Add this code for when the player is successfully logged in:

APlayerController* PlayerController = GetWorld()->GetFirstPlayerController();
ensure(PlayerController);


OnLoadGameConfigFromCloud(PlayerController, OnGetGameRecordComplete);

And add this code on ULoginWidget::OnMessageNotif:

if (InMessage.Topic == "GAMECONFIG_CHANGE")
{
OnLoadGameConfigFromCloud(GetOwningPlayer(), OnGetGameRecordComplete);
}

Review results

Run the game and see the results. You can change the game configuration you created in the AGS Admin Portal to enable or disable the Play Online and/or Store buttons. You can give the default value for the game configuration via DefaultEngine.ini.

[GameConfig]
EnableMatchmaking=true
EnableStore=true

Or you can change the value on the fly by using Game Record Cloud Save and Freeform Push Notification.

Game Config on Game Record Admin Portal

Send Freeform Push Notification on Admin Portal

This is the Byte Wars main menu with Matchmaking and Store enabled:

Game Config All On

This is the Byte Wars main menu with Matchmaking disabled from the game configuration:

Game Config Matchmaking off

This is the Byte Wars main menu with Store disabled from the game configuration.

Game Config Store off

And this is the Byte Wars main menu with Matchmaking and Store disabled from the game configuration.

Game Config All Off