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

Implement Subsystem - Entitlements Essentials - (Unreal Engine module)

Last updated on July 28, 2025
注記

Make sure you have completed and activated the In-Game Store Essentials module Starter mode before proceeding with this page.

Unwrap the subsystem

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

What's in the Starter Pack

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

  • Header file: Source/AccelByteWars/TutorialModules/Monetization/EntitlementsEssentials/EntitlementsEssentialsSubsystem_Starter.h
  • CPP file: Source/AccelByteWars/TutorialModules/Monetization/EntitlementsEssentials/EntitlementsEssentialsSubsystem_Starter.cpp

The EntitlementsEssentialsSubsystem_Starter class contains several helpful components:

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

const IOnlineSubsystem* Subsystem = Online::GetSubsystem(GetWorld());
if (!ensure(Subsystem))
{
return;
}

EntitlementsInterface = StaticCastSharedPtr<FOnlineEntitlementsAccelByte>(Subsystem->GetEntitlementsInterface());
if (!ensure(EntitlementsInterface))
{
return;
}
// ...
}
  • Variable to store in-game store items.
private:
// ...
UPROPERTY()
TArray<UStoreItemDataObject*> StoreOffers;
  • Functions to convert the FOnlineEntitlement data struct (used by the interface) to UStoreItemDataObject (used by the game). These also consolidate data from both entitlement and store item sources.
private:
// ...
TArray<UStoreItemDataObject*> EntitlementsToDataObjects(TArray<TSharedRef<FOnlineEntitlement>> Entitlements) const;
UStoreItemDataObject* EntitlementToDataObject(TSharedRef<FOnlineEntitlement> Entitlement) const;
  • 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 GetLocalPlayerUniqueNetId(const APlayerController* PlayerController) const;

Additionally, there is a model file located at Source/AccelByteWars/TutorialModules/Monetization/EntitlementsEssentials/EntitlementsEssentialsModel.h that defines the delegates and structs used to handle backend responses.

// ...
DECLARE_DELEGATE_TwoParams(FOnGetOrQueryUserEntitlementsComplete, const FOnlineError& /*Error*/, const TArray<UStoreItemDataObject*> /*Entitlements*/)
DECLARE_DELEGATE_TwoParams(FOnGetOrQueryUserItemEntitlementComplete, const FOnlineError& /*Error*/, const UStoreItemDataObject* /*Entitlement*/)
DECLARE_DELEGATE_TwoParams(FOnConsumeUserEntitlementComplete, const FOnlineError& /*Error*/, const UStoreItemDataObject* /*Entitlement*/)
// ...
DECLARE_MULTICAST_DELEGATE_FiveParams(
FOnItemPurchased,
const FString& /*BuyingPlayerNetId*/,
const FString& /*TransactionId*/,
const FString& /*ItemName*/,
const int /*Amount*/,
const int /*EndAmount*/)

struct FUserItemEntitlementRequest
{
const APlayerController* PlayerController;
FOnGetOrQueryUserItemEntitlementComplete OnComplete;
};
struct FConsumeEntitlementRequest
{
const APlayerController* PlayerController;
FOnConsumeUserEntitlementComplete OnComplete;
};

Implement query entitlements

