Skip to main content

Implement Subsystem - In-Game Store Essentials - (Unreal Engine module)

Last updated on June 23, 2025

Game Setup

Byte Wars uses SKUs to link in-game items with store items. If you look at any Data Asset under AccelByteWars/Content/ByteWars/InGameItems, you will see how Byte Wars' items are configured. For example, in DA_ByteBomb, there are three different SKUs for each platform. This is useful when a single SKU cannot be used across platforms due to naming restrictions or other limitations. This setup enables each in-game item to connect to its corresponding store item.

Item's SKU

Functions for retrieving an in-game item by either its ID or SKU are available in Source/AccelByteWars/Core/AssetManager/InGameItems/InGameItemUtility.h.

public:
UFUNCTION(BlueprintPure, Category = "Item")
static UInGameItemDataAsset* GetItemDataAsset(const FString& ItemId);

UFUNCTION(BlueprintPure, Category = "Item")
static UInGameItemDataAsset* GetItemDataAssetBySku(
const EItemSkuPlatform Platform,
const FString& Sku);

Unwrap the subsystem

Byte Wars uses the Game Instance Subsystem called InGameStoreEssentialsSubsystem to wrap the AccelByte Gaming Services (AGS) Online Subsystem (OSS). This subsystem utilizes the IOnlineStoreV2Ptr provided by the Unreal Engine's OSS, which the AGS OSS integrates with. In this tutorial, you'll work with a starter version of the subsystem, allowing you to implement the required functions from scratch.

The store functionality is divided into two parts: retrieving categories and retrieving store items. Each part includes both a get function (to return stored values) and a query function (to make backend requests). The diagram below illustrates how Byte Wars implements these two functionalities:

What's in the Starter Pack

To follow along with this tutorial, a starter subsystem class named InGameStoreEssentialsSubsystem_Starter has been prepared. You can find it in the Resources section. It includes the following files:

  • Header file: Source/AccelByteWars/TutorialModules/Monetization/InGameStoreEssentials/InGameStoreEssentialsSubsystem_Starter.h
  • CPP file: Source/AccelByteWars/TutorialModules/Monetization/InGameStoreEssentials/InGameStoreEssentialsSubsystem_Starter.cpp

The InGameStoreEssentialsSubsystem_Starter class includes several helpful components:

  • Declaration and initialization of the Unreal Engine's OSS IOnlineStoreV2Ptr interface, which allows access to AGS SDK functionalities.
private:
IOnlineStoreV2Ptr StoreInterface;
void UInGameStoreEssentialsSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
// ...
const IOnlineSubsystem* Subsystem = Online::GetSubsystem(GetWorld());
ensure(Subsystem);
StoreInterface = Subsystem->GetStoreV2Interface();
ensure(StoreInterface);
}
  • A helper function for retrieving the unique Net ID from a Player Controller. This is necessary because widgets, the primary users of this subsystem, use Player Controllers to reference players. Meanwhile, the OSS interface requires a unique Net ID to identify the requesting user.
private:
// ...
FUniqueNetIdPtr GetUniqueNetIdFromPlayerController(const APlayerController* PlayerController) const;
  • A helper function to convert store item data objects from the OSS interface to Byte Wars-specific data objects. These custom objects are optimized for the game’s logic.
private:
// ...
UStoreItemDataObject* ConvertStoreData(
const FOnlineStoreOffer& Offer) const;

Additionally, there is a model file in Source/AccelByteWars/TutorialModules/Monetization/InGameStoreEssentials/InGameStoreEssentialsModel.h that defines delegates used to handle backend responses.

DECLARE_DELEGATE_OneParam(FOnGetOrQueryOffersByCategory, TArray<UStoreItemDataObject*> /*Offers*/)
DECLARE_DELEGATE_OneParam(FOnGetOrQueryOfferById, UStoreItemDataObject* /*Offer*/)
DECLARE_DELEGATE_OneParam(FOnGetOrQueryCategories, TArray<FOnlineStoreCategory> /*Category*/)

You’ll also find an image viewer widget in Source/AccelByteWars/Core/UI/Components/AccelByteWarsAsyncImageWidget.h that retrieves images from a URL.

public:
/**
* Retrieve image from the given URL. Will use DefaultBrush if URL isn't valid
* @param ImageUrl URL to retrieve image from
*/
UFUNCTION(BlueprintCallable)
void LoadImage(const FString& ImageUrl);

Implement query categories

