メインコンテンツまでスキップ

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

Last updated on July 28, 2025

Unwrap the subsystem

Byte Wars uses the Game Instance Subsystem called InGameStoreDisplaysSubsystem to wrap the AccelByte Gaming Services (AGS) Online Subsystem (OSS). This subsystem uses FOnlineStoreV2AccelByte, which is AccelByte's implementation of Unreal Engine's IOnlineStoreV2 interface. In this tutorial, you'll be working with a starter version of the subsystem, allowing you to implement the required functions from scratch.

This subsystem manages the process of retrieving Store Displays, their Sections, and the associated Offers. Each part has its own function for retrieving data from cache. However, updating those caches requires two separate queries: one to query the store front, which returns all displays, their sections, and the store item IDs, but not the full item data; and another to query offers, which fetches the complete store item details. The subsystem abstracts this complexity by exposing a single unified function. Other objects simply call this function and provide an onComplete handler, without needing to worry about the underlying query sequence. The diagram below illustrates how this behavior works.

What's in the Starter Pack

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

  • Header file: Source/AccelByteWars/TutorialModules/Monetization/InGameStoreDisplays/InGameStoreDisplaysSubsystem_Starter.h
  • CPP file: Source/AccelByteWars/TutorialModules/Monetization/InGameStoreDisplays/InGameStoreDisplaysSubsystem_Starter.cpp

The InGameStoreDisplaysSubsystem_Starter class includes several helpful components:

  • Declaration and initialization of the AGS OSS FOnlineStoreV2AccelByte interface, which provides access to AGS Software Development Kit (SDK) features.
private:
FOnlineStoreV2AccelBytePtr StoreInterface;
void UInGameStoreDisplaysSubsystem_Starter::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);

const IOnlineSubsystem* Subsystem = Online::GetSubsystem(GetWorld());
ensure(Subsystem);
const IOnlineStoreV2Ptr StorePtr = Subsystem->GetStoreV2Interface();
ensure(StorePtr);
StoreInterface = StaticCastSharedPtr<FOnlineStoreV2AccelByte>(StorePtr);
ensure(StoreInterface);
}
  • Function to retrieve the unique Net ID from a Player Controller. This is necessary because widgets, which are the primary users of this subsystem, use Player Controllers to reference players, while the OSS interface requires a unique Net ID to identify the user.
private:
// ...
FUniqueNetIdPtr GetUniqueNetIdFromPlayerController(const APlayerController* PlayerController) const;
  • Functions to convert the FOnlineStoreOfferAccelByteRef data struct (used by the interface) to UStoreItemDataObject (used by the game).
private:
// ...
UStoreItemDataObject* ConvertStoreData(const FOnlineStoreOfferAccelByteRef Offer) const;

Additionally, there is a model file in Source/AccelByteWars/TutorialModules/Monetization/InGameStoreDisplays/InGameStoreDisplaysModel.h that defines structs, delegates used to store the caller's request, and class used to store the section data.

DECLARE_DELEGATE_TwoParams(FOnQueryOrGetDisplaysComplete, TArray<TSharedRef<FAccelByteModelsViewInfo>>&, const FOnlineError&)
DECLARE_DELEGATE_TwoParams(FOnQueryOrGetSectionsInDisplayComplete, TArray<TSharedRef<FAccelByteModelsSectionInfo>>&, const FOnlineError&)
DECLARE_DELEGATE_TwoParams(FOnQueryOrGetOffersInSectionComplete, TArray<UStoreItemDataObject*>&, const FOnlineError&)

UCLASS()
class USectionDataObject : public UObject
{
GENERATED_BODY()

public:
FAccelByteModelsSectionInfo SectionInfo;
FLinearColor SectionColor;

UPROPERTY()
TArray<UStoreItemDataObject*> Offers;

bool operator== (const USectionDataObject& Other) const
{
return SectionInfo.SectionId.Equals(Other.SectionInfo.SectionId);
}
};

struct FQueryOrGetSectionsParam
{
const FUniqueNetIdPtr UserId;
const FString& DisplayId;
const FOnQueryOrGetSectionsInDisplayComplete OnComplete;
};