When implementing entitlement queries, it's important to understand that entitlements returned from the backend only indicate ownership of a store item, they do not contain the full metadata or configuration associated with that item. This means that simply knowing which item a player owns isn't enough; the game also needs to retrieve detailed store item data (such as SKU) separately. Because of this, the game must perform two requests in parallel: one to fetch the player's entitlements, and another to fetch the relevant store items. Only once both responses are available can the game accurately interpret and act on the entitlements. The diagram below shows how Byte Wars handles this logic in practice.

  1. Open EntitlementsEssentialsSubsystem_Starter header file and declare the following functions to retrieve the entitlements.

    public:
    void GetOrQueryUserEntitlements(
    const APlayerController* PlayerController,
    const FOnGetOrQueryUserEntitlementsComplete& OnComplete,
    const bool bForceRequest = false);
    void GetOrQueryUserItemEntitlement(
    const APlayerController* PlayerController,
    const FUniqueOfferId& StoreItemId,
    const FOnGetOrQueryUserItemEntitlementComplete& OnComplete,
    const bool bForceRequest = false);
  2. Declare the following variables to store completion delegates while a query request is in progress.

    private:
    // ...
    TMultiMap<const APlayerController*, FOnGetOrQueryUserEntitlementsComplete> UserEntitlementsParams;
    TMultiMap<const FUniqueOfferId /*OfferId*/, FUserItemEntitlementRequest> UserItemEntitlementParams;
  3. Declare the following variables and functions to trigger and handle the queries.

    • QueryProcess: tracks how many query responses has been executed.
    • QueryResultUserId: tracks the player the current entitlement query is for. Useful when multiple accounts are logged in.
    • QueryResultError: stores errors from the entitlement query response.
    • QueryUserEntitlement(): triggers both entitlement and store item queries.
    • OnQueryEntitlementComplete(): handles the entitlement query response.
    • OnQueryStoreOfferComplete(): handles the store item query response.
    • GetItemEntitlement(): retrieves a cached entitlement for a given store item ID.
    • CompleteQuery(): called when both query responses have been received.
    private:
    // ...
    uint8 QueryProcess = 0;
    FUniqueNetIdPtr QueryResultUserId;
    FOnlineError QueryResultError;

    void QueryUserEntitlement(const APlayerController* PlayerController);
    void OnQueryEntitlementComplete(
    bool bWasSuccessful,
    const FUniqueNetId& UserId,
    const FString& Namespace,
    const FString& ErrorMessage);
    void OnQueryStoreOfferComplete(TArray<UStoreItemDataObject*> Offers);
    UStoreItemDataObject* GetItemEntitlement(const FUniqueNetIdPtr UserId, const FUniqueOfferId OfferId) const;
    void CompleteQuery();
  4. Open the EntitlementsEssentialsSubsystem_Starter CPP file and implement the GetOrQueryUserEntitlements() function to fetch all owned items. It checks the cache, calls the completion delegate if available, or stores it for later and starts the query.

    void UEntitlementsEssentialsSubsystem_Starter::GetOrQueryUserEntitlements(
    const APlayerController* PlayerController,
    const FOnGetOrQueryUserEntitlementsComplete& OnComplete,
    const bool bForceRequest)
    {
    // Check overall cache.
    TArray<TSharedRef<FOnlineEntitlement>> Entitlements;
    if (!bForceRequest)
    {
    EntitlementsInterface->GetAllEntitlements(
    *GetLocalPlayerUniqueNetId(PlayerController).Get(),
    FString(),
    Entitlements);
    }

    // If empty, trigger query.
    if (Entitlements.IsEmpty() || bForceRequest)
    {
    UserEntitlementsParams.Add(PlayerController, OnComplete);
    QueryUserEntitlement(PlayerController);
    }
    // If not, trigger OnComplete immediately.
    else
    {
    const FOnlineError Error = FOnlineError::Success();
    OnComplete.Execute(Error, EntitlementsToDataObjects(Entitlements));
    }
    }
  5. Implement the GetOrQueryUserItemEntitlement() to fetch an entitlement for a specific store item ID.

    void UEntitlementsEssentialsSubsystem_Starter::GetOrQueryUserItemEntitlement(
    const APlayerController* PlayerController,
    const FUniqueOfferId& StoreItemId,
    const FOnGetOrQueryUserItemEntitlementComplete& OnComplete,
    const bool bForceRequest)
    {
    // Check overall cache.
    TArray<TSharedRef<FOnlineEntitlement>> Entitlements;
    if (!bForceRequest)
    {
    EntitlementsInterface->GetAllEntitlements(
    *GetLocalPlayerUniqueNetId(PlayerController).Get(),
    FString(),
    Entitlements);
    }

    // If empty, trigger query.
    if (Entitlements.IsEmpty() || bForceRequest)
    {
    UserItemEntitlementParams.Add(StoreItemId, {PlayerController, OnComplete});
    QueryUserEntitlement(PlayerController);
    }
    // If not, trigger OnComplete immediately.
    else
    {
    const FOnlineError Error = FOnlineError::Success();
    OnComplete.ExecuteIfBound(Error, GetItemEntitlement(GetLocalPlayerUniqueNetId(PlayerController), StoreItemId));
    }
    }
  6. Implement the QueryUserEntitlement() to trigger both entitlement and store item queries. This function adds QueryProcess twice to track the two expected responses.

    void UEntitlementsEssentialsSubsystem_Starter::QueryUserEntitlement(const APlayerController* PlayerController)
    {
    if (!PlayerController)
    {
    return;
    }

    if (QueryProcess > 0)
    {
    return;
    }
    QueryProcess++;

    // Trigger query entitlements.
    EntitlementsInterface->QueryEntitlements(
    GetLocalPlayerUniqueNetId(PlayerController).ToSharedRef().Get(),
    TEXT(""),
    FPagedQuery());

    // Trigger query offers.
    const UTutorialModuleDataAsset* StoreDataAsset = UTutorialModuleUtility::GetTutorialModuleDataAsset(
    FPrimaryAssetId{ "TutorialModule:INGAMESTOREESSENTIALS" },
    this,
    true);
    if (StoreDataAsset)
    {
    STORE_SUBSYSTEM_CLASS* StoreSubsystem = GetWorld()->GetGameInstance()->GetSubsystem<STORE_SUBSYSTEM_CLASS>();
    if (!ensure(StoreSubsystem))
    {
    UE_LOG_ENTITLEMENTS_ESSENTIALS(Warning, "INGAMESTOREESSENTIALS module is inactive or its starter mode doesn't match entitlement module's starter mode. Treating as failed.")
    OnQueryStoreOfferComplete({});
    return;
    }

    QueryProcess++;
    StoreSubsystem->GetOrQueryOffersByCategory(
    PlayerController,
    TEXT("/ingamestore/item"),
    FOnGetOrQueryOffersByCategory::CreateUObject(this, &ThisClass::OnQueryStoreOfferComplete));
    }
    else
    {
    UE_LOG_ENTITLEMENTS_ESSENTIALS(Warning, "INGAMESTOREESSENTIALS module is inactive or its starter mode doesn't match entitlement module's starter mode. Treating as failed.")
    OnQueryStoreOfferComplete({});
    }
    }
  7. Implement the OnQueryEntitlementComplete() to handle the entitlement query response. It stores the response status, decreases QueryProcess, and calls CompleteQuery() if both responses are received. The actual entitlement data isn't in the response, it’s stored locally in the OSS and retrieved separately in the next step.

    void UEntitlementsEssentialsSubsystem_Starter::OnQueryEntitlementComplete(
    bool bWasSuccessful,
    const FUniqueNetId& UserId,
    const FString& Namespace,
    const FString& ErrorMessage)
    {
    QueryProcess--;

    FOnlineError Error;
    Error.bSucceeded = bWasSuccessful;
    Error.ErrorRaw = ErrorMessage;

    QueryResultUserId = UserId.AsShared();
    QueryResultError = Error;

    if (QueryProcess <= 0)
    {
    CompleteQuery();
    }
    }
  8. Implement the OnQueryStoreOfferComplete() to handle the store item query response.

    void UEntitlementsEssentialsSubsystem_Starter::OnQueryStoreOfferComplete(TArray<UStoreItemDataObject*> Offers)
    {
    QueryProcess--;
    StoreOffers = Offers;

    if (QueryProcess <= 0)
    {
    CompleteQuery();
    }
    }
  9. Implement the GetItemEntitlement() to retrieve an entitlement for a specific store item ID from the cache.

    UStoreItemDataObject* UEntitlementsEssentialsSubsystem_Starter::GetItemEntitlement(
    const FUniqueNetIdPtr UserId,
    const FUniqueOfferId OfferId) const
    {
    if (!UserId)
    {
    return nullptr;
    }

    UStoreItemDataObject* Item = nullptr;
    if (const TSharedPtr<FOnlineEntitlement> Entitlement =
    EntitlementsInterface->GetItemEntitlement(UserId.ToSharedRef().Get(), OfferId);
    Entitlement.IsValid())
    {
    Item = EntitlementToDataObject(Entitlement.ToSharedRef());
    }

    return Item;
    }
  10. Implement the CompleteQuery() to finalize the query process and trigger all stored completion delegates.

    void UEntitlementsEssentialsSubsystem_Starter::CompleteQuery()
    {
    // Trigger on complete delegate of GetOrQueryUserEntitlements function.
    TArray<const APlayerController*> UserEntitlementsParamToDelete;
    for (const TTuple<const APlayerController*, FOnGetOrQueryUserEntitlementsComplete>& Param : UserEntitlementsParams)
    {
    if (GetLocalPlayerUniqueNetId(Param.Key) == QueryResultUserId)
    {
    TArray<TSharedRef<FOnlineEntitlement>> Entitlements;
    if (QueryResultError.bSucceeded)
    {
    EntitlementsInterface->GetAllEntitlements(
    *GetLocalPlayerUniqueNetId(Param.Key).Get(),
    FString(),
    Entitlements);
    }
    Param.Value.Execute(QueryResultError, EntitlementsToDataObjects(Entitlements));

    UserEntitlementsParamToDelete.AddUnique(Param.Key);
    }
    }

    // Delete delegates.
    for (const APlayerController* PlayerController : UserEntitlementsParamToDelete)
    {
    UserEntitlementsParams.Remove(PlayerController);
    }

    // Trigger on complete delegate of GetOrQueryUserItemEntitlement function.
    TArray<FUniqueOfferId> UserItemEntitlementParamToDelete;
    for (const TTuple<const FUniqueOfferId, FUserItemEntitlementRequest>& Param : UserItemEntitlementParams)
    {
    if (GetLocalPlayerUniqueNetId(Param.Value.PlayerController) == QueryResultUserId)
    {
    Param.Value.OnComplete.Execute(
    QueryResultError,
    GetItemEntitlement(GetLocalPlayerUniqueNetId(Param.Value.PlayerController), Param.Key));

    UserItemEntitlementParamToDelete.AddUnique(Param.Key);
    }
    }

    // Delete delegates.
    for (const FUniqueOfferId& OfferId : UserItemEntitlementParamToDelete)
    {
    UserItemEntitlementParams.Remove(OfferId);
    }
    }
  11. Still in the CPP file, locate the Initialize() function and replace the existing implementation with the code below. It binds OnQueryEntitlementComplete() to the OSS delegate and QueryUserEntitlement() to the shop widget's OnActivatedMulticastDelegate so entitlements are refreshed when the shop opens.

    void UEntitlementsEssentialsSubsystem_Starter::Initialize(FSubsystemCollectionBase& Collection)
    {
    Super::Initialize(Collection);

    const IOnlineSubsystem* Subsystem = Online::GetSubsystem(GetWorld());
    if (!ensure(Subsystem))
    {
    return;
    }

    EntitlementsInterface = StaticCastSharedPtr<FOnlineEntitlementsAccelByte>(Subsystem->GetEntitlementsInterface());
    if (!ensure(EntitlementsInterface))
    {
    return;
    }

    EntitlementsInterface->OnQueryEntitlementsCompleteDelegates.AddUObject(this, &ThisClass::OnQueryEntitlementComplete);
    UShopWidget::OnActivatedMulticastDelegate.AddWeakLambda(this, [this](const APlayerController* PC)
    {
    QueryUserEntitlement(PC);
    });
    // ...
    }
  12. Navigate to the Deinitialize() function and replace the existing implementation with the code below. It unbinds the OnQueryEntitlementComplete() and QueryUserEntitlement() functions.

    void UEntitlementsEssentialsSubsystem_Starter::Deinitialize()
    {
    Super::Deinitialize();

    EntitlementsInterface->OnQueryEntitlementsCompleteDelegates.RemoveAll(this);
    UShopWidget::OnActivatedMulticastDelegate.RemoveAll(this);
    // ...
    }

