サブシステムの実装 - ゲーム内ストアの基本 - (Unreal Engine モジュール)
注釈:本資料はAI技術を用いて翻訳されています。
ゲームのセットアップ
Byte Wars では、SKU を使用してゲーム内アイテムとストアアイテムをリンクしています。AccelByteWars/Content/ByteWars/InGameItems 配下の任意の Data Asset を見ると、Byte Wars のアイテムがどのように設定されているかがわかります。例えば、DA_ByteBomb には、各プラットフォーム用に 3 つの異なる SKU があります。これは、命名規則やその他の制限により、単一の SKU をプラットフォーム間で使用できない場合に便利です。このセットアップにより、各ゲーム内アイテムを対応するストアアイテムに接続できます。

ID または SKU でゲーム内アイテムを取得する関数は、Source/AccelByteWars/Core/AssetManager/InGameItems/InGameItemUtility.h で利用できます。
public:
UFUNCTION(BlueprintPure, Category = "Item")
static UInGameItemDataAsset* GetItemDataAsset(const FString& ItemId);
UFUNCTION(BlueprintPure, Category = "Item")
static UInGameItemDataAsset* GetItemDataAssetBySku(
const EItemSkuPlatform Platform,
const FString& Sku);
サブシステムの展開
Byte Wars では、InGameStoreEssentialsSubsystem という Game Instance Subsystem を使用して、AccelByte Gaming Services (AGS) Online Subsystem (OSS) をラップしています。このサブシステムは、Unreal Engine の OSS が提供する IOnlineStoreV2Ptr を利用しており、AGS OSS と統合されています。このチュートリアルでは、サブシステムのスターターバージョンを使用して、必要な関数をゼロから実装します。
ストア機能は 2 つの部分に分かれています。カテゴリの取得とストアアイテムの取得です。各部分には、get 関数(保存された値を返す)と query 関数(バックエンドにリクエストを送る)の両方が含まれています。以下の図は、Byte Wars がこれら 2 つの機能をどのように実装しているかを示しています。
スターターパックの内容
このチュートリアルに従うために、InGameStoreEssentialsSubsystem_Starter という名前のスターターサブシステムクラスが用意されています。リソースセクションで見つけることができます。以下のファイルが含まれています。
- ヘッダーファイル:
Source/AccelByteWars/TutorialModules/Monetization/InGameStoreEssentials/InGameStoreEssentialsSubsystem_Starter.h - CPP ファイル:
Source/AccelByteWars/TutorialModules/Monetization/InGameStoreEssentials/InGameStoreEssentialsSubsystem_Starter.cpp
InGameStoreEssentialsSubsystem_Starter クラスには、いくつかの便利なコンポーネントが含まれています。
- Unreal Engine の OSS
IOnlineStoreV2Ptrインターフェースの宣言と初期化。これにより、AGS SDK の機能にアクセスできます。
private:
IOnlineStoreV2Ptr StoreInterface;
void UInGameStoreEssentialsSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
// ...
const IOnlineSubsystem* Subsystem = Online::GetSubsystem(GetWorld());
ensure(Subsystem);
StoreInterface = Subsystem->GetStoreV2Interface();
ensure(StoreInterface);
}
- Player Controller から一意の Net ID を取得するためのヘルパー関数。これは、このサブシステムの主要なユーザーであるウィジェットが、プレイヤーを参照するために Player Controller を使用するために必要です。一方、OSS インターフェースは、リクエストしているユーザーを識別するために一意の Net ID を必要とします。
private:
// ...
FUniqueNetIdPtr GetUniqueNetIdFromPlayerController(const APlayerController* PlayerController) const;
- OSS インターフェースからのストアアイテムデータオブジェクトを Byte Wars 固有のデータオブジェクトに変換するヘルパー関数。これらのカスタムオブジェクトは、ゲームのロジックに最適化されています。
public:
static UStoreItemDataObject* ConvertStoreData(const FOnlineStoreOffer& Offer);
さらに、Source/AccelByteWars/TutorialModules/Monetization/InGameStoreEssentials/InGameStoreEssentialsModel.h にモデルファイルがあり、バックエンドレスポンスを処理するために使用されるデリゲートが定義されています。
DECLARE_DELEGATE_OneParam(FOnGetOrQueryOffersByCategory, TArray<UStoreItemDataObject*> /*Offers*/)
DECLARE_DELEGATE_OneParam(FOnGetOrQueryOfferById, UStoreItemDataObject* /*Offer*/)
DECLARE_DELEGATE_OneParam(FOnGetOrQueryCategories, TArray<FOnlineStoreCategory> /*Category*/)
また、URL から画像を取得する画像ビューアウィジェットが Source/AccelByteWars/Core/UI/Components/AccelByteWarsAsyncImageWidget.h にあります。
public:
/**
* Retrieve image from the given URL. Will use DefaultBrush if URL isn't valid
* @param ImageUrl URL to retrieve image from
*/
UFUNCTION(BlueprintCallable)
void LoadImage(const FString& ImageUrl);
クエリカテゴリの実装
前のセクションで説明したように、サブシステムは状態に応じて query または get を呼び出します。この関数の出力はデリゲートであるため、呼び出し元はこれらのシナリオを個別に処理する必要はありません。
-
InGameStoreEssentialsSubsystem_Starterヘッダーファイルを開き、バックエンドレスポンスを待っている間に保留中のリクエストを保持する次のデリゲートを宣言します。private:
// ...
TMultiMap<const FString /*CategoryPath*/, FOnGetOrQueryCategories> CategoriesByRootPathDelegates; -
引き続きヘッダーファイルで、クエリカテゴリが現在実行中かどうかを示す変数を宣言します。
private:
// ...
bool bIsQueryCategoriesRunning = false; -
ストアインターフェースと対話する関数を宣言します。
private:
// ...
TArray<FOnlineStoreCategory> GetCategories(const FString& RootPath) const;
// ...
void QueryCategories(const FUniqueNetIdPtr UserId);
void OnQueryCategoriesComplete(bool bWasSuccessful, const FString& Error); -
他のオブジェクトがこのサブシステムと対話するために呼び出すパブリック関数を宣言します。
public:
// ...
void GetOrQueryCategoriesByRootPath(
const FUniqueNetIdPtr UserId,
const FString& RootPath,
FOnGetOrQueryCategories OnComplete,
bool bForceRefresh = false); -
InGameStoreEssentialsSubsystem_StarterCPP ファイルに移動し、GetCategories()関数を実装します。これは、指定されたルートパスに一致するキャッシュされたカテゴリを取得します。すべてのカテゴリを取得するには/を使用します。TArray<FOnlineStoreCategory> UInGameStoreEssentialsSubsystem_Starter::GetCategories(const FString& RootPath) const
{
TArray<FOnlineStoreCategory> Categories;
StoreInterface->GetCategories(Categories);
TArray<FOnlineStoreCategory> ChildCategories;
for (FOnlineStoreCategory& Category : Categories)
{
if (Category.Id.Find(RootPath) == 0)
{
ChildCategories.Add(Category);
}
}
return ChildCategories;
} -
OnQueryCategoriesComplete()関数を実装して、バックエンドレスポンスを処理し、すべての保留中のリクエストをトリガーします。void UInGameStoreEssentialsSubsystem_Starter::OnQueryCategoriesComplete(bool bWasSuccessful, const FString& Error)
{
bIsQueryCategoriesRunning = false;
TArray<const FString*> KeysToDelete;
for (TTuple<const FString, FOnGetOrQueryCategories>& Delegate : CategoriesByRootPathDelegates)
{
Delegate.Value.Execute(GetCategories(Delegate.Key));
// Avoid modifying while it still being used.
KeysToDelete.Add(&Delegate.Key);
}
// Delete delegates.
for (const FString* Key : KeysToDelete)
{
CategoriesByRootPathDelegates.Remove(*Key);
}
} -
QueryCategories()関数を実装して、バックエンドからカテゴリをリクエストします。void UInGameStoreEssentialsSubsystem_Starter::QueryCategories(const FUniqueNetIdPtr UserId)
{
// Abort if the query process is already running.
if (bIsQueryCategoriesRunning)
{
return;
}
// If bound, meaning NativePlatformPurchaseSubsystem is active
if (FNativePlatformPurchaseUtils::OnQueryItemMapping.IsBound())
{
FNativePlatformPurchaseUtils::OnQueryItemMapping.Execute(UserId, FOnQueryItemMappingCompleted::CreateWeakLambda(this, [this, UserId](const FNativeItemPricingMap& Pricing)
{
StoreInterface->QueryCategories(*UserId.Get(), FOnQueryOnlineStoreCategoriesComplete::CreateUObject(this, &ThisClass::OnQueryCategoriesComplete));
}));
}
// If not bound, meaning the NativePlatformPurchaseSubsystem is not active
else
{
StoreInterface->QueryCategories(*UserId.Get(), FOnQueryOnlineStoreCategoriesComplete::CreateUObject(this, &ThisClass::OnQueryCategoriesComplete));
}
bIsQueryCategoriesRunning = true;
} -
GetOrQueryCategoriesByRootPath()関数を実装します。これは、他のオブジェクトがストアカテゴリを取得するために呼び出す関数です。void UInGameStoreEssentialsSubsystem_Starter::GetOrQueryCategoriesByRootPath(
const FUniqueNetIdPtr UserId,
const FString& RootPath,
FOnGetOrQueryCategories OnComplete,
bool bForceRefresh)
{
// Check overall cache.
TArray<FOnlineStoreCategory> Categories;
StoreInterface->GetCategories(Categories);
// If empty or forced to refresh, query.
if (Categories.IsEmpty() || bForceRefresh)
{
if (!UserId)
{
ExecuteNextTick(FTimerDelegate::CreateWeakLambda(this, [OnComplete]()
{
OnComplete.Execute(TArray<FOnlineStoreCategory>());
}));
return;
}
CategoriesByRootPathDelegates.Add(RootPath, OnComplete);
QueryCategories(UserId);
}
// Else, execute immediately.
else
{
const TArray<FOnlineStoreCategory> StoreCategories = GetCategories(RootPath);
ExecuteNextTick(FTimerDelegate::CreateWeakLambda(this, [OnComplete, StoreCategories]()
{
OnComplete.Execute(StoreCategories);
}));
}
}
クエリストアアイテムの実装
カテゴリのクエリと同様に、ストアインターフェースはストアアイテムを get および query する 2 つの関数を提供します。Byte Wars では、これらのストアインターフェース関数を内部的に使用する 2 つのラッパー関数を作成しました。1 つは AccelByte のストアアイテム ID でストアアイテムを取得し、もう 1 つはカテゴリで取得します。
ストアインターフェースでは、ストアアイテムは「オファー」と呼ばれます。両方の用語は同じ意味です。
-
InGameStoreEssentialsSubsystem_Starterヘッダーファイルを開き、次のデリゲートを宣言します。これらのデリゲートは、ストアインターフェースがバックエンドからのレスポンスを待っている間、保留中のリクエストを保存します。Byte Wars にはカテゴリと ID でクエリする 2 つの別々の関数があるため、対応する 2 つのデリゲートを使用します。private:
// ...
TMultiMap<const FString /*Category*/, FOnGetOrQueryOffersByCategory> OffersByCategoryDelegates;
TMultiMap<const FUniqueOfferId /*OfferId*/, FOnGetOrQueryOfferById> OfferByIdDelegates; -
同じヘッダーファイルで、バックエンドへのクエリオファーが現在進行中かどうかを示す変数を宣言します。
private:
// ...
bool bIsQueryOfferRunning = false; -
ストアインターフェースと対話する次の関数を宣言します。
private:
// ...
TArray<UStoreItemDataObject*> GetOffersByCategory(const FString Category) const;
UStoreItemDataObject* GetOfferById(const FUniqueOfferId& OfferId) const;
// ...
void QueryOffers(const FUniqueNetIdPtr UserId);
void OnQueryOffersComplete(bool bWasSuccessful, const TArray<FUniqueOfferId>& OfferIds, const FString& Error); -
他のオブジェクトがこのサブシステムと対話するために使用する次の関数を宣言します。
public:
void GetOrQueryOffersByCategory(
const FUniqueNetIdPtr UserId,
const FString& Category,
FOnGetOrQueryOffersByCategory OnComplete,
bool bForceRefresh = false);
void GetOrQueryOfferById(
const FUniqueNetIdPtr UserId,
const FUniqueOfferId& OfferId,
FOnGetOrQueryOfferById OnComplete); -
InGameStoreEssentialsSubsystem_StarterCPP ファイルに移動し、GetOffersByCategory()およびGetOfferById()関数を実装します。これらの関数は、ストアインターフェースのGetOffers()関数を介してキャッシュされたストアアイテムを取得します。両者の唯一の違いは、アイテムのフィルタリング方法です。TArray<UStoreItemDataObject*> UInGameStoreEssentialsSubsystem_Starter::GetOffersByCategory(const FString Category) const
{
TArray<FOnlineStoreOfferRef> FilteredOffers;
TArray<UStoreItemDataObject*> StoreItems;
TArray<FOnlineStoreOfferRef> Offers;
StoreInterface->GetOffers(Offers);
for (const FOnlineStoreOfferRef& Offer : Offers)
{
if (Offer->DynamicFields.Find(TEXT("Category"))->Find(Category) == 0)
{
FilteredOffers.Add(Offer);
}
}
for (const TSharedRef<FOnlineStoreOffer>& Offer : FilteredOffers)
{
StoreItems.Add(FInGameStoreEssentialsUtils::ConvertStoreData(Offer.Get()));
}
return StoreItems;
}UStoreItemDataObject* UInGameStoreEssentialsSubsystem_Starter::GetOfferById(const FUniqueOfferId& OfferId) const
{
UStoreItemDataObject* StoreItem = nullptr;
if (const TSharedPtr<FOnlineStoreOffer> Offer = StoreInterface->GetOffer(OfferId); Offer.IsValid())
{
StoreItem = FInGameStoreEssentialsUtils::ConvertStoreData(*Offer.Get());
}
return StoreItem;
} -
OnQueryOffersComplete()関数を実装して、バックエンドレスポンスを処理します。この関数は、OffersByCategoryDelegatesおよびOfferByIdDelegatesデリゲートに保存されているすべての保留中のリクエストを処理し、適切に完了します。void UInGameStoreEssentialsSubsystem_Starter::OnQueryOffersComplete(
bool bWasSuccessful,
const TArray<FUniqueOfferId>& OfferIds,
const FString& Error)
{
bIsQueryOfferRunning = false;
TArray<const FString*> OffersByCategoryDelegateToBeDeleted;
for (TTuple<const FString /*Category*/, FOnGetOrQueryOffersByCategory>& Delegate : OffersByCategoryDelegates)
{
Delegate.Value.Execute(GetOffersByCategory(Delegate.Key));
// Avoid modifying while it still being used.
OffersByCategoryDelegateToBeDeleted.Add(&Delegate.Key);
}
// Delete delegates.
for (const FString* Key : OffersByCategoryDelegateToBeDeleted)
{
OffersByCategoryDelegates.Remove(*Key);
}
TArray<const FUniqueOfferId*> OfferByIdDelegateToBeDeleted;
for (TTuple<const FUniqueOfferId, FOnGetOrQueryOfferById>& Delegate : OfferByIdDelegates)
{
Delegate.Value.Execute(GetOfferById(Delegate.Key));
// Avoid modifying while it still being used.
OfferByIdDelegateToBeDeleted.Add(&Delegate.Key);
}
// Delete delegates.
for (const FUniqueOfferId* Key : OfferByIdDelegateToBeDeleted)
{
OfferByIdDelegates.Remove(*Key);
}
} -
QueryOffers()関数を実装します。これは、ストアインターフェースをトリガーして、バックエンドからすべてのストアアイテムをリクエストします。void UInGameStoreEssentialsSubsystem_Starter::QueryOffers(const FUniqueNetIdPtr UserId)
{
// Abort if the query process is already running.
if (bIsQueryOfferRunning)
{
return;
}
StoreInterface->QueryOffersByFilter(
*UserId.Get(),
FOnlineStoreFilter(),
FOnQueryOnlineStoreOffersComplete::CreateUObject(this, &ThisClass::OnQueryOffersComplete));
bIsQueryOfferRunning = true;
} -
GetOrQueryOffersByCategory()およびGetOrQueryOfferById()関数を実装します。これらは、他のオブジェクトがカテゴリまたは ID に基づいてストアアイテムを取得するために呼び出すパブリック関数です。void UInGameStoreEssentialsSubsystem_Starter::GetOrQueryOffersByCategory(
const FUniqueNetIdPtr UserId,
const FString& Category,
FOnGetOrQueryOffersByCategory OnComplete,
bool bForceRefresh)
{
// Check overall cache.
TArray<FOnlineStoreOfferRef> Offers;
StoreInterface->GetOffers(Offers);
// If empty or forced to refresh, call query.
if (Offers.IsEmpty() || bForceRefresh)
{
if (!UserId)
{
ExecuteNextTick(FTimerDelegate::CreateWeakLambda(this, [OnComplete]()
{
OnComplete.Execute(TArray<UStoreItemDataObject*>());
}));
return;
}
OffersByCategoryDelegates.Add(Category, OnComplete);
QueryOffers(UserId);
}
// Else, call get.
else
{
const TArray<UStoreItemDataObject*> StoreItemDataObjects = GetOffersByCategory(Category);
ExecuteNextTick(FTimerDelegate::CreateWeakLambda(this, [OnComplete, StoreItemDataObjects]()
{
OnComplete.Execute(StoreItemDataObjects);
}));
}
}void UInGameStoreEssentialsSubsystem_Starter::GetOrQueryOfferById(
const FUniqueNetIdPtr UserId,
const FUniqueOfferId& OfferId,
FOnGetOrQueryOfferById OnComplete)
{
// Check overall cache.
TArray<FOnlineStoreOfferRef> Offers;
StoreInterface->GetOffers(Offers);
// If empty, call query.
if (Offers.IsEmpty())
{
if (!UserId)
{
ExecuteNextTick(FTimerDelegate::CreateWeakLambda(this, [OnComplete]()
{
OnComplete.Execute(nullptr);
}));
return;
}
OfferByIdDelegates.Add(OfferId, OnComplete);
QueryOffers(UserId);
}
// Else, call get.
else
{
UStoreItemDataObject* StoreItemDataObject = GetOfferById(OfferId);
ExecuteNextTick(FTimerDelegate::CreateWeakLambda(this, [OnComplete, StoreItemDataObject]()
{
OnComplete.Execute(StoreItemDataObject);
}));
}
}
リソース
- このチュートリアルセクションで使用されているファイルは、Byte Wars GitHub リポジトリで入手できます。