As explained in the previous section, the subsystem either calls query or just get depending on the state. The output of this function is a delegate, so the caller doesn’t need to handle these scenarios separately.

  1. Open the InGameStoreEssentialsSubsystem_Starter Header file and declare the following delegate to hold pending requests while waiting for a backend response.

    private:
    // ...
    TMultiMap<const FString /*CategoryPath*/, FOnGetOrQueryCategories> CategoriesByRootPathDelegates;
  2. Still in the Header file, declare a variable to indicate whether the query category is currently running.

    private:
    // ...
    bool bIsQueryCategoriesRunning = false;
  3. Declare the functions that interact with the store interface.

    private:
    // ...
    TArray<FOnlineStoreCategory> GetCategories(const FString& RootPath) const;
    // ...
    void QueryCategories(const FUniqueNetIdPtr UserId);
    void OnQueryCategoriesComplete(bool bWasSuccessful, const FString& Error);
  4. Declare the public function that other objects will call to interact with this subsystem.

    public:
    // ...
    void GetOrQueryCategoriesByRootPath(
    const APlayerController* PlayerController,
    const FString& RootPath,
    FOnGetOrQueryCategories OnComplete,
    bool bForceRefresh = false);
  5. Go to the InGameStoreEssentialsSubsystem_Starter CPP file and implement the GetCategories() function. This retrieves the cached categories that match the given root path. Use / to retrieve all categories.

    TArray<FOnlineStoreCategory> UInGameStoreEssentialsSubsystem_Starter::GetCategories(const FString& RootPath) const
    {
    TArray<FOnlineStoreCategory> Categories;
    StoreInterface->GetCategories(Categories);

    TArray<FOnlineStoreCategory> ChildCategories;
    for (FOnlineStoreCategory& Category : Categories)
    {
    if (Category.Id.Find(RootPath) == 0)
    {
    ChildCategories.Add(Category);
    }
    }

    return ChildCategories;
    }
  6. Implement the OnQueryCategoriesComplete() function to handle the backend response and trigger all pending requests.

    void UInGameStoreEssentialsSubsystem_Starter::OnQueryCategoriesComplete(bool bWasSuccessful, const FString& Error)
    {
    bIsQueryCategoriesRunning = false;

    TArray<const FString*> KeysToDelete;
    for (TTuple<const FString, FOnGetOrQueryCategories>& Delegate : CategoriesByRootPathDelegates)
    {
    Delegate.Value.Execute(GetCategories(Delegate.Key));

    // Avoid modifying while it still being used.
    KeysToDelete.Add(&Delegate.Key);
    }

    // Delete delegates.
    for (const FString* Key : KeysToDelete)
    {
    CategoriesByRootPathDelegates.Remove(*Key);
    }
    }
  7. Implement the QueryCategories() function to request categories from the backend.

    void UInGameStoreEssentialsSubsystem_Starter::QueryCategories(const FUniqueNetIdPtr UserId)
    {
    // Abort if the query process is already running.
    if (bIsQueryCategoriesRunning)
    {
    return;
    }

    StoreInterface->QueryCategories(
    *UserId.Get(),
    FOnQueryOnlineStoreCategoriesComplete::CreateUObject(this, &ThisClass::OnQueryCategoriesComplete));
    bIsQueryCategoriesRunning = true;
    }
  8. Implement the GetOrQueryCategoriesByRootPath() function. This is the function that other object will call to retrieve store categories.

    void UInGameStoreEssentialsSubsystem_Starter::GetOrQueryCategoriesByRootPath(
    const APlayerController* PlayerController,
    const FString& RootPath,
    FOnGetOrQueryCategories OnComplete,
    bool bForceRefresh)
    {
    // Check overall cache.
    TArray<FOnlineStoreCategory> Categories;
    StoreInterface->GetCategories(Categories);

    // If empty or forced to refresh, query.
    if (Categories.IsEmpty() || bForceRefresh)
    {
    const FUniqueNetIdPtr LocalUserNetId = GetUniqueNetIdFromPlayerController(PlayerController);
    if (!LocalUserNetId.IsValid())
    {
    ExecuteNextTick(FTimerDelegate::CreateWeakLambda(this, [OnComplete]()
    {
    OnComplete.Execute(TArray<FOnlineStoreCategory>());
    }));
    return;
    }

    CategoriesByRootPathDelegates.Add(RootPath, OnComplete);
    QueryCategories(LocalUserNetId);
    }
    // Else, execute immediately.
    else
    {
    const TArray<FOnlineStoreCategory> StoreCategories = GetCategories(RootPath);
    ExecuteNextTick(FTimerDelegate::CreateWeakLambda(this, [OnComplete, StoreCategories]()
    {
    OnComplete.Execute(StoreCategories);
    }));
    }
    }