struct FQueryOrGetOffersParam
{
const FUniqueNetIdPtr UserId;
const FString SectionId;
const FOnQueryOrGetOffersInSectionComplete OnComplete;
};

Implement query display, section, and offers

  1. Open the InGameStoreDisplaysSubsystem_Starter header file and add the following function declarations. These functions are called by other objects to retrieve displays, sections, or offers.

    public:
    /**
    * @brief Retrieve store displays info from endpoint / cache if exist
    * @param PlayerController User's player controller to use
    * @param OnComplete Executed on response received
    * @param bForceRefresh if true, force call endpoint
    */
    void QueryOrGetDisplays(
    const APlayerController* PlayerController,
    const FOnQueryOrGetDisplaysComplete& OnComplete,
    const bool bForceRefresh = false);

    /**
    * @brief Retrieve store display's sections info from endpoint / cache if exist
    * @param PlayerController User's player controller to use
    * @param DisplayId Display Id to retrieve from
    * @param OnComplete Executed on response received
    * @param bForceRefresh if true, force call endpoint
    */
    void QueryOrGetSectionsForDisplay(
    const APlayerController* PlayerController,
    const FString& DisplayId,
    const FOnQueryOrGetSectionsInDisplayComplete& OnComplete,
    const bool bForceRefresh = false);

    /**
    * @brief Retrieve store section's offers info from endpoint / cache if exist
    * @param PlayerController User's player controller to use
    * @param SectionId Section Id to retrieve from
    * @param OnComplete Executed on response received
    * @param bForceRefresh if true, force call endpoint
    */
    void QueryOrGetOffersInSection(
    const APlayerController* PlayerController,
    const FString& SectionId,
    const FOnQueryOrGetOffersInSectionComplete& OnComplete,
    const bool bForceRefresh = false);
  2. Add the following function declarations to retrieve data from the cache.

    private:
    // ...
    TArray<TSharedRef<FAccelByteModelsViewInfo>> GetDisplays() const;
    TArray<TSharedRef<FAccelByteModelsSectionInfo>> GetSectionsForDisplay(
    const FUniqueNetId& UserId,
    const FString& DisplayId) const;
    TArray<UStoreItemDataObject*> GetOffersForSection(const FUniqueNetId& UserId, const FString& SectionId) const;
  3. Add the following variables to store the request parameters. These will be used later to trigger the completion delegate after receiving the query response.

    private:
    // ...
    TArray<FOnQueryOrGetDisplaysComplete> QueryOrGetDisplaysOnCompleteDelegates;
    TArray<FQueryOrGetSectionsParam> QueryOrGetSectionsOnCompleteDelegates;
    TArray<FQueryOrGetOffersParam> QueryOrGetOffersOnCompleteDelegates;
  4. Add the following function and variable declarations to trigger backend queries and handle their responses.

    private:
    // ...
    bool bIsQueryStoreFrontRunning = false;
    void QueryStoreFront(const FUniqueNetId& UserId);
    void OnQueryStoreFrontComplete(
    bool bWasSuccessful,
    const TArray<FString>& ViewIds,
    const TArray<FString>& SectionIds,
    const TArray<FUniqueOfferId>& OfferIds,
    const TArray<FString>& ItemMappingIds,
    const FString& Error);

    bool bIsQueryOffersRunning = false;
    void OnQueryOffersComplete(
    bool bWasSuccessful,
    const TArray<FUniqueOfferId>& OfferIds,
    const FString& Error);
  5. Open the InGameStoreDisplaysSubsystem_Starter CPP file and implement the QueryOrGetDisplays() function. This function retrieves all displays. It checks the cache first and triggers a store front query if the cache is empty.

    void UInGameStoreDisplaysSubsystem_Starter::QueryOrGetDisplays(
    const APlayerController* PlayerController,
    const FOnQueryOrGetDisplaysComplete& OnComplete,
    const bool bForceRefresh)
    {
    const FUniqueNetIdPtr UserId = GetUniqueNetIdFromPlayerController(PlayerController);
    if (!UserId.IsValid())
    {
    return;
    }

    // check cache
    if (TArray<TSharedRef<FAccelByteModelsViewInfo>> Displays = GetDisplays(); !Displays.IsEmpty() && !bForceRefresh)
    {
    OnComplete.ExecuteIfBound(Displays, FOnlineError::Success());
    return;
    }

    // cache empty, trigger query
    QueryOrGetDisplaysOnCompleteDelegates.Add(OnComplete);
    QueryStoreFront(*UserId.Get());
    }
  6. Implement the QueryOrGetSectionsForDisplay() function to retrieve sections for a specified display. It checks the cache for the display’s sections and triggers a store front query if none are found.

    void UInGameStoreDisplaysSubsystem_Starter::QueryOrGetSectionsForDisplay(
    const APlayerController* PlayerController,
    const FString& DisplayId,
    const FOnQueryOrGetSectionsInDisplayComplete& OnComplete,
    const bool bForceRefresh)
    {
    const FUniqueNetIdPtr UserId = GetUniqueNetIdFromPlayerController(PlayerController);
    if (!UserId.IsValid())
    {
    return;
    }

    // check cache
    if (TArray<TSharedRef<FAccelByteModelsSectionInfo>> Sections = GetSectionsForDisplay(*UserId.Get(), DisplayId);
    !Sections.IsEmpty() && !bForceRefresh)
    {
    OnComplete.ExecuteIfBound(Sections, FOnlineError::Success());
    return;
    }

    // cache empty, trigger query
    QueryOrGetSectionsOnCompleteDelegates.Add({UserId, DisplayId, OnComplete});
    QueryStoreFront(*UserId.Get());
    }
  7. Implement the QueryOrGetOffersInSection() function to retrieve offers for a specified section. It checks the cache and triggers an offers query if none are found. Unlike the previous functions, this one directly triggers a query to the OSS interface. Since this query retrieves all store items, there's no need for new request to trigger a new query while a previous one is still running, controlled by the bIsQueryOffersRunning flag.

    void UInGameStoreDisplaysSubsystem_Starter::QueryOrGetOffersInSection(
    const APlayerController* PlayerController,
    const FString& SectionId,
    const FOnQueryOrGetOffersInSectionComplete& OnComplete,
    const bool bForceRefresh)
    {
    const FUniqueNetIdPtr UserId = GetUniqueNetIdFromPlayerController(PlayerController);
    if (!UserId.IsValid())
    {
    return;
    }

    // check cache
    if (TArray<UStoreItemDataObject*> Offers = GetOffersForSection(*UserId.Get(), SectionId);
    !Offers.IsEmpty() && !bForceRefresh)
    {
    OnComplete.ExecuteIfBound(Offers, FOnlineError::Success());
    return;
    }

    // cache empty, trigger query
    QueryOrGetOffersOnCompleteDelegates.Add({UserId, SectionId, OnComplete});
    if (bIsQueryOffersRunning)
    {
    return;
    }
    bIsQueryOffersRunning = true;
    StoreInterface->QueryOffersByFilter(
    *UserId.Get(),
    FOnlineStoreFilter(),
    FOnQueryOnlineStoreOffersComplete::CreateUObject(this, &ThisClass::OnQueryOffersComplete));
    }
  8. Still in the CPP file, implement the GetDisplays() function to return the cached displays.

    TArray<TSharedRef<FAccelByteModelsViewInfo>> UInGameStoreDisplaysSubsystem_Starter::GetDisplays() const
    {
    TArray<TSharedRef<FAccelByteModelsViewInfo, ESPMode::ThreadSafe>> Displays;
    StoreInterface->GetDisplays(Displays);
    return Displays;
    }
  9. Implement the GetSectionsForDisplay() function to return the cached sections for a display.

    TArray<TSharedRef<FAccelByteModelsSectionInfo>> UInGameStoreDisplaysSubsystem_Starter::GetSectionsForDisplay(
    const FUniqueNetId& UserId,
    const FString& DisplayId) const
    {
    TArray<TSharedRef<FAccelByteModelsSectionInfo, ESPMode::ThreadSafe>> Sections;
    StoreInterface->GetSectionsForDisplay(UserId, DisplayId, Sections);
    return Sections;
    }
  10. Implement the GetOffersForSection() function to get the cached offers.

    TArray<UStoreItemDataObject*> UInGameStoreDisplaysSubsystem_Starter::GetOffersForSection(
    const FUniqueNetId& UserId,
    const FString& SectionId) const
    {
    TArray<FOnlineStoreOfferAccelByteRef> Offers;
    StoreInterface->GetOffersForSection(UserId, SectionId, Offers);

    TArray<UStoreItemDataObject*> OfferObjects;
    for (const FOnlineStoreOfferAccelByteRef& Offer : Offers)
    {
    OfferObjects.Add(ConvertStoreData(Offer));
    }
    return OfferObjects;
    }
  11. Implement the QueryStoreFront() function to query all displays, sections, and item IDs in those sections. Since this query retrieves everything except the full item details, it should not be triggered again while a previous query is still running, managed by the bIsQueryStoreFrontRunning flag.

    void UInGameStoreDisplaysSubsystem_Starter::QueryStoreFront(const FUniqueNetId& UserId)
    {
    if (bIsQueryStoreFrontRunning)
    {
    return;
    }
    bIsQueryStoreFrontRunning = true;

    FString StoreId;
    FString ViewId;
    FString Region;
    constexpr EAccelBytePlatformMapping PlatformMapping = EAccelBytePlatformMapping::NONE;
    StoreInterface->QueryStorefront(
    UserId,
    StoreId,
    ViewId,
    Region,
    PlatformMapping,
    FOnQueryStorefrontComplete::CreateUObject(this, &ThisClass::OnQueryStoreFrontComplete));
    }
  12. Implement the OnQueryStoreFrontComplete() function to handle responses from the QueryStoreFront() function. This function processes the response and triggers the stored request parameters.

    void UInGameStoreDisplaysSubsystem_Starter::OnQueryStoreFrontComplete(
    bool bWasSuccessful,
    const TArray<FString>& ViewIds,
    const TArray<FString>& SectionIds,
    const TArray<FUniqueOfferId>& OfferIds,
    const TArray<FString>& ItemMappingIds,
    const FString& Error)
    {
    bIsQueryStoreFrontRunning = false;

    FOnlineError OnlineError;
    OnlineError.bSucceeded = bWasSuccessful;
    OnlineError.ErrorMessage = FText::FromString(Error);

    for (const FOnQueryOrGetDisplaysComplete& Param : QueryOrGetDisplaysOnCompleteDelegates)
    {
    TArray<TSharedRef<FAccelByteModelsViewInfo>> Displays = GetDisplays();
    Param.ExecuteIfBound(Displays, OnlineError);
    }
    QueryOrGetDisplaysOnCompleteDelegates.Empty();

    for (FQueryOrGetSectionsParam& Param : QueryOrGetSectionsOnCompleteDelegates)
    {
    TArray<TSharedRef<FAccelByteModelsSectionInfo>> Sections = GetSectionsForDisplay(*Param.UserId.Get(), Param.DisplayId);
    Param.OnComplete.ExecuteIfBound(Sections, OnlineError);
    }
    QueryOrGetSectionsOnCompleteDelegates.Empty();
    }
  13. Implement the OnQueryOffersComplete() function to handle responses from the QueryOrGetOffersInSection() function.

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

    FOnlineError OnlineError;
    OnlineError.bSucceeded = bWasSuccessful;
    OnlineError.ErrorMessage = FText::FromString(Error);

    for (FQueryOrGetOffersParam& Param : QueryOrGetOffersOnCompleteDelegates)
    {
    TArray<UStoreItemDataObject*> Offers = GetOffersForSection(*Param.UserId.Get(), Param.SectionId);
    Param.OnComplete.ExecuteIfBound(Offers, OnlineError);
    }
    QueryOrGetOffersOnCompleteDelegates.Empty();
    }

Resources