Implement consume entitlement

Byte Wars provide two ways to consume entitlement: via entitlement ID, which is straight forward; and via in-game item ID, which is more complicated. The diagram below shows how Byte Wars handles the consume entitlement via in-game item ID.

  1. Open EntitlementsEssentialsSubsystem_Starter header file and declare the following functions to trigger the consume entitlement logic.

    public:
    // ...
    void ConsumeItemEntitlementByInGameId(
    const APlayerController* PlayerController,
    const FString& InGameItemId,
    const int32 UseCount = 1,
    const FOnConsumeUserEntitlementComplete& OnComplete = FOnConsumeUserEntitlementComplete());
    void ConsumeEntitlementByEntitlementId(
    const APlayerController* PlayerController,
    const FString& EntitlementId,
    const int32 UseCount = 1,
    const FOnConsumeUserEntitlementComplete& OnComplete = FOnConsumeUserEntitlementComplete());
  2. Declare the following function and variable to handle the response.

    private:
    // ...
    TMultiMap<const FString /*InGameItemId*/, FConsumeEntitlementRequest> ConsumeEntitlementParams;
    private:
    // ...
    void OnConsumeEntitlementComplete(
    bool bWasSuccessful,
    const FUniqueNetId& UserId,
    const TSharedPtr<FOnlineEntitlement>& Entitlement,
    const FOnlineError& Error);
  3. Declare the following function to trigger the consume entitlement when a player use an item in-game.

    private:
    // ...
    void OnPowerUpActivated(const APlayerController* PlayerController, const FString& ItemId);
  4. Open the EntitlementsEssentialsSubsystem_Starter CPP file and implement the ConsumeItemEntitlementByInGameId() to perform the sequence shown in the flowchart. It stores delegates to be triggered later in OnConsumeEntitlementComplete().

    void UEntitlementsEssentialsSubsystem_Starter::ConsumeItemEntitlementByInGameId(
    const APlayerController* PlayerController,
    const FString& InGameItemId,
    const int32 UseCount,
    const FOnConsumeUserEntitlementComplete& OnComplete)
    {
    // Get item's AB SKU.
    UInGameItemDataAsset* Item = UInGameItemUtility::GetItemDataAsset(InGameItemId);
    if (!ensure(Item))
    {
    return;
    }
    const FString ItemSku = Item->SkuMap[EItemSkuPlatform::AccelByte];

    // Construct delegate to consume item.
    FOnGetOrQueryUserItemEntitlementComplete OnItemEntitlementComplete = FOnGetOrQueryUserItemEntitlementComplete::CreateWeakLambda(
    this, [this, PlayerController, OnComplete, UseCount](const FOnlineError& Error, const UStoreItemDataObject* Entitlement)
    {
    if (Entitlement)
    {
    ConsumeEntitlementParams.Add(Entitlement->GetEntitlementId(), {PlayerController, OnComplete});
    EntitlementsInterface->ConsumeEntitlement(
    *GetLocalPlayerUniqueNetId(PlayerController).Get(),
    Entitlement->GetEntitlementId(),
    UseCount);
    }
    });

    // Construct delegate to get store Item ID by SKU, then get entitlement ID.
    const FOnGetOrQueryOffersByCategory OnStoreOfferComplete = FOnGetOrQueryOffersByCategory::CreateWeakLambda(
    this, [PlayerController, this, OnItemEntitlementComplete, ItemSku](TArray<UStoreItemDataObject*> Offers)
    {
    for (const UStoreItemDataObject* Offer : Offers)
    {
    if (Offer->GetSkuMap()[EItemSkuPlatform::AccelByte].Equals(ItemSku))
    {
    GetOrQueryUserItemEntitlement(PlayerController, Offer->GetStoreItemId(), OnItemEntitlementComplete);
    break;
    }
    }
    });

    // Trigger query store item.
    UTutorialModuleDataAsset* StoreDataAsset = UTutorialModuleUtility::GetTutorialModuleDataAsset(
    FPrimaryAssetId{ "TutorialModule:INGAMESTOREESSENTIALS" },
    this,
    true);
    if (StoreDataAsset && !StoreDataAsset->IsStarterModeActive())
    {
    STORE_SUBSYSTEM_CLASS* StoreSubsystem = GetWorld()->GetGameInstance()->GetSubsystem<STORE_SUBSYSTEM_CLASS>();
    if (!ensure(StoreSubsystem))
    {
    return;
    }

    StoreSubsystem->GetOrQueryOffersByCategory(PlayerController, TEXT("/ingamestore/item"), OnStoreOfferComplete);
    }
    else
    {
    UE_LOG_ENTITLEMENTS_ESSENTIALS(Warning, "INGAMESTOREESSENTIALS module is inactive or not in the same starter mode as this module. Treating as failed.")

    // Store Essential module is inactive or not in the same starter mode as this module. Treat as failed.
    OnStoreOfferComplete.ExecuteIfBound({});
    }
    }
  5. Implement the ConsumeEntitlementByEntitlementId() to directly consume an entitlement by its ID.

    void UEntitlementsEssentialsSubsystem_Starter::ConsumeEntitlementByEntitlementId(
    const APlayerController* PlayerController,
    const FString& EntitlementId,
    const int32 UseCount,
    const FOnConsumeUserEntitlementComplete& OnComplete)
    {
    ConsumeEntitlementParams.Add(EntitlementId, {PlayerController, OnComplete});
    EntitlementsInterface->ConsumeEntitlement(
    *GetLocalPlayerUniqueNetId(PlayerController).Get(),
    EntitlementId,
    UseCount);
    }
  6. Implement the OnConsumeEntitlementComplete() to handle the backend response and trigger stored delegates.

    void UEntitlementsEssentialsSubsystem_Starter::OnConsumeEntitlementComplete(
    bool bWasSuccessful,
    const FUniqueNetId& UserId,
    const TSharedPtr<FOnlineEntitlement>& Entitlement,
    const FOnlineError& Error)
    {
    TArray<FString> ConsumeEntitlementParamToDelete;
    for (const TTuple<const FString /*InGameItemId*/, FConsumeEntitlementRequest>& Param : ConsumeEntitlementParams)
    {
    if (Entitlement->Id.Equals(Param.Key))
    {
    if (*GetLocalPlayerUniqueNetId(Param.Value.PlayerController).Get() == UserId)
    {
    Param.Value.OnComplete.ExecuteIfBound(
    Error,
    bWasSuccessful ? EntitlementToDataObject(MakeShared<FOnlineEntitlement>(*Entitlement.Get())) : nullptr);
    }
    ConsumeEntitlementParamToDelete.Add(Param.Key);
    }
    }

    for (const FString& Param : ConsumeEntitlementParamToDelete)
    {
    ConsumeEntitlementParams.Remove(Param);
    }
    }
  7. Implement the OnPowerUpActivated() function to trigger the consume entitlement when a player activate a power up.

    void UEntitlementsEssentialsSubsystem_Starter::OnPowerUpActivated(const APlayerController* PlayerController, const FString& ItemId)
    {
    for (FConstPlayerControllerIterator It = GetWorld()->GetPlayerControllerIterator(); It; ++It)
    {
    const APlayerController* PC = It->Get();
    if (!PC || !PC->IsLocalPlayerController())
    {
    return;
    }

    if (PlayerController == PC)
    {
    ConsumeItemEntitlementByInGameId(
    PlayerController,
    ItemId,
    1);
    }
    }
    }
  8. In the Initialize() function, replace the existing implementation with the code below. This code binds the OnConsumeEntitlementComplete() to the OSS delegate and OnPowerUpActivated() to the pawn's OnActivatedMulticastDelegate delegate to make sure the consume entitlement is called when player use their power up.

    void UEntitlementsEssentialsSubsystem_Starter::Initialize(FSubsystemCollectionBase& Collection)
    {
    Super::Initialize(Collection);

    const IOnlineSubsystem* Subsystem = Online::GetSubsystem(GetWorld());
    if (!ensure(Subsystem))
    {
    return;
    }

    EntitlementsInterface = StaticCastSharedPtr<FOnlineEntitlementsAccelByte>(Subsystem->GetEntitlementsInterface());
    if (!ensure(EntitlementsInterface))
    {
    return;
    }

    EntitlementsInterface->OnQueryEntitlementsCompleteDelegates.AddUObject(this, &ThisClass::OnQueryEntitlementComplete);
    UShopWidget::OnActivatedMulticastDelegate.AddWeakLambda(this, [this](const APlayerController* PC)
    {
    QueryUserEntitlement(PC);
    });

    EntitlementsInterface->OnConsumeEntitlementCompleteDelegates.AddUObject(this, &ThisClass::OnConsumeEntitlementComplete);
    AAccelByteWarsPlayerPawn::OnPowerUpActivatedDelegates.AddUObject(this, &ThisClass::OnPowerUpActivated);
    // ...
    }
  9. Navigate to the Deinitialize() function and replace the existing implementation with the code below. This unbinds the OnConsumeEntitlementComplete() and OnPowerUpActivated() functions.

    void UEntitlementsEssentialsSubsystem_Starter::Deinitialize()
    {
    Super::Deinitialize();

    EntitlementsInterface->OnQueryEntitlementsCompleteDelegates.RemoveAll(this);
    UShopWidget::OnActivatedMulticastDelegate.RemoveAll(this);

    AAccelByteWarsPlayerPawn::OnPowerUpActivatedDelegates.RemoveAll(this);
    }

Resources