Implement Subsystem - Entitlements Essentials - (Unreal Engine module)
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) toUStoreItemDataObject
(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.
-
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); -
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; -
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(); -
Open the
EntitlementsEssentialsSubsystem_Starter
CPP file and implement theGetOrQueryUserEntitlements()
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));
}
} -
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));
}
} -
Implement the
QueryUserEntitlement()
to trigger both entitlement and store item queries. This function addsQueryProcess
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({});
}
} -
Implement the
OnQueryEntitlementComplete()
to handle the entitlement query response. It stores the response status, decreasesQueryProcess
, and callsCompleteQuery()
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();
}
} -
Implement the
OnQueryStoreOfferComplete()
to handle the store item query response.void UEntitlementsEssentialsSubsystem_Starter::OnQueryStoreOfferComplete(TArray<UStoreItemDataObject*> Offers)
{
QueryProcess--;
StoreOffers = Offers;
if (QueryProcess <= 0)
{
CompleteQuery();
}
} -
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;
} -
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);
}
} -
Still in the CPP file, locate the
Initialize()
function and replace the existing implementation with the code below. It bindsOnQueryEntitlementComplete()
to the OSS delegate andQueryUserEntitlement()
to the shop widget'sOnActivatedMulticastDelegate
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);
});
// ...
} -
Navigate to the
Deinitialize()
function and replace the existing implementation with the code below. It unbinds theOnQueryEntitlementComplete()
andQueryUserEntitlement()
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.
-
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()); -
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); -
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); -
Open the
EntitlementsEssentialsSubsystem_Starter
CPP file and implement theConsumeItemEntitlementByInGameId()
to perform the sequence shown in the flowchart. It stores delegates to be triggered later inOnConsumeEntitlementComplete()
.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({});
}
} -
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);
} -
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);
}
} -
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);
}
}
} -
In the
Initialize()
function, replace the existing implementation with the code below. This code binds theOnConsumeEntitlementComplete()
to the OSS delegate andOnPowerUpActivated()
to the pawn'sOnActivatedMulticastDelegate
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);
// ...
} -
Navigate to the
Deinitialize()
function and replace the existing implementation with the code below. This unbinds theOnConsumeEntitlementComplete()
andOnPowerUpActivated()
functions.void UEntitlementsEssentialsSubsystem_Starter::Deinitialize()
{
Super::Deinitialize();
EntitlementsInterface->OnQueryEntitlementsCompleteDelegates.RemoveAll(this);
UShopWidget::OnActivatedMulticastDelegate.RemoveAll(this);
AAccelByteWarsPlayerPawn::OnPowerUpActivatedDelegates.RemoveAll(this);
}
Resources
- The files used in this tutorial section are available in the Byte Wars GitHub repository.