Implement query store items

Similar to querying categories, the store interface provides two functions to get and query store items. In Byte Wars, we’ve created two wrapper functions that internally use these store interface functions: one retrieves store items by AccelByte’s store item ID, and the other retrieves them by category.

note

In the store interface, store items are referred to as "offers". Both terms mean the same thing.

  1. Open the InGameStoreEssentialsSubsystem_Starter Header file and declare the following delegates. These delegates store pending requests while the store interface waits for a response from the backend. As Byte Wars has two separate functions for querying by category and by ID, we use two corresponding delegates.

    private:
    // ...
    TMultiMap<const FString /*Category*/, FOnGetOrQueryOffersByCategory> OffersByCategoryDelegates;
    TMultiMap<const FUniqueOfferId /*OfferId*/, FOnGetOrQueryOfferById> OfferByIdDelegates;
  2. In the same header file, declare a variable to indicate whether a query offer to the backend is currently in progress.

    private:
    // ...
    bool bIsQueryOfferRunning = false;
  3. Declare the following functions to interact with the store interface.

    private:
    // ...
    TArray<UStoreItemDataObject*> GetOffersByCategory(const FString Category) const;
    UStoreItemDataObject* GetOfferById(const FUniqueOfferId& OfferId) const;
    // ...
    void QueryOffers(const FUniqueNetIdPtr UserId);
    void OnQueryOffersComplete(bool bWasSuccessful, const TArray<FUniqueOfferId>& OfferIds, const FString& Error);
  4. Declare the following functions, which will be used by other objects to interact with this subsystem.

    public:
    void GetOrQueryOffersByCategory(
    const APlayerController* PlayerController,
    const FString& Category,
    FOnGetOrQueryOffersByCategory OnComplete,
    bool bForceRefresh = false);
    void GetOrQueryOfferById(
    const APlayerController* PlayerController,
    const FUniqueOfferId& OfferId,
    FOnGetOrQueryOfferById OnComplete);
  5. Go to the InGameStoreEssentialsSubsystem_Starter CPP file and implement the GetOffersByCategory() and GetOfferById() functions. These functions retrieve cached store items via the GetOffers() function of the store interface. The only difference between them is how the items are filtered.

    TArray<UStoreItemDataObject*> UInGameStoreEssentialsSubsystem_Starter::GetOffersByCategory(const FString Category) const
    {
    TArray<FOnlineStoreOfferRef> FilteredOffers;
    TArray<UStoreItemDataObject*> StoreItems;

    TArray<FOnlineStoreOfferRef> Offers;
    StoreInterface->GetOffers(Offers);
    for (const FOnlineStoreOfferRef& Offer : Offers)
    {
    if (Offer->DynamicFields.Find(TEXT("Category"))->Find(Category) == 0)
    {
    FilteredOffers.Add(Offer);
    }
    }

    for (const TSharedRef<FOnlineStoreOffer>& Offer : FilteredOffers)
    {
    StoreItems.Add(ConvertStoreData(Offer.Get()));
    }

    return StoreItems;
    }
    UStoreItemDataObject* UInGameStoreEssentialsSubsystem_Starter::GetOfferById(const FUniqueOfferId& OfferId) const
    {
    UStoreItemDataObject* StoreItem = nullptr;
    if (const TSharedPtr<FOnlineStoreOffer> Offer = StoreInterface->GetOffer(OfferId); Offer.IsValid())
    {
    StoreItem = ConvertStoreData(*Offer.Get());
    }
    return StoreItem;
    }
  6. Implement the OnQueryOffersComplete() function to handle the backend response. This function processes all pending requests stored in the OffersByCategoryDelegates and OfferByIdDelegates delegates, completing them as appropriate.

    void UInGameStoreEssentialsSubsystem_Starter::OnQueryOffersComplete(
    bool bWasSuccessful,
    const TArray<FUniqueOfferId>& OfferIds,
    const FString& Error)
    {
    bIsQueryOfferRunning = false;

    TArray<const FString*> OffersByCategoryDelegateToBeDeleted;
    for (TTuple<const FString /*Category*/, FOnGetOrQueryOffersByCategory>& Delegate : OffersByCategoryDelegates)
    {
    Delegate.Value.Execute(GetOffersByCategory(Delegate.Key));

    // Avoid modifying while it still being used.
    OffersByCategoryDelegateToBeDeleted.Add(&Delegate.Key);
    }

    // Delete delegates.
    for (const FString* Key : OffersByCategoryDelegateToBeDeleted)
    {
    OffersByCategoryDelegates.Remove(*Key);
    }

    TArray<const FUniqueOfferId*> OfferByIdDelegateToBeDeleted;
    for (TTuple<const FUniqueOfferId, FOnGetOrQueryOfferById>& Delegate : OfferByIdDelegates)
    {
    Delegate.Value.Execute(GetOfferById(Delegate.Key));

    // Avoid modifying while it still being used.
    OfferByIdDelegateToBeDeleted.Add(&Delegate.Key);
    }

    // Delete delegates.
    for (const FUniqueOfferId* Key : OfferByIdDelegateToBeDeleted)
    {
    OfferByIdDelegates.Remove(*Key);
    }
    }
  7. Implement the QueryOffers() function, which triggers the store interface to request all store items from the backend.

    void UInGameStoreEssentialsSubsystem_Starter::QueryOffers(const FUniqueNetIdPtr UserId)
    {
    // Abort if the query process is already running.
    if (bIsQueryOfferRunning)
    {
    return;
    }

    StoreInterface->QueryOffersByFilter(
    *UserId.Get(),
    FOnlineStoreFilter(),
    FOnQueryOnlineStoreOffersComplete::CreateUObject(this, &ThisClass::OnQueryOffersComplete));
    bIsQueryOfferRunning = true;
    }
  8. Implement the GetOrQueryOffersByCategory() and GetOrQueryOfferById() functions. These are the public-facing functions other objects will call to retrieve store items based on category or ID.

    void UInGameStoreEssentialsSubsystem_Starter::GetOrQueryOffersByCategory(
    const APlayerController* PlayerController,
    const FString& Category,
    FOnGetOrQueryOffersByCategory OnComplete,
    bool bForceRefresh)
    {
    // Check overall cache.
    TArray<FOnlineStoreOfferRef> Offers;
    StoreInterface->GetOffers(Offers);

    // If empty or forced to refresh, call query.
    if (Offers.IsEmpty() || bForceRefresh)
    {
    const FUniqueNetIdPtr LocalUserNetId = GetUniqueNetIdFromPlayerController(PlayerController);
    if (!LocalUserNetId.IsValid())
    {
    ExecuteNextTick(FTimerDelegate::CreateWeakLambda(this, [OnComplete]()
    {
    OnComplete.Execute(TArray<UStoreItemDataObject*>());
    }));
    return;
    }

    OffersByCategoryDelegates.Add(Category, OnComplete);
    QueryOffers(LocalUserNetId);
    }
    // Else, call get.
    else
    {
    const TArray<UStoreItemDataObject*> StoreItemDataObjects = GetOffersByCategory(Category);
    ExecuteNextTick(FTimerDelegate::CreateWeakLambda(this, [OnComplete, StoreItemDataObjects]()
    {
    OnComplete.Execute(StoreItemDataObjects);
    }));
    }
    }
    void UInGameStoreEssentialsSubsystem_Starter::GetOrQueryOfferById(
    const APlayerController* PlayerController,
    const FUniqueOfferId& OfferId,
    FOnGetOrQueryOfferById OnComplete)
    {
    // Check overall cache.
    TArray<FOnlineStoreOfferRef> Offers;
    StoreInterface->GetOffers(Offers);

    // If empty, call query.
    if (Offers.IsEmpty())
    {
    const FUniqueNetIdPtr LocalUserNetId = GetUniqueNetIdFromPlayerController(PlayerController);
    if (!LocalUserNetId.IsValid())
    {
    ExecuteNextTick(FTimerDelegate::CreateWeakLambda(this, [OnComplete]()
    {
    OnComplete.Execute(nullptr);
    }));
    return;
    }

    OfferByIdDelegates.Add(OfferId, OnComplete);
    QueryOffers(LocalUserNetId);
    }
    // Else, call get.
    else
    {
    UStoreItemDataObject* StoreItemDataObject = GetOfferById(OfferId);
    ExecuteNextTick(FTimerDelegate::CreateWeakLambda(this, [OnComplete, StoreItemDataObject]()
    {
    OnComplete.Execute(StoreItemDataObject);
    }));
    }
    }

Resources