Implement Subsystem - Entitlements Essentials - (Unreal Engine module)
注釈:本資料はAI技術を用いて翻訳されています。
このページに進む前に、In-Game Store Essentials モジュールの Starter モードを完了してアクティブ化していることを確認してください。
サブシステムの展開
Byte Wars は、AccelByte Gaming Services (AGS) Online Subsystem (OSS) をラップするために EntitlementsEssentialsSubsystem という Game Instance Subsystem を使用しています。このサブシステムは、Unreal Engine の IOnlineEntitlements インターフェースの AccelByte 実装である FOnlineEntitlementsAccelByte を使用します。このチュートリアルでは、サブシステムのスターターバージョンを使用して、必要な関数をゼロから実装します。
Starter Pack の内容
このチュートリアルに従うために、EntitlementsEssentialsSubsystem_Starter という名前のスターターサブシステムクラスが用意されています。Resources セクションで見つけることができます。以下のファイルが含まれています:
- ヘッダーファイル:
Source/AccelByteWars/TutorialModules/Monetization/EntitlementsEssentials/EntitlementsEssentialsSubsystem_Starter.h - CPP ファイル:
Source/AccelByteWars/TutorialModules/Monetization/EntitlementsEssentials/EntitlementsEssentialsSubsystem_Starter.cpp
EntitlementsEssentialsSubsystem_Starter クラスには、いくつかの便利なコンポーネントが含まれています:
-
AGS OSS インターフェースの宣言と初期化。これにより、AGS Software Development Kit (SDK) 機能へのアクセスが提供されます。
private:
FOnlineIdentityAccelBytePtr IdentityInterface;
FOnlineCloudSaveAccelBytePtr CloudSaveInterface;
FOnlineEntitlementsAccelBytePtr EntitlementsInterface;void UEntitlementsEssentialsSubsystem_Starter::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
const FOnlineSubsystemAccelByte* Subsystem = static_cast<const FOnlineSubsystemAccelByte*>(Online::GetSubsystem(GetWorld()));
if (!ensure(Subsystem))
{
return;
}
IdentityInterface = StaticCastSharedPtr<FOnlineIdentityAccelByte>(Subsystem->GetIdentityInterface());
CloudSaveInterface = StaticCastSharedPtr<FOnlineCloudSaveAccelByte>(Subsystem->GetCloudSaveInterface());
EntitlementsInterface = StaticCastSharedPtr<FOnlineEntitlementsAccelByte>(Subsystem->GetEntitlementsInterface());
if (!ensure(IdentityInterface) || !ensure(CloudSaveInterface) || !ensure(EntitlementsInterface))
{
return;
}
// ...
} -
ゲーム内ストアアイテムを保存する変数。
private:
// ...
UPROPERTY()
TArray<UStoreItemDataObject*> StoreOffers; -
FOnlineEntitlementデータ構造体(インターフェースで使用)をUStoreItemDataObject(ゲームで使用)に変換する関数。これらは、エンタイトルメントとストアアイテムソースの両方からデータを統合します。private:
// ...
TArray<UStoreItemDataObject*> EntitlementsToDataObjects(TArray<TSharedRef<FOnlineEntitlement>> Entitlements) const;
UStoreItemDataObject* EntitlementToDataObject(TSharedRef<FOnlineEntitlement> Entitlement) const;
サブシステムクラスに加えて、ヘルパークラス、デリゲート、定数を含む EntitlementsEssentialsModel.h という名前のファイルがあります。このファイルは以下の場所にあります:
このファイルには以下のヘルパーが含まれています:
-
イベントとコールバックを処理するヘルパーデリゲート。
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_DELEGATE_TwoParams(FOnUpdateUserEquipmentsComplete, const FOnlineError& /*Error*/, const FPlayerEquipments& /*Equipments*/)
/**
* Delegate for tracking when a player purchased an in-game item.
* @param BuyingPlayerNetId Unique Net ID of the player doing the purchase.
* @param TransactionId Unity ID of the transaction.
* @param ItemName In-game item name of the purchased item.
* @param Amount The quantity of the item purchased.
* @param EndAmount Total quantity of the item that the player owned.
*/
DECLARE_MULTICAST_DELEGATE_FiveParams(
FOnItemPurchased,
const FString& /*BuyingPlayerNetId*/,
const FString& /*TransactionId*/,
const FString& /*ItemName*/,
const int /*Amount*/,
const int /*EndAmount*/) -
エンタイトルメントリクエストとプレイヤー装備を保存するヘルパー構造体。
struct FUserItemEntitlementRequest
{
const FUniqueNetIdRef UserId;
FOnGetOrQueryUserItemEntitlementComplete OnComplete;
};struct FConsumeEntitlementRequest
{
const FUniqueNetIdRef UserId;
FOnConsumeUserEntitlementComplete OnComplete;
};USTRUCT(BlueprintType)
struct FPlayerEquipments
{
GENERATED_BODY()
UPROPERTY(BlueprintReadWrite)
FString SkinId;
UPROPERTY(BlueprintReadWrite)
FString ColorId;
UPROPERTY(BlueprintReadWrite)
FString ExplosionFxId;
UPROPERTY(BlueprintReadWrite)
FString MissileTrailFxId;
UPROPERTY(BlueprintReadWrite)
FString PowerUpId;
}; -
メッセージを表示するための文字列定数。
#define USER_EQUIPMENT_KEY FString(TEXT("UserEqipment"))
#define TEXT_SAVING_EQUIPMENTS NSLOCTEXT("AccelByteWars", "Saving Equipments", "Saving Equipments")
#define TEXT_OWNED NSLOCTEXT("AccelByteWars", "Owned", "Owned")
クエリエンタイトルメントの実装
エンタイトルメントクエリを実装する際、バックエンドから返されるエンタイトルメントはストアアイテムの所有権のみを示し、そのアイテムに関連付けられた完全なメタデータや設定は含まれていないことを理解することが重要です。これは、プレイヤーがどのアイテムを所有しているかを知るだけでは不十分であり、ゲームは詳細なストアアイテムデータ(SKU など)を別途取得する必要があることを意味します。このため、ゲームは2つのリクエストを並行して実行する必要があります。1つはプレイヤーのエンタイトルメントを取得し、もう1つは関連するストアアイテムを取得します。両方のレスポンスが利用可能になって初めて、ゲームはエンタイトルメントを正確に解釈して動作できます。以下の図は、Byte Wars がこのロジックを実際にどのように処理するかを示しています。
-
EntitlementsEssentialsSubsystem_Starterヘッダーファイルを開き、エンタイトルメントを取得するための以下の関数を宣言します。public:
void GetOrQueryUserEntitlements(
const FUniqueNetIdPtr UserId,
const FOnGetOrQueryUserEntitlementsComplete& OnComplete,
const bool bForceRequest = false);
void GetOrQueryUserItemEntitlement(
const FUniqueNetIdPtr UserId,
const FUniqueOfferId& StoreItemId,
const FOnGetOrQueryUserItemEntitlementComplete& OnComplete,
const bool bForceRequest = false); -
クエリリクエストが進行中の間、完了デリゲートを保存するための以下の変数を宣言します。
private:
// ...
TMultiMap<const FUniqueNetIdRef, FOnGetOrQueryUserEntitlementsComplete> UserEntitlementsParams;
TMultiMap<const FUniqueOfferId /*OfferId*/, FUserItemEntitlementRequest> UserItemEntitlementParams; -
クエリをトリガーして処理するための以下の変数と関数を宣言します。
-
クエリをトリガーして処理するための以下の変数と関数を宣言します。
QueryProcess: 実行されたクエリレスポンスの数を追跡します。QueryResultUserId: 現在のエンタイトルメントクエリの対象プレイヤーを追跡します。複数のアカウントがログインしている場合に便利です。QueryResultError: エンタイトルメントクエリレスポンスからのエラーを保存します。QueryUserEntitlement(): エンタイトルメントとストアアイテムの両方のクエリをトリガーします。OnQueryEntitlementComplete(): エンタイトルメントクエリレスポンスを処理します。OnQueryStoreOfferComplete(): ストアアイテムクエリレスポンスを処理します。GetItemEntitlement(): 指定されたストアアイテム ID のキャッシュされたエンタイトルメントを取得します。CompleteQuery(): 両方のクエリレスポンスが受信されたときに呼び出されます。
private:
// ...
uint8 QueryProcess = 0;
FUniqueNetIdPtr QueryResultUserId;
FOnlineError QueryResultError;
void QueryUserEntitlement(const FUniqueNetIdPtr UserId);
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(); -
EntitlementsEssentialsSubsystem_StarterCPP ファイルを開き、すべての所有アイテムを取得するためのGetOrQueryUserEntitlements()関数を実装します。この関数はキャッシュをチェックし、利用可能な場合は完了デリゲートを呼び出すか、後で使用するために保存してクエリを開始します。void UEntitlementsEssentialsSubsystem_Starter::GetOrQueryUserEntitlements(
const FUniqueNetIdPtr UserId,
const FOnGetOrQueryUserEntitlementsComplete& OnComplete,
const bool bForceRequest)
{
if (!UserId)
{
UE_LOG_ENTITLEMENTS_ESSENTIALS(Warning, "Failed to get or query user entitlement. User ID is invalid.");
OnComplete.ExecuteIfBound(FOnlineError::CreateError(TEXT(""), EOnlineErrorResult::InvalidParams, TEXT(""), FText::FromString(TEXT("User ID is invalid."))), {});
return;
}
// Check overall cache.
TArray<TSharedRef<FOnlineEntitlement>> Entitlements;
if (!bForceRequest)
{
EntitlementsInterface->GetAllEntitlements(UserId.ToSharedRef().Get(), FString(), Entitlements);
}
// If empty, trigger query.
if (Entitlements.IsEmpty() || bForceRequest)
{
UserEntitlementsParams.Add(UserId.ToSharedRef(), OnComplete);
QueryUserEntitlement(UserId);
}
// If not, trigger OnComplete immediately.
else
{
OnComplete.Execute(FOnlineError::Success(), EntitlementsToDataObjects(Entitlements));
}
} -
特定のストアアイテム ID のエンタイトルメントを取得するための
GetOrQueryUserItemEntitlement()を実装します。void UEntitlementsEssentialsSubsystem_Starter::GetOrQueryUserItemEntitlement(
const FUniqueNetIdPtr UserId,
const FUniqueOfferId& StoreItemId,
const FOnGetOrQueryUserItemEntitlementComplete& OnComplete,
const bool bForceRequest)
{
if (!UserId)
{
UE_LOG_ENTITLEMENTS_ESSENTIALS(Warning, "Failed to get or query user entitlement. User ID is invalid.");
OnComplete.ExecuteIfBound(FOnlineError::CreateError(TEXT(""), EOnlineErrorResult::InvalidParams, TEXT(""), FText::FromString(TEXT("User ID is invalid."))), nullptr);
return;
}
// Check overall cache.
TArray<TSharedRef<FOnlineEntitlement>> Entitlements;
if (!bForceRequest)
{
EntitlementsInterface->GetAllEntitlements(UserId.ToSharedRef().Get(), FString(), Entitlements);
}
// If empty, trigger query.
if (Entitlements.IsEmpty() || bForceRequest)
{
UserItemEntitlementParams.Add(StoreItemId, {UserId.ToSharedRef(), OnComplete});
QueryUserEntitlement(UserId);
}
// If not, trigger OnComplete immediately.
else
{
OnComplete.ExecuteIfBound(FOnlineError::Success(), GetItemEntitlement(UserId, StoreItemId));
}
} -
エンタイトルメントとストアアイテムの両方のクエリをトリガーするための
QueryUserEntitlement()を実装します。この関数は、2つの予想されるレスポンスを追跡するためにQueryProcessを2回追加します。void UEntitlementsEssentialsSubsystem_Starter::QueryUserEntitlement(const FUniqueNetIdPtr UserId)
{
if (!UserId)
{
UE_LOG_ENTITLEMENTS_ESSENTIALS(Warning, "Failed to query user entitlement. User ID is invalid.");
return;
}
if (QueryProcess > 0)
{
return;
}
QueryProcess++;
// Trigger query entitlements.
EntitlementsInterface->QueryEntitlements(UserId.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(
UserId,
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({});
}
} -
エンタイトルメントクエリレスポンスを処理するための
OnQueryEntitlementComplete()を実装します。この関数はレスポンスステータスを保存し、QueryProcessを減らし、両方のレスポンスが受信された場合はCompleteQuery()を呼び出します。実際のエンタイトルメントデータはレスポンスに含まれておらず、OSS にローカルに保存され、次のステップで個別に取得されます。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();
} -
ストアアイテムクエリレスポンスを処理するための
OnQueryStoreOfferComplete()を実装します。<!--SNIPEND-->
<!--SNIPSTART EntitlementsEssentialsSubsystem_Starter.cpp-OnQueryStoreOfferComplete {"numberOfLeadingSpaces":4}-->
```cpp
void UEntitlementsEssentialsSubsystem_Starter::OnQueryStoreOfferComplete(TArray<UStoreItemDataObject*> Offers)
{
QueryProcess--;
StoreOffers = Offers;
if (QueryProcess <= 0)
{
CompleteQuery();
}
} -
キャッシュから特定のストアアイテム ID のエンタイトルメントを取得するための
GetItemEntitlement()を実装します。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());
} -
クエリプロセスを完了し、保存されたすべての完了デリゲートをトリガーするための
CompleteQuery()を実装します。 }<!--SNIPEND-->
<!--SNIPSTART EntitlementsEssentialsSubsystem_Starter.cpp-CompleteQuery {"numberOfLeadingSpaces":4}-->
```cpp
void UEntitlementsEssentialsSubsystem_Starter::CompleteQuery()
{
// Trigger on complete delegate of GetOrQueryUserEntitlements function.
for (TMultiMap<const FUniqueNetIdRef, FOnGetOrQueryUserEntitlementsComplete>::TIterator It = UserEntitlementsParams.CreateIterator(); It; ++It)
{
const FUniqueNetIdRef& Key = It.Key();
if (!QueryResultUserId || Key.Get() != QueryResultUserId.ToSharedRef().Get())
{
continue;
}
TArray<TSharedRef<FOnlineEntitlement>> Entitlements;
if (QueryResultError.bSucceeded)
{
EntitlementsInterface->GetAllEntitlements(Key.Get(), FString(), Entitlements);
}
It.Value().Execute(QueryResultError, EntitlementsToDataObjects(Entitlements));
It.RemoveCurrent();
}
// Trigger on complete delegate of GetOrQueryUserItemEntitlement function.
for (TMultiMap<const FUniqueOfferId, FUserItemEntitlementRequest>::TIterator It = UserItemEntitlementParams.CreateIterator(); It; ++It)
{
const FUniqueOfferId& OfferId = It.Key();
const FUserItemEntitlementRequest& Request = It.Value();
if (!QueryResultUserId || Request.UserId.Get() != QueryResultUserId.ToSharedRef().Get())
{
continue;
}
Request.OnComplete.Execute(QueryResultError, GetItemEntitlement(Request.UserId, OfferId));
It.RemoveCurrent();
}
}void UEntitlementsEssentialsSubsystem_Starter::Initialize(FSubsystemCollectionBase& Collection)
{
// ...
EntitlementsInterface->AddOnQueryEntitlementsCompleteDelegate_Handle(FOnQueryEntitlementsCompleteDelegate::CreateUObject(this, &ThisClass::OnQueryEntitlementComplete));
// ...
UShopWidget::OnActivatedMulticastDelegate.AddWeakLambda(this, [this](const APlayerController* PC)
{
if (const ULocalPlayer* LocalPlayer = PC ? PC->GetLocalPlayer() : nullptr)
{
QueryUserEntitlement(LocalPlayer->GetPreferredUniqueNetId().GetUniqueNetId());
}
});
// ... -
Deinitialize()関数に移動し、既存の実装を以下のコードに置き換えます。これにより、OnQueryEntitlementComplete()とQueryUserEntitlement()関数のバインドが解除されます。<!--SNIPEND-->
<!--SNIPSTART EntitlementsEssentialsSubsystem_Starter.cpp-Deinitialize-Query {"numberOfLeadingSpaces":4}-->
```cpp
void UEntitlementsEssentialsSubsystem_Starter::Deinitialize()
{
// ...
if (EntitlementsInterface.IsValid())
{
EntitlementsInterface->ClearOnQueryEntitlementsCompleteDelegates(this);
// ...
}
// ...
UShopWidget::OnActivatedMulticastDelegate.RemoveAll(this);
// ...
}
コンシュームエンタイトルメントの実装
Byte Wars は、エンタイトルメントをコンシュームする2つの方法を提供しています。エンタイトルメント ID を使用する方法は簡単で、ゲーム内アイテム ID を使用する方法はより複雑です。以下の図は、Byte Wars がゲーム内アイテム ID を使用してエンタイトルメントをコンシュームする方法を示しています。
-
EntitlementsEssentialsSubsystem_Starterヘッダーファイルを開き、コンシュームエンタイトルメントロジックをトリガーするための以下の関数を宣言します。public:
// ...
void ConsumeItemEntitlementByInGameId(
const FUniqueNetIdPtr UserId,
const FString& InGameItemId,
const int32 UseCount = 1,
const FOnConsumeUserEntitlementComplete& OnComplete = FOnConsumeUserEntitlementComplete());
void ConsumeEntitlementByEntitlementId(
const FUniqueNetIdPtr UserId,
const FString& EntitlementId,
const int32 UseCount = 1,
const FOnConsumeUserEntitlementComplete& OnComplete = FOnConsumeUserEntitlementComplete()); -
レスポンスを処理するための以下の関数と変数を宣言します。
private:
// ...
TMultiMap<const FString /*InGameItemId*/, FConsumeEntitlementRequest> ConsumeEntitlementParams;private:
// ...
void OnConsumeEntitlementComplete(
bool bWasSuccessful,
const FUniqueNetId& UserId,
const TSharedPtr<FOnlineEntitlement>& Entitlement,
const FOnlineError& Error); -
EntitlementsEssentialsSubsystem_StarterCPP ファイルを開き、フローチャートに示されたシーケンスを実行するためのConsumeItemEntitlementByInGameId()を実装します。この関数は、後でOnConsumeEntitlementComplete()でトリガーされるデリゲートを保存します。void UEntitlementsEssentialsSubsystem_Starter::ConsumeItemEntitlementByInGameId(
const FUniqueNetIdPtr UserId,
const FString& InGameItemId,
const int32 UseCount,
const FOnConsumeUserEntitlementComplete& OnComplete)
{
if (!UserId)
{
UE_LOG_ENTITLEMENTS_ESSENTIALS(Warning, "Failed to consume item entitlement. User ID is invalid.");
OnComplete.ExecuteIfBound(FOnlineError::CreateError(TEXT(""), EOnlineErrorResult::InvalidParams, TEXT(""), FText::FromString(TEXT("User ID is invalid."))), nullptr);
return;
}
// Get item's AB SKU.
UInGameItemDataAsset* Item = UInGameItemUtility::GetItemDataAsset(InGameItemId);
if (!ensure(Item))
{
UE_LOG_ENTITLEMENTS_ESSENTIALS(Warning, "Failed to consume item entitlement. Item is invalid.");
OnComplete.ExecuteIfBound(FOnlineError::CreateError(TEXT(""), EOnlineErrorResult::InvalidParams, TEXT(""), FText::FromString(TEXT("Item is invalid."))), nullptr);
return;
}
const FString ItemSku = Item->SkuMap[EItemSkuPlatform::AccelByte];
// Construct delegate to consume item.
FOnGetOrQueryUserItemEntitlementComplete OnItemEntitlementComplete =
FOnGetOrQueryUserItemEntitlementComplete::CreateWeakLambda(this, [this, UserId, OnComplete, UseCount]
(const FOnlineError& Error, const UStoreItemDataObject* Entitlement)
{
if (Entitlement)
{
ConsumeEntitlementParams.Add(Entitlement->GetEntitlementId(), {UserId.ToSharedRef(), OnComplete});
EntitlementsInterface->ConsumeEntitlement(UserId.ToSharedRef().Get(), Entitlement->GetEntitlementId(), UseCount);
}
});
// Construct delegate to get store Item ID by SKU, then get entitlement ID.
const FOnGetOrQueryOffersByCategory OnStoreOfferComplete = FOnGetOrQueryOffersByCategory::CreateWeakLambda(
this, [UserId, this, OnItemEntitlementComplete, ItemSku](TArray<UStoreItemDataObject*> Offers)
{
for (const UStoreItemDataObject* Offer : Offers)
{
if (Offer->GetSkuMap()[EItemSkuPlatform::AccelByte].Equals(ItemSku))
{
GetOrQueryUserItemEntitlement(UserId, 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(UserId, 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({});
}
} -
エンタイトルメント ID を使用してエンタイトルメントを直接コンシュームするための
ConsumeEntitlementByEntitlementId()を実装します。void UEntitlementsEssentialsSubsystem_Starter::ConsumeEntitlementByEntitlementId(
const FUniqueNetIdPtr UserId,
const FString& EntitlementId,
const int32 UseCount,
const FOnConsumeUserEntitlementComplete& OnComplete)
{
if (!UserId)
{
UE_LOG_ENTITLEMENTS_ESSENTIALS(Warning, "Failed to consume item entitlement. User ID is invalid.");
OnComplete.ExecuteIfBound(FOnlineError::CreateError(TEXT(""), EOnlineErrorResult::InvalidParams, TEXT(""), FText::FromString(TEXT("User ID is invalid."))), nullptr);
return;
}
ConsumeEntitlementParams.Add(EntitlementId, {UserId.ToSharedRef(), OnComplete});
EntitlementsInterface->ConsumeEntitlement(UserId.ToSharedRef().Get(), EntitlementId, UseCount);
} -
バックエンドレスポンスを処理し、保存されたデリゲートをトリガーするための
OnConsumeEntitlementComplete()を実装します。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 (Param.Value.UserId.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);
}
} -
Initialize()関数で、以下のコードを追加してOnConsumeEntitlementComplete()を OSS デリゲートにバインドし、ConsumeItemEntitlementByInGameId()をポーンのOnActivatedMulticastDelegateにバインドして、プレイヤーがパワーアップを使用したときにコンシュームエンタイトルメントが呼び出されるようにします。void UEntitlementsEssentialsSubsystem_Starter::Initialize(FSubsystemCollectionBase& Collection)
{
// ...
EntitlementsInterface->AddOnConsumeEntitlementCompleteDelegate_Handle(FOnConsumeEntitlementCompleteDelegate::CreateUObject(this, &ThisClass::OnConsumeEntitlementComplete));
// ...
AAccelByteWarsPlayerPawn::OnPowerUpActivatedDelegates.AddWeakLambda(this, [this](const APlayerController* PC, const FString& ItemId)
{
if (const ULocalPlayer* LocalPlayer = PC ? PC->GetLocalPlayer() : nullptr)
{
ConsumeItemEntitlementByInGameId(LocalPlayer->GetPreferredUniqueNetId().GetUniqueNetId(), ItemId, 1);
}
});
} -
Deinitialize()関数に移動し、以下のコードを追加してOnConsumeEntitlementComplete()とConsumeItemEntitlementByInGameId()関数のバインドを解除します。void UEntitlementsEssentialsSubsystem_Starter::Deinitialize()
{
// ...
if (EntitlementsInterface.IsValid())
{
// ...
EntitlementsInterface->ClearOnConsumeEntitlementCompleteDelegates(this);
}
// ...
AAccelByteWarsPlayerPawn::OnPowerUpActivatedDelegates.RemoveAll(this);
}
Implement store equipments
ストア装備の実装
Byte Wars uses in-game item IDs to reference the player’s equipped items. To make these equipments persistent, we use Cloud Save to store the player’s equipped item IDs. Byte Wars は、プレイヤーの装備アイテムを参照するためにゲーム内アイテム ID を使用します。これらの装備を永続化するために、Cloud Save を使用してプレイヤーの装備アイテム ID を保存します。
-
EntitlementsEssentialsSubsystem_Starterヘッダーファイルを開き、以下の関数を宣言します。private:
// ...
void UpdateUserEquipments(
const int32 LocalUserNum,
const FUniqueNetIdPtr UserId,
const FPlayerEquipments& Equipments,
const FOnUpdateUserEquipmentsComplete& OnComplete);public:
// ...
void GetUserEquipments(
const int32 LocalUserNum,
const FUniqueNetIdPtr UserId,
const FOnUpdateUserEquipmentsComplete& OnComplete = FOnUpdateUserEquipmentsComplete());public:
// ...
void SetUserEquipments(
const int32 LocalUserNum,
const FUniqueNetIdPtr UserId,
const FPlayerEquipments& Equipments,
const FOnUpdateUserEquipmentsComplete& OnComplete = FOnUpdateUserEquipmentsComplete()); -
次に、装備データが保存および取得されたときに処理するための以下のコールバックを宣言します。
private:
// ...
void OnGetUserEquipmentsComplete(
const int32 LocalUserNum,
const FOnlineError& Result,
const FString& Key,
const FAccelByteModelsUserRecord& Record,
const FUniqueNetIdRef UserId,
const FOnUpdateUserEquipmentsComplete OnComplete);
void OnSetUserEquipmentsComplete(
int32 LocalUserNum,
const FOnlineError& Result,
const FString& Key,
const FUniqueNetIdRef UserId,
const FPlayerEquipments Equipments,
const FOnUpdateUserEquipmentsComplete OnComplete);
FDelegateHandle OnGetUserEquipmentsCompleteDelegateHandle, OnSetUserEquipmentsCompleteDelegateHandle; -
EntitlementsEssentialsSubsystem_StarterCPP ファイルを開き、UpdateUserEquipments()関数を定義します。この関数は、装備アイテム ID を受け取り、プレイヤーがエンタイトルメントレコードに基づいてこれらのアイテムを実際に所有していることを検証した後、ローカルに適用します。void UEntitlementsEssentialsSubsystem_Starter::UpdateUserEquipments(
const int32 LocalUserNum,
const FUniqueNetIdPtr UserId,
const FPlayerEquipments& Equipments,
const FOnUpdateUserEquipmentsComplete& OnComplete)
{
if (!UserId)
{
UE_LOG_ENTITLEMENTS_ESSENTIALS(Warning, "Failed to update user equipments. User ID is invalid.");
OnComplete.ExecuteIfBound(
FOnlineError::CreateError(TEXT(""), EOnlineErrorResult::InvalidParams, TEXT(""), FText::FromString(TEXT("User ID is invalid."))),
FPlayerEquipments());
return;
}
GetOrQueryUserEntitlements(
UserId,
FOnGetOrQueryUserEntitlementsComplete::CreateWeakLambda(this, [this, LocalUserNum, Equipments, OnComplete]
(const FOnlineError& Error, const TArray<UStoreItemDataObject*> Entitlements)
{
if (!Error.bSucceeded)
{
UE_LOG_ENTITLEMENTS_ESSENTIALS(Warning, "Failed to update user equipment. Error %s: %s", *Error.ErrorCode, *Error.ErrorMessage.ToString());
OnComplete.ExecuteIfBound(Error, FPlayerEquipments());
return;
}
if (!GetWorld())
{
UE_LOG_ENTITLEMENTS_ESSENTIALS(Warning, "Failed to update user equipment. World is invalid");
OnComplete.ExecuteIfBound(
FOnlineError::CreateError(TEXT(""), EOnlineErrorResult::InvalidResults, TEXT(""), FText::FromString(TEXT("World is invalid."))),
FPlayerEquipments());
return;
}
UAccelByteWarsGameInstance* GameInstance = Cast<UAccelByteWarsGameInstance>(GetWorld()->GetGameInstance());
if (!GameInstance)
{
UE_LOG_ENTITLEMENTS_ESSENTIALS(Warning, "Failed to update user equipment. Game instance is invalid");
OnComplete.ExecuteIfBound(
FOnlineError::CreateError(TEXT(""), EOnlineErrorResult::InvalidResults, TEXT(""), FText::FromString(TEXT("Game instance is invalid."))),
FPlayerEquipments());
return;
}
const TArray<FString> ItemIds =
{
Equipments.SkinId,
Equipments.ColorId,
Equipments.ExplosionFxId,
Equipments.MissileTrailFxId,
Equipments.PowerUpId
};
// Check whether the items are owned.
TMap<FString, int32> EquippedItems;
const EItemSkuPlatform PlatformSku = EItemSkuPlatform::AccelByte;
for (const FString& ItemId : ItemIds)
{
const UInGameItemDataAsset* ItemDataAsset = UInGameItemUtility::GetItemDataAsset(ItemId);
if (!ItemDataAsset || !ItemDataAsset->SkuMap.Contains(PlatformSku))
{
continue;
}
for (const UStoreItemDataObject* Entitlement : Entitlements)
{
const bool bIsOwned = Entitlement && Entitlement->GetSku(PlatformSku).Equals(ItemDataAsset->SkuMap[PlatformSku]);
if (bIsOwned)
{
EquippedItems.Add(ItemDataAsset->Id, Entitlement->GetIsConsumable() ? Entitlement->GetCount() : 1);
break;
}
}
}
// Update equipped items.
CurrentEquipments = Equipments;
GameInstance->UnEquipAll(LocalUserNum);
GameInstance->UpdateEquippedItemsByInGameItemId(LocalUserNum, EquippedItems);
UE_LOG_ENTITLEMENTS_ESSENTIALS(Log, "Success to update user equipment");
OnComplete.ExecuteIfBound(FOnlineError::Success(), Equipments);
}));
} -
次に、
GetUserEquipments()関数を定義します。この関数は、提供されたレコードキーを使用して Cloud Save からプレイヤーの装備アイテム ID を取得します。void UEntitlementsEssentialsSubsystem_Starter::GetUserEquipments(
const int32 LocalUserNum,
const FUniqueNetIdPtr UserId,
const FOnUpdateUserEquipmentsComplete& OnComplete)
{
if (!UserId)
{
UE_LOG_ENTITLEMENTS_ESSENTIALS(Warning, "Failed to get user equipments. User ID is invalid.");
OnComplete.ExecuteIfBound(
FOnlineError::CreateError(TEXT(""), EOnlineErrorResult::InvalidParams, TEXT(""), FText::FromString(TEXT("User ID is invalid."))),
FPlayerEquipments());
return;
}
OnGetUserEquipmentsCompleteDelegateHandle =
CloudSaveInterface->AddOnGetUserRecordCompletedDelegate_Handle(
LocalUserNum,
FOnGetUserRecordCompletedDelegate::CreateUObject(
this,
&ThisClass::OnGetUserEquipmentsComplete,
UserId.ToSharedRef(), OnComplete));
CloudSaveInterface->GetUserRecord(LocalUserNum, USER_EQUIPMENT_KEY); -
次に、装備データが受信されたときに処理するコールバック関数を定義します。この関数は、ゲーム内でアイテムを装備する前にレコードを解析します。
<!--SNIPEND-->
<!--SNIPSTART EntitlementsEssentialsSubsystem_Starter.cpp-OnGetUserEquipmentsComplete {"numberOfLeadingSpaces":4}-->
```cpp
void UEntitlementsEssentialsSubsystem_Starter::OnGetUserEquipmentsComplete(
const int32 LocalUserNum,
const FOnlineError& Result,
const FString& Key,
const FAccelByteModelsUserRecord& Record,
const FUniqueNetIdRef UserId,
const FOnUpdateUserEquipmentsComplete OnComplete)
{
CloudSaveInterface->ClearOnGetUserRecordCompletedDelegate_Handle(LocalUserNum, OnGetUserEquipmentsCompleteDelegateHandle);
if (!Result.bSucceeded)
{
UE_LOG_ENTITLEMENTS_ESSENTIALS(Warning, "Failed to get user equipment. Error %s: %s", *Result.ErrorCode, *Result.ErrorMessage.ToString());
OnComplete.ExecuteIfBound(Result, FPlayerEquipments());
return;
}
TSharedPtr<FJsonObject> JsonObject = Record.Value.JsonObject;
if (JsonObject.IsValid() == false)
{
UE_LOG_ENTITLEMENTS_ESSENTIALS(Warning, "Failed to get user equipment. Record object is invalid.");
OnComplete.ExecuteIfBound(
FOnlineError::CreateError(TEXT(""), EOnlineErrorResult::InvalidResults, TEXT(""), FText::FromString(TEXT("Record object is invalid."))),
FPlayerEquipments());
return;
}
FPlayerEquipments Equipments;
if (!FJsonObjectConverter::JsonObjectToUStruct(JsonObject.ToSharedRef(), FPlayerEquipments::StaticStruct(), &Equipments, 0, 0))
{
UE_LOG_ENTITLEMENTS_ESSENTIALS(Warning, "Failed to get user equipment. Unable to parse record to PlayerEquipments struct.");
OnComplete.ExecuteIfBound(
FOnlineError::CreateError(TEXT(""), EOnlineErrorResult::InvalidResults, TEXT(""), FText::FromString(TEXT("Unable to parse record."))),
FPlayerEquipments());
return;
}
UE_LOG_ENTITLEMENTS_ESSENTIALS(Log, "Success to get user equipment");
UpdateUserEquipments(LocalUserNum, UserId, Equipments, OnComplete);
} -
SetUserEquipments()関数を定義します。この関数は、装備アイテム ID を Cloud Save に保存してデータを永続化します。void UEntitlementsEssentialsSubsystem_Starter::SetUserEquipments(
const int32 LocalUserNum,
const FUniqueNetIdPtr UserId,
const FPlayerEquipments& Equipments,
const FOnUpdateUserEquipmentsComplete& OnComplete)
{
if (!UserId)
{
UE_LOG_ENTITLEMENTS_ESSENTIALS(Warning, "Failed to set user equipments. User ID is invalid.");
OnComplete.ExecuteIfBound(
FOnlineError::CreateError(TEXT(""), EOnlineErrorResult::InvalidParams, TEXT(""), FText::FromString(TEXT("User ID is invalid."))),
FPlayerEquipments());
return;
}
TSharedPtr<FJsonObject> JsonObject = MakeShared<FJsonObject>();
if (!FJsonObjectConverter::UStructToJsonObject(FPlayerEquipments::StaticStruct(), &Equipments, JsonObject.ToSharedRef(), 0, 0))
{
UE_LOG_ENTITLEMENTS_ESSENTIALS(Warning, "Failed to set user equipment. Unable to parse record to Json object.");
OnComplete.ExecuteIfBound(
FOnlineError::CreateError(TEXT(""), EOnlineErrorResult::InvalidResults, TEXT(""), FText::FromString(TEXT("Unable to parse record."))),
FPlayerEquipments());
return;
}
OnSetUserEquipmentsCompleteDelegateHandle =
CloudSaveInterface->AddOnReplaceUserRecordCompletedDelegate_Handle(
LocalUserNum,
FOnReplaceUserRecordCompletedDelegate::CreateUObject(
this,
&ThisClass::OnSetUserEquipmentsComplete,
UserId.ToSharedRef(),
FPlayerEquipments(Equipments),
OnComplete));
CloudSaveInterface->ReplaceUserRecord(LocalUserNum, USER_EQUIPMENT_KEY, JsonObject.ToSharedRef().Get());
} -
次に、装備データが保存されたときに処理するコールバック関数を定義します。成功した場合、ゲーム内でアイテムを装備しようとします。
void UEntitlementsEssentialsSubsystem_Starter::OnSetUserEquipmentsComplete(
int32 LocalUserNum,
const FOnlineError& Result,
const FString& Key,
const FUniqueNetIdRef UserId,
const FPlayerEquipments Equipments,
const FOnUpdateUserEquipmentsComplete OnComplete)
{
CloudSaveInterface->ClearOnReplaceUserRecordCompletedDelegate_Handle(LocalUserNum, OnSetUserEquipmentsCompleteDelegateHandle);
if (!Result.bSucceeded)
{
UE_LOG_ENTITLEMENTS_ESSENTIALS(Warning, "Failed to set user equipment. Error %s: %s", *Result.ErrorCode, *Result.ErrorMessage.ToString());
OnComplete.ExecuteIfBound(Result, FPlayerEquipments());
return;
}
UE_LOG_ENTITLEMENTS_ESSENTIALS(Log, "Success to set user equipment");
UpdateUserEquipments(LocalUserNum, UserId, Equipments, OnComplete);
} -
Initialize()関数で、以下のコードを追加してロビーが接続されたときにGetUserEquipments()を呼び出します。これにより、プレイヤーがログインまたは再接続するたびに、装備が適切に更新されます。void UEntitlementsEssentialsSubsystem_Starter::Initialize(FSubsystemCollectionBase& Collection)
{
// ...
IdentityInterface->AddOnConnectLobbyCompleteDelegate_Handle(0, FOnConnectLobbyCompleteDelegate::CreateWeakLambda(this, [this]
(int32 LocalUserNum, bool bWasSuccessful, const FUniqueNetId& UserId, const FString& Error)
{
if (bWasSuccessful)
{
GetUserEquipments(LocalUserNum, UserId.AsShared());
}
}));
// ...
} -
Deinitialize()関数で、以下のコードを追加してこのクラスからすべてのロビー接続イベントのバインドを解除します。void UEntitlementsEssentialsSubsystem_Starter::Deinitialize()
{
// ...
if (IdentityInterface.IsValid())
{
IdentityInterface->ClearOnConnectLobbyCompleteDelegates(0, this);
}
// ...
}
リソース
Resources
- このチュートリアルセクションで使用されるファイルは、Byte Wars GitHub リポジトリで入手できます。