Adding UI - Entitlements Essentials - (Unreal Engine module)
What's on the menu
In this section, you'll learn how to prepare the widgets used to display items owned by the player. The related files are available in the Resources section.
Owned count widget
OwnedCountWidgetEntry_Starter: A C++ class used to display item details. You won't need to modify this menu, as it’s provided as a ready-to-use component.- Header file:
Source/AccelByteWars/TutorialModules/Monetization/EntitlementsEssentials/UI/OwnedCountWidgetEntry_Starter.h - CPP file:
Source/AccelByteWars/TutorialModules/Monetization/EntitlementsEssentials/UI/OwnedCountWidgetEntry_Starter.cpp - Blueprint widget:
Content/TutorialModules/Monetization/EntitlementEssentials/UI/W_OwnedIndicator_Starter.uasset
- Header file:
The OwnedCountWidgetEntry_Starter shows how many units of a certain item the player owns. For durable items, it will display owned instead of a count. This widget is attached to the W_StoreItemEntry widget using the Byte Wars modular system. That means W_StoreItemEntry will always include OwnedCountWidgetEntry_Starter when the entitlements module is active.
Here is a preview of the OwnedCountWidgetEntry_Starter Blueprint widget:

The components used in this widget are defined in the OwnedCountWidgetEntry_Starter class header file.
private:
// ...
UPROPERTY(BlueprintReadOnly, meta = (BindWidget, BlueprintProtected = true, AllowPrivateAccess = true))
UTextBlock* Tb_OwnedCount;
This widget includes pre-configured variables and helper functions tailored for Byte Wars integration.
-
Reference to the subsystem that handles the entitlement logic.
private:
UPROPERTY()
UEntitlementsEssentialsSubsystem_Starter* EntitlementsSubsystem;void UOwnedCountWidgetEntry_Starter::NativeOnActivated()
{
// ...
EntitlementsSubsystem = GetGameInstance()->GetSubsystem<UEntitlementsEssentialsSubsystem_Starter>();
ensure(EntitlementsSubsystem);
// ...
} -
Reference to its parent widget,
W_StoreItemEntry. Since this widget is only used within that context, it safely assumesW_StoreItemEntryas its parent.private:
UPROPERTY()
UStoreItemListEntry* W_Parent;
Equipment inventory widget
This widget displays all equipable items, including ship customizations, visual effects, and power-ups. This widget is defined in the files below:
-
EquipmentInventoryWidget_Starter: A C++ class used to display the list of owned items and their details.- Header file:
Source/AccelByteWars/TutorialModules/Monetization/EntitlementsEssentials/UI/EquipmentInventoryWidget_Starter.h - CPP file:
Source/AccelByteWars/TutorialModules/Monetization/EntitlementsEssentials/UI/EquipmentInventoryWidget_Starter.cpp - Blueprint widgets:
Content/TutorialModules/Monetization/EntitlementEssentials/UI/W_ShipCustomization_Starter.uassetContent/TutorialModules/Monetization/EntitlementEssentials/UI/W_FxCustomization_Starter.uassetContent/TutorialModules/Monetization/EntitlementEssentials/UI/W_PowerUpCustomization_Starter.uasset
- Header file:
There are several variants of this inventory widget, but all of them are controlled by a single EquipmentInventoryWidget_Starter class. These widget variants are:
-
Ship customization widget
The
W_ShipCustomization_Starterwidget displays list of owned ship shapes and colors to equip. Here is the preview of this widget:
-
Visual effects customization
The
W_FxCustomization_Starterwidget displays list of owned visual effects to equip, such as explosion and missile trail. Here is the preview of this widget:
-
Power ups customization
The
W_PowerUpCustomization_Starterwidget displays list of owned power ups to equip. Here is the preview of this widget:
The components used in these widgets are defined in the EquipmentInventoryWidget_Starter class header file.
-
Reference to the subsystem that handles the entitlement logic.
protected:
// ...
UPROPERTY()
UEntitlementsEssentialsSubsystem_Starter* EntitlementsSubsystem;void UEquipmentInventoryWidget_Starter::NativeOnActivated()
{
// ...
EntitlementsSubsystem = GetGameInstance()->GetSubsystem<UEntitlementsEssentialsSubsystem_Starter>();
ensure(EntitlementsSubsystem);
// ...
} -
Reference to widget components and helper variables.
protected:
// ...
UPROPERTY(BlueprintReadOnly, meta = (BindWidget, BlueprintProtected = true, AllowPrivateAccess = true))
UAccelByteWarsWidgetSwitcher* Ws_Root;
UPROPERTY(BlueprintReadOnly, meta = (BindWidget, BlueprintProtected = true, AllowPrivateAccess = true))
UInventoryWidget* W_Inventory;
UPROPERTY(BlueprintReadOnly, meta = (BindWidget, BlueprintProtected = true, AllowPrivateAccess = true))
UCommonButtonBase* Btn_Back;
UPROPERTY(BlueprintReadOnly, EditAnywhere)
TSubclassOf<AActor> PreviewActorClass;
UPROPERTY(BlueprintReadOnly)
AActor* PreviewActor;
UPROPERTY(BlueprintReadOnly, EditAnywhere)
FName PreviewActorTag;
UPROPERTY(BlueprintReadOnly, EditAnywhere)
FTransform PreviewActorTransform;
UPROPERTY(BlueprintReadOnly, EditAnywhere)
FSlateBrush PreviewActorRenderTarget;
UPROPERTY()
UEntitlementsEssentialsSubsystem_Starter* EntitlementsSubsystem;
UPROPERTY()
TArray<UStoreItemDataObject*> CachedEntitlements;
UPROPERTY()
FPlayerEquipments CachedEquipments;
Ready the menu
-
Open the
OwnedCountWidgetEntry_Starterheader file and add the following function declarations.private:
// ...
void RetrieveEntitlementWithForceRequest(const bool bForceRequest);
void ShowOwnedCount(const FOnlineError& Error, const UStoreItemDataObject* Entitlement); -
Open the
OwnedCountWidgetEntry_StarterCPP file and implement theRetrieveEntitlementWithForceRequest()function. For now, this function retrieves only the item represented by the widget. You'll expand this later to include entitlement retrieval.void UOwnedCountWidgetEntry_Starter::RetrieveEntitlementWithForceRequest(const bool bForceRequest)
{
SetVisibility(ESlateVisibility::Collapsed);
const ULocalPlayer* LocalPlayer = GetOwningPlayer()->GetLocalPlayer();
W_Parent = GetFirstOccurenceOuter<UStoreItemListEntry>();
if (W_Parent && LocalPlayer)
{
// Get item.
const UStoreItemDataObject* ItemData = W_Parent->GetItemData();
if (!ItemData)
{
return;
// ...
}
} -
Implement the
ShowOwnedCount()function. This displays the number of items owned, or an Owned indicator if the item is durable, based on the backend response.void UOwnedCountWidgetEntry_Starter::ShowOwnedCount(const FOnlineError& Error, const UStoreItemDataObject* Entitlement)
{
if (!Error.bSucceeded || !Entitlement)
{
return;
}
// Set owned count.
FText Text;
if (Entitlement->GetCount() > 0)
{
Text = Entitlement->GetIsConsumable() ?
FText::FromString(FString::Printf(TEXT("%d x"), Entitlement->GetCount())) : TEXT_OWNED;
}
Tb_OwnedCount->SetText(Text);
// Show the owned count if not empty.
SetVisibility(Tb_OwnedCount->GetText().IsEmpty() ? ESlateVisibility::Collapsed : ESlateVisibility::Visible);
} -
Find the
NativeOnActivated()function and replace the existing implementation with the code below. This triggersRetrieveEntitlementWithForceRequest()when the widget is activated and binds the function to the item purchase widget delegate, so the item count updates immediately after a successful purchase.void UOwnedCountWidgetEntry_Starter::NativeOnActivated()
{
Super::NativeOnActivated();
EntitlementsSubsystem = GetGameInstance()->GetSubsystem<UEntitlementsEssentialsSubsystem_Starter>();
ensure(EntitlementsSubsystem);
RetrieveEntitlementWithForceRequest(false);
UItemPurchaseWidget::OnPurchaseCompleteMulticastDelegate.AddWeakLambda(this, [this](const APlayerController* PC)
{
RetrieveEntitlementWithForceRequest(true);
});
UItemPurchaseWidget_Starter::OnPurchaseCompleteMulticastDelegate.AddWeakLambda(this, [this](const APlayerController* PC)
{
RetrieveEntitlementWithForceRequest(true);
});
} -
Navigate to the
NativeOnDeactivated()function and replace the existing implementation with the code below. This unbinds all delegates used by the widget to ensure there are no lingering references when the widget is deactivated.void UOwnedCountWidgetEntry_Starter::NativeOnDeactivated()
{
Super::NativeOnDeactivated();
UItemPurchaseWidget::OnPurchaseCompleteMulticastDelegate.RemoveAll(this);
UItemPurchaseWidget_Starter::OnPurchaseCompleteMulticastDelegate.RemoveAll(this);
} -
Now, open the
EquipmentInventoryWidget_Starterheader file and add the following function declarations.protected:
void SpawnPreviewActor();protected:
// ...
void QueryEquipmentItems();protected:
// ...
void DisplayEquipmentItems(FInventoryCategory* Category);protected:
// ...
void OnEquipmentItemClicked(UObject* Item, FInventoryCategory* Category);
void OnEquipmentItemEquipped(UObject* Item, FInventoryCategory* Category);
void OnEquipmentItemUnequipped(UObject* Item, FInventoryCategory* Category);protected:
// ...
void SaveEquipments(); -
Open the
EquipmentInventoryWidget_StarterCPP file to define the functions above. Let's start with theSpawnPreviewActor()function. This function spawns an actor that will be used to display the applied ship customizations.void UEquipmentInventoryWidget_Starter::SpawnPreviewActor()
{
if (PreviewActorClass && !PreviewActor)
{
// Try to get existing preview actor.
TArray<AActor*> Actors;
UGameplayStatics::GetAllActorsOfClassWithTag(this, PreviewActorClass, { PreviewActorTag }, Actors);
if (!Actors.IsEmpty() && Actors[0]->IsValidLowLevel())
{
PreviewActor = Actors[0];
}
// Otherwise, try to spawn a new preview actor.
if (!PreviewActor && GetWorld())
{
PreviewActor = GetWorld()->SpawnActor<AActor>(PreviewActorClass, PreviewActorTransform);
if (PreviewActor)
{
PreviewActor->Tags.Add(PreviewActorTag);
}
}
ensureMsgf(PreviewActor, TEXT("Preview actor is not found. Failed to spawn or find existing actor."));
}
if (PreviewActor)
{
W_Inventory->SetPreview(PreviewActorRenderTarget);
}
} -
Next, define the
QueryEquipmentItems()function. Later, you will complete this function to query the player's item entitlements to be displayed on the widget. For now, add the code below to display a loading spinner indicating that the data is being loaded.void UEquipmentInventoryWidget_Starter::QueryEquipmentItems()
{
const ULocalPlayer* LocalPlayer = GetOwningPlayer()->GetLocalPlayer();
if (!LocalPlayer)
{
UE_LOG_ENTITLEMENTS_ESSENTIALS(Warning, "Failed to save customization. Local player is invalid.");
Ws_Root->SetWidgetState(EAccelByteWarsWidgetSwitcherState::Error);
return;
}
const int32 LocalUserNum = LocalPlayer->GetControllerId();
const FUniqueNetIdPtr UserId = LocalPlayer->GetPreferredUniqueNetId().GetUniqueNetId();
Ws_Root->SetWidgetState(EAccelByteWarsWidgetSwitcherState::Loading);
// ...
} -
Next, define the
DisplayEquipmentItems()function to display inventory items based on the category selected by the player.void UEquipmentInventoryWidget_Starter::DisplayEquipmentItems(FInventoryCategory* Category)
{
if (!Category)
{
UE_LOG_ENTITLEMENTS_ESSENTIALS(Warning, "Failed to display customization items. Category is invalid.");
return;
}
const FString CategoryId = Category->CategoryId.ToString();
const EItemSkuPlatform PlatformSku = EItemSkuPlatform::AccelByte;
TArray<UStoreItemDataObject*> FilteredEntitlements = CachedEntitlements;
UObject* EquippedItemData = nullptr;
const FString EquippedItemId = [&]() -> FString
{
switch (Category->ItemType)
{
case EItemType::Skin:
return CachedEquipments.SkinId;
case EItemType::Color:
return CachedEquipments.ColorId;
case EItemType::ExplosionFx:
return CachedEquipments.ExplosionFxId;
case EItemType::MissileTrailFx:
return CachedEquipments.MissileTrailFxId;
case EItemType::PowerUp:
return CachedEquipments.PowerUpId;
default:
return FString();
}
}();
FString EquippedItemSku = TEXT("");
if (const UInGameItemDataAsset* EquippedItemDataAsset = UInGameItemUtility::GetItemDataAsset(EquippedItemId))
{
EquippedItemSku = EquippedItemDataAsset->SkuMap.Contains(PlatformSku) ? EquippedItemDataAsset->SkuMap[PlatformSku] : TEXT("");
}
// If there is a default item, add it as the first item in the list.
if (const UInGameItemDataAsset* DefaultItemDataAsset = Category->DefaultItemDataAsset)
{
if (UStoreItemDataObject* DefaultItemData = NewObject<UStoreItemDataObject>())
{
DefaultItemData->Setup(
DefaultItemDataAsset->DisplayName,
FText::FromName(Category->CategoryId),
UEnum::GetValueAsString(Category->ItemType),
DefaultItemDataAsset->SkuMap[PlatformSku],
TEXT(""),
DefaultItemDataAsset->Icon->GetPathName(),
DefaultItemDataAsset->SkuMap,
{}, 1, false);
EquippedItemData = DefaultItemData;
FilteredEntitlements.Insert(DefaultItemData, 0);
}
}
// Filter entitlements based on category.
FilteredEntitlements.RemoveAll(
[CategoryId, EquippedItemSku, PlatformSku, &EquippedItemData](UStoreItemDataObject* Item)
{
if (!Item)
{
return false;
}
const bool bIsValid = Item->GetCategory().ToString().Contains(CategoryId);
if (bIsValid)
{
if (Item->GetSkuMap().Contains(PlatformSku) && Item->GetSkuMap()[PlatformSku].Equals(EquippedItemSku))
{
EquippedItemData = Item;
}
Item->SetIsShowItemCount(Item->GetIsConsumable());
}
return !bIsValid;
});
// Initialize preview actor based on equipped items.
if (APlayerShipBase* ShipPreview = Cast<APlayerShipBase>(PreviewActor))
{
if (const UInGameItemDataAsset* SkinDataAsset = UInGameItemUtility::GetItemDataAsset(CachedEquipments.SkinId))
{
ShipPreview->SetAlphaTexture(SkinDataAsset->Icon);
}
if (const UInGameItemDataAsset* ColorDataAsset = UInGameItemUtility::GetItemDataAsset(CachedEquipments.ColorId))
{
ShipPreview->SetColorTexture(ColorDataAsset->Icon);
}
}
// Set initial equipped item and update the list items.
if (EquippedItemData)
{
Category->EquippedItem = EquippedItemData;
}
W_Inventory->SetListItems(FilteredEntitlements);
} -
Define these functions to be called when an inventory item is clicked, equipped, or unequipped. When an item is clicked, the widget displays the item's information and its preview. When an item is equipped, the widget temporarily stores the equipped item. When an item is unequipped, the item is removed from the temporary data. This temporary data will be finalized as the equipped items once the user closes the menu.
void UEquipmentInventoryWidget_Starter::OnEquipmentItemClicked(UObject* Item, FInventoryCategory* Category)
{
const UStoreItemDataObject* ItemData = Cast<UStoreItemDataObject>(Item);
if (!Category || !ItemData)
{
UE_LOG_ENTITLEMENTS_ESSENTIALS(Warning, "Failed handle item clicked event. Item is invalid.");
return;
}
const EItemSkuPlatform PlatformSku = EItemSkuPlatform::AccelByte;
const UInGameItemDataAsset* ItemDataAsset =
UInGameItemUtility::GetItemDataAssetBySku(PlatformSku, ItemData->GetSkuMap()[PlatformSku]);
if (!ItemDataAsset)
{
UE_LOG_ENTITLEMENTS_ESSENTIALS(Warning, "Failed handle item clicked event. Item data asset is invalid.");
return;
}
// Display preview based on item type.
W_Inventory->SetPreviewDetails(ItemDataAsset->DisplayName, ItemDataAsset->Description);
switch (Category->ItemType)
{
case EItemType::Skin:
if (APlayerShipBase* ShipPreview = Cast<APlayerShipBase>(PreviewActor))
{
ShipPreview->SetAlphaTexture(ItemDataAsset->Icon);
}
break;
case EItemType::Color:
if (APlayerShipBase* ShipPreview = Cast<APlayerShipBase>(PreviewActor))
{
ShipPreview->SetColorTexture(ItemDataAsset->Icon);
}
break;
default:
if (ItemDataAsset->PreviewVideo)
{
W_Inventory->SetPreview(ItemDataAsset->PreviewVideo);
}
break;
}
}void UEquipmentInventoryWidget_Starter::OnEquipmentItemEquipped(UObject* Item, FInventoryCategory* Category)
{
const EItemSkuPlatform PlatformSku = EItemSkuPlatform::AccelByte;
const UStoreItemDataObject* ItemData = Cast<UStoreItemDataObject>(Item);
if (!Category || !ItemData || !ItemData->GetSkuMap().Contains(PlatformSku))
{
UE_LOG_ENTITLEMENTS_ESSENTIALS(Warning, "Failed handle item unequipped event. Item is invalid.");
return;
}
const UInGameItemDataAsset* ItemDataAsset =
UInGameItemUtility::GetItemDataAssetBySku(PlatformSku, ItemData->GetSkuMap()[PlatformSku]);
if (!ItemDataAsset)
{
UE_LOG_ENTITLEMENTS_ESSENTIALS(Warning, "Failed handle item unequipped event. Item data asset is invalid.");
return;
}
// Update cached equipments based on item type.
const FString& ItemId = ItemDataAsset->Id;
switch (Category->ItemType)
{
case EItemType::Skin:
CachedEquipments.SkinId = ItemId;
break;
case EItemType::Color:
CachedEquipments.ColorId = ItemId;
break;
case EItemType::ExplosionFx:
CachedEquipments.ExplosionFxId = ItemId;
break;
case EItemType::MissileTrailFx:
CachedEquipments.MissileTrailFxId = ItemId;
break;
case EItemType::PowerUp:
CachedEquipments.PowerUpId = ItemId;
break;
}
}void UEquipmentInventoryWidget_Starter::OnEquipmentItemUnequipped(UObject* Item, FInventoryCategory* Category)
{
if (!Category)
{
UE_LOG_ENTITLEMENTS_ESSENTIALS(Warning, "Failed handle item equipped event. Item is invalid.");
return;
}
// Unequip cached equipments based on item type.
switch (Category->ItemType)
{
case EItemType::Skin:
CachedEquipments.SkinId = TEXT("");
break;
case EItemType::Color:
CachedEquipments.ColorId = TEXT("");
break;
case EItemType::ExplosionFx:
CachedEquipments.ExplosionFxId = TEXT("");
break;
case EItemType::MissileTrailFx:
CachedEquipments.MissileTrailFxId = TEXT("");
break;
case EItemType::PowerUp:
CachedEquipments.PowerUpId = TEXT("");
break;
}
} -
Define the
SaveEquipments()function. Later, you will complete this function to save the player’s equipment to cloud storage. For now, add the code below to display a loading spinner indicating that the data is being saved.void UEquipmentInventoryWidget_Starter::SaveEquipments()
{
const ULocalPlayer* LocalPlayer = GetOwningPlayer()->GetLocalPlayer();
if (!LocalPlayer)
{
UE_LOG_ENTITLEMENTS_ESSENTIALS(Warning, "Failed to save customization. Local player is invalid.");
return;
}
const int32 LocalUserNum = LocalPlayer->GetControllerId();
UAccelByteWarsGameInstance* GameInstance = Cast<UAccelByteWarsGameInstance>(GetGameInstance());
if (!GameInstance)
{
UE_LOG_ENTITLEMENTS_ESSENTIALS(Warning, "Failed to save customization. Game instance is invalid.");
return;
}
UPromptSubsystem* PromptSubsystem = GameInstance->GetSubsystem<UPromptSubsystem>();
if (!PromptSubsystem)
{
UE_LOG_ENTITLEMENTS_ESSENTIALS(Warning, "Failed to save customization. Prompt subsystem is invalid.");
return;
}
PromptSubsystem->ShowLoading(TEXT_SAVING_EQUIPMENTS);
// ...
} -
Next, bind the functions above to their respective widget components by adding the code below to the
NativeOnActivated()function.void UEquipmentInventoryWidget_Starter::NativeOnActivated()
{
// ...
W_Inventory->OnCategorySwitched.AddUObject(this, &ThisClass::DisplayEquipmentItems);
W_Inventory->OnItemClicked.AddUObject(this, &ThisClass::OnEquipmentItemClicked);
W_Inventory->OnItemEquipClicked.AddUObject(this, &ThisClass::OnEquipmentItemEquipped);
W_Inventory->OnItemUnequipClicked.AddUObject(this, &ThisClass::OnEquipmentItemUnequipped);
Btn_Back->OnClicked().AddUObject(this, &ThisClass::SaveEquipments);
SpawnPreviewActor();
QueryEquipmentItems();
} -
Build your project and open it in the Unreal Engine Editor. In the Editor, go to
/Content/TutorialModules/Monetization/EntitlementEssentials/. You will find a data asset calledDA_EntitlementsEssentials. Open it and enableIs Starter Mode Active. Then, save the data asset. This will activate the widgets, allowing you to navigate through them when you play the game. -
Play the game in the Editor, log in, and navigate from the main menu to Store > Inventory, then select one of the inventory customization types. You will see that the menu is currently in a loading state. Later, you will change this behavior to actually load the entitlement item list.
Resources
- The files used in this tutorial section are available in the Byte Wars GitHub repository.
- AccelByteWars/Source/AccelByteWars/TutorialModules/Monetization/EntitlementsEssentials/UI/OwnedCountWidgetEntry_Starter.h
- AccelByteWars/Source/AccelByteWars/TutorialModules/Monetization/EntitlementsEssentials/UI/OwnedCountWidgetEntry_Starter.cpp
- AccelByteWars/Content/TutorialModules/Monetization/EntitlementEssentials/UI/W_OwnedIndicator_Starter.uasset
- AccelByteWars/Source/AccelByteWars/TutorialModules/Monetization/EntitlementsEssentials/UI/EquipmentInventoryWidget_Starter.h
- AccelByteWars/Source/AccelByteWars/TutorialModules/Monetization/EntitlementsEssentials/UI/EquipmentInventoryWidget_Starter.cpp
- AccelByteWars/Content/TutorialModules/Monetization/EntitlementEssentials/UI/W_ShipCustomization_Starter.uasset
- AccelByteWars/Content/TutorialModules/Monetization/EntitlementEssentials/UI/W_FxCustomization_Starter.uasset
- AccelByteWars/Content/TutorialModules/Monetization/EntitlementEssentials/UI/W_PowerUpCustomization_Starter.uasset
- AccelByteWars/Content/TutorialModules/Monetization/EntitlementEssentials/DA_EntitlementsEssentials.uasset