Economy v2、Unity、Android の概要

Important

エコノミー v2 が一般提供になりました。 サポートとフィードバックについては、 PlayFab フォーラムにアクセスしてください。

このチュートリアルでは、PlayFab、Unity + IAP サービス、Android 課金 API を使用して、アプリ内購入 (IAP) を設定する方法を説明します。

開始の前に

以下の図は、Android 課金 API と PlayFab がどのように連動し、顧客に安定した IAP 体験を提供するかを示しています。

PlayFab Economy v2 - 引き換えタイムライン

まず、PlayMarket で製品 ID価格を設定します。 最初は、すべての製品が特定化されていません。プレイヤーが購入可能な単なるデジタル エンティティであり、PlayFab プレイヤーにとって何の意味もありません。

これらのエンティティを有益化するには、PlayFab アイテム カタログでミラー化する必要があります。 これによって、特定化されていないエンティティがバンドル、コンテナー、個別のアイテムとなります。

それぞれに次のような独自の側面があります:

  • タイトル
  • 説明
  • Tags
  • types
  • Images
  • ビヘイビア によって特徴付けられます。

ID を共有することで、これらすべてがマーケットの製品にリンクされます。

購入可能な実際の金額のアイテムにアクセスする最適な方法は、GetItems を使用することです。

アイテムの ID は、PlayFab と外部 IAP システム間のリンクです。 したがって、IAP サービスにアイテム ID を渡します。

この時点で、購入プロセスが開始します。 プレイヤーが IAP インターフェイスと通信し、購入が成功すると、領収書を取得できます。

PlayFab はその後その領収書を検証し、購入を登録して、購入アイテムをプレイヤーに付与します。

クライアント アプリケーションを設定する

このセクションでは、アプリケーションを構成し、PlayFab、UnityIAP、 Android 課金 API を使用した IAP をテストする方法を説明します。

前提条件:

  • Unity プロジェクト。
  • PlayFab Unity SDK がインポートされ、タイトルに対して動作するように構成されました。
  • Visual Studio のようなエディターがインストールされ、Unity プロジェクトで動作するように構成されました。

まずは UnityIAP を設定します。

  1. [サービス] に移動します。
  2. [サービス] タブが選択されていることを確認してください。
  3. 自分の Unity サービスのプロフィールまたは組織を選択します。
  4. [作成] を選択します。

UnityIAP サービスを設定する

  1. 次に、[アプリ内購入 (IAP)] サービスに移動します。

UnityIAP サービスに移動する

  1. [Simplify cross-platform IAP (プラットフォーム間 IAP の簡素化)] の切り替えをオンにして、サービス を有効にします。

  2. 次に、[Continue (続ける)] を選択します。

UnityIAP サービスを有効化する

プラグインの一覧ページが表示されます。

  1. [Import (インポート)] を選択します。

UnityIAP サービス - プラグインのインポート

Uすべてのプラグインがインポートされる段階まで Unity のインストールとインポート手順を続行します。

  1. プラグインが揃っていることを確認してください。
  2. 次に AndroidIAPExample.cs という名前の新しいスクリプトを作成します。

UnityIAP 新しいスクリプトを作成する

AndroidIAPExample.cs には、次のコードが含まれます (詳細については、コードのコメントを参照してください)。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.Purchasing;
using UnityEngine.Purchasing.Extension;

using PlayFab;
using PlayFab.ClientModels;
using PlayFab.EconomyModels;

using CatalogItem = PlayFab.EconomyModels.CatalogItem;

/**
 * Unity behavior that implements the the Unity IAP Store interface.
 * 
 * Attach as an asset to your Scene.
 */
public class AndroidIAPExample : MonoBehaviour, IDetailedStoreListener
{

    // Bundles for sale on the Google Play Store.
    private Dictionary<string, PlayFab.EconomyModels.CatalogItem> GooglePlayCatalog;

    // In-game items for sale at the example vendor.
    private Dictionary<string, PlayFab.EconomyModels.CatalogItem> StorefrontCatalog;

    private string purchaseIdempotencyId = null;

    private PlayFabEconomyAPIAsyncResult lastAPICallResult = null;

    private static PlayFabEconomyAPIAsync economyAPI = new();

    private static IStoreController m_StoreController;

    /**
     * True if the Store Controller, extensions, and Catalog are set.
     */
    public bool IsInitialized
    {
        get
        {
            return m_StoreController != null
                && GooglePlayCatalog != null
                && StorefrontCatalog != null;
        }
    }

    /**
     * Returns false as this is just sample code.
     * 
     * @todo Implement this functionality for your game.
     */
    public bool UserHasExistingSave
    {
        get
        {
            return false;
        }
    }

    /**
     * Integrates game purchasing with the Unity IAP API.
     */
    public void BuyProductByID(string productID)
    {
        if (!IsInitialized) throw new Exception("IAP Service is not initialized!");

        m_StoreController.InitiatePurchase(productID);
    }

    /**
     * Purchases a PlayFab inventory item by ID.
     * 
     * @see the PlayFabEconomyAPIAsync class for details on error handling
     * and calling patterns.
     */
    async public Task<bool> PlayFabPurchaseItemByID(string itemID, PlayFabEconomyAPIAsyncResult result)
    {
        if (!IsInitialized) throw new Exception("IAP Service is not initialized!");

        Debug.Log("Player buying product " + itemID);

        if (string.IsNullOrEmpty(purchaseIdempotencyId))
        {
            purchaseIdempotencyId = Guid.NewGuid().ToString();
        }

        GetItemRequest getVillagerStoreRequest = new GetItemRequest()
        {
            AlternateId = new CatalogAlternateId()
            {
                Type = "FriendlyId",
                Value = "villagerstore"
            }
        };
        GetItemResponse getStoreResponse = await economyAPI.getItemAsync(getVillagerStoreRequest);
        if (getStoreResponse == null || string.IsNullOrEmpty(getStoreResponse?.Item?.Id))
        {
            result.error = "Unable to contact the store. Check your internet connection and try again in a few minutes.";
            return false;
        }

        CatalogPriceAmount price = StorefrontCatalog.FirstOrDefault(item => item.Key == itemID).Value.PriceOptions.Prices.FirstOrDefault().Amounts.FirstOrDefault();
        PurchaseInventoryItemsRequest purchaseInventoryItemsRequest = new PurchaseInventoryItemsRequest()
        {
            Amount = 1,
            Item = new InventoryItemReference()
            {
                Id = itemID
            },
            PriceAmounts = new List<PurchasePriceAmount>
            {
                new PurchasePriceAmount()
                {
                    Amount = price.Amount,
                    ItemId = price.ItemId
                }
            },
            IdempotencyId = purchaseIdempotencyId,
            StoreId = getStoreResponse.Item.Id
        };
        PurchaseInventoryItemsResponse purchaseInventoryItemsResponse = await economyAPI.purchaseInventoryItemsAsync(purchaseInventoryItemsRequest);
        if (purchaseInventoryItemsResponse == null || purchaseInventoryItemsResponse?.TransactionIds.Count < 1)
        {
            result.error = "Unable to purchase. Try again in a few minutes.";
            return false;
        }

        purchaseIdempotencyId = "";
        result.message = "Purchasing!";
        return true;
    }

    private void InitializePurchasing()
    {
        if (IsInitialized) return;

        var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance(AppStore.GooglePlay));

        foreach (CatalogItem item in GooglePlayCatalog.Values)
        {
            var googlePlayItemId = item.AlternateIds.FirstOrDefault(item => item.Type == "GooglePlay")?.Value;
            if (!googlePlayItemId.IsUnityNull()) {
                builder.AddProduct(googlePlayItemId, ProductType.Consumable);
            }
        }

        UnityPurchasing.Initialize(this, builder);
    }

    /**
     * Attempts to log the player in via the Android Device ID.
     */
    private void Login()
    {
        // Best practice is to soft-login with a unique ID, then prompt the player to finish 
        // creating a PlayFab account in order to retrive cross-platform saves or other benefits.
        if (UserHasExistingSave)
        {
            // @todo Integrate this with the save system.
            LoginWithPlayFabRequest loginWithPlayFabRequest = new()
            {
                Username = "",
                Password = ""
            };
            PlayFabClientAPI.LoginWithPlayFab(loginWithPlayFabRequest, OnRegistration, OnPlayFabError);
            return;
        }

        // AndroidDeviceID will prompt for permissions on newer devices.
        // Using a non-device specific GUID and saving to a local file
        // is a better approach. PlayFab does allow you to link multiple
        // Android device IDs to a single PlayFab account.
        PlayFabClientAPI.LoginWithAndroidDeviceID(new LoginWithAndroidDeviceIDRequest()
        {
            CreateAccount = true,
            AndroidDeviceId = SystemInfo.deviceUniqueIdentifier
        }, result => {
            RefreshIAPItems();
        }, error => Debug.LogError(error.GenerateErrorReport()));
    }

    /**
     * Draw a debug IMGUI for testing examples.
     *
     * Use UI Toolkit for your production game runtime UI instead.
     */
    public void OnGUI()
    {
        // Support high-res devices.
        GUI.matrix = Matrix4x4.TRS(new Vector3(0, 0, 0), Quaternion.identity, new Vector3(3, 3, 3));

        if (!IsInitialized)
        {
            GUILayout.Label("Initializing IAP and logging in...");
            return;
        }

        if (!string.IsNullOrEmpty(purchaseIdempotencyId)
            && (!string.IsNullOrEmpty(lastAPICallResult?.message)
                || !string.IsNullOrEmpty(lastAPICallResult?.error)))
        {
            GUILayout.Label(lastAPICallResult?.message + lastAPICallResult?.error);
        }

        GUILayout.Label("Shop for game currency bundles.");
        // Draw a purchase menu for each catalog item.
        foreach (CatalogItem item in GooglePlayCatalog.Values)
        {
            // Use a dictionary to select the proper language.
            if (GUILayout.Button("Get " + (item.Title.ContainsKey("en-US") ? item.Title["en-US"] : item.Title["NEUTRAL"])))
            {
                BuyProductByID(item.AlternateIds.FirstOrDefault(item => item.Type == "GooglePlay").Value);
            }
        }

        GUILayout.Label("Hmmm. (Translation: Welcome to my humble Villager store.)");
        // Draw a purchase menu for each catalog item.
        foreach (CatalogItem item in StorefrontCatalog.Values)
        {
            // Use a dictionary to select the proper language.
            if (GUILayout.Button("Buy "
                + (item.Title.ContainsKey("en-US") ? item.Title["en-US"] : item.Title["NEUTRAL"]
                + ": "
                + item.PriceOptions.Prices.FirstOrDefault().Amounts.FirstOrDefault().Amount.ToString()
                + " Diamonds"
                )))
            {
                PlayFabPurchaseItemByID(item.Id, lastAPICallResult);
            }
        }
    }

    private void OnRegistration(LoginResult result)
    {
        PlayFabSettings.staticPlayer.ClientSessionTicket = result.SessionTicket;
    }

    public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
    {
        m_StoreController = controller;
    }

    public void OnInitializeFailed(InitializationFailureReason error)
    {
        Debug.Log("OnInitializeFailed InitializationFailureReason:" + error);
    }

    public void OnInitializeFailed(InitializationFailureReason error, string message)
    {
        Debug.Log("OnInitializeFailed InitializationFailureReason:" + error + message);
    }

    private void OnPlayFabError(PlayFabError error)
    {
        Debug.LogError(error.GenerateErrorReport());
    }

    public void OnPurchaseFailed(UnityEngine.Purchasing.Product product, PurchaseFailureReason failureReason)
    {
        Debug.Log(string.Format("OnPurchaseFailed: FAIL. Product: '{0}', PurchaseFailureReason: {1}",
            product.definition.storeSpecificId, failureReason));
    }

    public void OnPurchaseFailed(UnityEngine.Purchasing.Product product, PurchaseFailureDescription failureDescription)
    {
        Debug.Log(string.Format("OnPurchaseFailed: FAIL. Product: '{0}', PurchaseFailureReason: {1}",
            product.definition.storeSpecificId, failureDescription));
    }

    /**
     * Callback for Store purchases.
     * 
     * @note This code does not account for purchases that were pending and are
     *   delivered on application start. Production code should account for these
     *   cases.
     *
     * @see https://docs.unity3d.com/Packages/com.unity.purchasing@4.8/api/UnityEngine.Purchasing.PurchaseProcessingResult.html
     */
    public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs purchaseEvent)
    {
        if (!IsInitialized)
        {
            return PurchaseProcessingResult.Complete;
        }

        if (purchaseEvent.purchasedProduct == null)
        {
            Debug.LogWarning("Attempted to process purchase with unknown product. Ignoring.");
            return PurchaseProcessingResult.Complete;
        }

        if (string.IsNullOrEmpty(purchaseEvent.purchasedProduct.receipt))
        {
            Debug.LogWarning("Attempted to process purchase with no receipt. Ignoring.");
            return PurchaseProcessingResult.Complete;
        }

        Debug.Log("Attempting purchase with receipt " + purchaseEvent.purchasedProduct.receipt.Serialize().ToString());
        GooglePurchase purchasePayload = GooglePurchase.FromJson(purchaseEvent.purchasedProduct.receipt);
        RedeemGooglePlayInventoryItemsRequest request = new()
        {
            Purchases = new List<GooglePlayProductPurchase> {
                new GooglePlayProductPurchase() {
                    ProductId = purchasePayload.PayloadData?.JsonData?.productId,
                    Token = purchasePayload.PayloadData?.signature
                }
            }
        };
        RedeemGooglePlayInventoryItemsResponse redeemResponse = new();
        PlayFabEconomyAPI.RedeemGooglePlayInventoryItems(request, result => {
            redeemResponse = result;
            Debug.Log("Processed receipt validation.");
        },
            error => Debug.Log("Validation failed: " + error.GenerateErrorReport()));
        if (redeemResponse?.Failed.Count > 0)
        {
            Debug.Log("Validation failed for " + redeemResponse.Failed.Count + " receipts.");
            Debug.Log(redeemResponse.Failed.Serialize().ToSafeString());
            return PurchaseProcessingResult.Pending;
        }
        else
        {
            Debug.Log("Validation succeeded!");
        }

        return PurchaseProcessingResult.Complete;
    }

    /**
     * Queries the PlayFab Economy Catalog V2 for updated listings
     * and then fills the local catalog objects.
     */
    private async void RefreshIAPItems()
    {
        GooglePlayCatalog = new Dictionary<string, PlayFab.EconomyModels.CatalogItem>();
        SearchItemsRequest playCatalogRequest = new()
        {
            Count = 50,
            Filter = "Platforms/any(platform: platform eq 'GooglePlay')"
        };
        SearchItemsResponse playCatalogResponse;
        do
        {
            playCatalogResponse = await economyAPI.searchItemsAsync(playCatalogRequest);
            Debug.Log("Search response: " + playCatalogResponse.Serialize().ToSafeString());
            foreach (CatalogItem item in playCatalogResponse.Items)
            {
                GooglePlayCatalog.Add(item.Id, item);
            }
        } while (!string.IsNullOrEmpty(playCatalogResponse.ContinuationToken));
        Debug.Log("Completed pulling from PlayFab Economy v2 googleplay Catalog: "
            + GooglePlayCatalog.Count()
            + " items retrieved");

        StorefrontCatalog = new Dictionary<string, PlayFab.EconomyModels.CatalogItem>();
        GetItemRequest storeCatalogRequest = new()
        {
            AlternateId = new CatalogAlternateId()
            {
                Type = "FriendlyId",
                Value = "villagerstore"
            }
        };
        GetItemResponse storeCatalogResponse;
        storeCatalogResponse = await economyAPI.getItemAsync(storeCatalogRequest);
        List<string> itemIds = new() { };
        foreach (CatalogItemReference item in storeCatalogResponse.Item.ItemReferences)
        {
            itemIds.Add(item.Id);
        }
        GetItemsRequest itemsCatalogRequest = new()
        {
            Ids = itemIds
        };
        GetItemsResponse itemsCatalogResponse = await economyAPI.getItemsAsync(itemsCatalogRequest);
        foreach (CatalogItem item in itemsCatalogResponse.Items)
        {
            StorefrontCatalog.Add(item.Id, item);
        }
        Debug.Log("Completed pulling from PlayFab Economy v2 villagerstore store: "
            + StorefrontCatalog.Count()
            + " items retrieved");
        
        InitializePurchasing();
    }

    // Start is called before the first frame update.
    public void Start()
    {
        Login();
    }

    // Update is called once per frame.
    public void Update() { }
}

// Utility classes for the sample.
public class PlayFabEconomyAPIAsyncResult
{
    public string error = null;

    public string message = null;
}

/**
 * Example Async wrapper for PlayFab API's.
 * 
 * This is just a quick sample for example purposes.
 * 
 * Write your own customer Logger implementation to log and handle errors
 * for user-facing scenarios. Use tags and map which PlayFab errors require your
 * game to handle GUI or gameplay updates vs which should be logged to crash and
 * error reporting services.
 */
public class PlayFabEconomyAPIAsync
{
    // @see https://learn.microsoft.com/en-us/rest/api/playfab/economy/catalog/get-item
    private TaskCompletionSource<GetItemResponse> getItemAsyncTaskSource;

    public void onGetItemRequestComplete(GetItemResponse response)
    {
        getItemAsyncTaskSource.SetResult(response);
    }

    public Task<GetItemResponse> getItemAsync(GetItemRequest request)
    {
        getItemAsyncTaskSource = new();
        PlayFabEconomyAPI.GetItem(request, onGetItemRequestComplete, error => Debug.LogError(error.GenerateErrorReport()));
        return getItemAsyncTaskSource.Task;
    }

    // @see https://learn.microsoft.com/en-us/rest/api/playfab/economy/catalog/get-items
    private TaskCompletionSource<GetItemsResponse> getItemsAsyncTaskSource;

    public void onGetItemsRequestComplete(GetItemsResponse response)
    {
        getItemsAsyncTaskSource.SetResult(response);
    }

    public Task<GetItemsResponse> getItemsAsync(GetItemsRequest request)
    {
        getItemsAsyncTaskSource = new();
        PlayFabEconomyAPI.GetItems(request, onGetItemsRequestComplete, error => Debug.LogError(error.GenerateErrorReport()));
        return getItemsAsyncTaskSource.Task;
    }

    // @see https://learn.microsoft.com/en-us/rest/api/playfab/economy/inventory/purchase-inventory-items
    private TaskCompletionSource<PurchaseInventoryItemsResponse> purchaseInventoryItemsAsyncTaskSource;

    public void OnPurchaseInventoryItemsRequestComplete(PurchaseInventoryItemsResponse response)
    {
        purchaseInventoryItemsAsyncTaskSource.SetResult(response);
    }

    public Task<PurchaseInventoryItemsResponse> purchaseInventoryItemsAsync(PurchaseInventoryItemsRequest request)
    {
        purchaseInventoryItemsAsyncTaskSource = new();
        PlayFabEconomyAPI.PurchaseInventoryItems(request,
            OnPurchaseInventoryItemsRequestComplete,
            error => { Debug.LogError(error.GenerateErrorReport()); });
        return purchaseInventoryItemsAsyncTaskSource.Task;
    }

    // @see https://learn.microsoft.com/en-us/rest/api/playfab/economy/catalog/search-items
    private TaskCompletionSource<SearchItemsResponse> searchItemsAsyncTaskSource;

    public void OnSearchItemsRequestComplete(SearchItemsResponse response)
    {
        searchItemsAsyncTaskSource.SetResult(response);
    }

    public Task<SearchItemsResponse> searchItemsAsync(SearchItemsRequest request) {
        searchItemsAsyncTaskSource = new();
        PlayFabEconomyAPI.SearchItems(request, OnSearchItemsRequestComplete, error => Debug.LogError(error.GenerateErrorReport()));
        return searchItemsAsyncTaskSource.Task;
    }
}

public class PurchaseJsonData
{
    public string orderId;
    public string packageName;
    public string productId;
    public long   purchaseTime;
    public int    purchaseState;
    public string purchaseToken;
}

public class PurchasePayloadData
{
    public PurchaseJsonData JsonData;

    public string signature;
    public string json;

    public static PurchasePayloadData FromJson(string json)
    {
        var payload = JsonUtility.FromJson<PurchasePayloadData>(json);
        payload.JsonData = JsonUtility.FromJson<PurchaseJsonData>(json);
        return payload;
    }
}

public class GooglePurchase
{
    public PurchasePayloadData PayloadData;

    public string Store;
    public string TransactionID;
    public string Payload;

    public static GooglePurchase FromJson(string json)
    {
        var purchase = JsonUtility.FromJson<GooglePurchase>(json);
        // Only fake receipts are returned in Editor play.
        if (Application.isEditor)
        {
            return purchase;
        }
        purchase.PayloadData = PurchasePayloadData.FromJson(purchase.Payload);
        return purchase;
    }
}
  1. Code と呼ばれる新しい GameObject を作成します。
  2. AndroidIAPExample コンポーネントを追加します (クリック アンド ドラッグ、または)。
  3. 必ずシーンを保存してください。

UnityIAP ゲーム オブジェクトの例を作成する

最後に、[Build Settings (ビルド設定)] に移動します。

  1. シーンが [Scenes In Build (ビルド中のシーン)] エリアに追加されたことを確認します。
  2. [Android] プラットフォームが選択されていることを確認します。
  3. [プレイヤーの設定] エリアに移動します。
  4. [パッケージ名] を割り当てます。

注意

PlayMarket の競合を避けるために、独自のパッケージ名を付けてください。

UnityIAP ゲーム プロジェクトの例を追加する

最後に、通常どおりにアプリケーションをビルドし、APK があることを確認します。

テストするには、PlayMarket と PlayFab を構成する必要があります。

IAP に向けて PlayMarket アプリケーションを設定する

このセクションでは、PlayMarket アプリケーションで IAP を有効にする方法の詳細を説明します。

注意

アプリケーション自体の設定は、このチュートリアルの範囲外です。 すでにアプリケーションがあり、少なくともアルファ版のリリースとして公開できるように構成されていると想定しています。

PlayMarket アプリケーションを有効にする

便利なメモ

  • そのポイントに移動するには、APK をアップロードする必要があります。 前のセクションで構築した APK を使用してください。
  • APK のアップロードを求められたら、[アルファ版] または [ベータ版] のアプリケーションとしてそれをアップロードし、IAP サンドボックスを有効にします。
  • [コンテンツの規則] の構成には、アプリケーションでの IAP の有効化方法に関する質問が含まれます。
  • PlayMarket では、 パブリッシャーが IAP を使用またはテストすることはできません。 テスト目的で別の Google アカウントを選択し、アルファ/ベータ ビルドのテスターとして追加します。
  1. アプリケーション ビルドを発行します。

  2. メニューから [In-app products (アプリ内製品)] を選択します。

    • 販売アカウントを求められたら、リンクするか作成します。
  3. [Add New Product (新しい製品の追加)] を選択します。

    PlayMarket 新しい製品の追加

  4. 新しい製品画面で、[マネージド製品] を選択します。

  5. 100diamonds など、わかりやすい製品 ID を指定します。

  6. [続行] を選択します。

    PlayMarket 製品 ID の追加

  7. PlayMarket で、[Title (タイトル)] (1)[Description (説明)] (2) を入力するように求められます (例: 100 DiamondsA pack of 100 diamonds to spend in-game)。

    データ アイテム データは PlayFab サービスからのみ取得され、一致する ID のみが必要です。

    PlayMarket 製品のタイトルと説明の追加

  8. さらにスクロールし、[Add a price (価格を追加)] を選択します。

    PlayMarket 製品価格の追加

  9. "$0.99" などの有効な価格を入力します (各国/地域に応じて、価格がどのように変換されるかに注意してください)。

  10. [Apply (適用)] を選択します。

    PlayMarket 製品の追加と現地価格の適用

  11. 最後に、画面の一番上までスクロールし、アイテムのステータスを [Active (アクティブ)] に変更します。

    PlayMarket 製品をアクティブにする

  12. ライセンス キーを保存して、PlayFab を PlayMarket にリンクします。

  13. メイン メニューの [Services & APIs (サービスと API)] に移動します。

  14. 次に、キーBase64 バージョンを見つけて保存します。

PlayMarket 製品ライセンス キーの保存

次の手順では、IAP のテストを有効にします。 アルファ版およびベータ版のビルドに対してサンドボックスは自動的に有効になりますが、アプリのテストのために承認されたアカウントを設定する必要があります。

  1. [ホーム] に移動します。
  2. 左側のメニューで、[Account details (アカウントの詳細)] を見つけて選択します。
  3. [License Testing (ライセンスのテスト)] エリアを見つけます。
  4. テスト アカウントが一覧にあることを確認します。
  5. [License Test Response (ライセンス テストの応答)]RESPOND_NORMALLY に設定されていることを確認します。

設定を適用することを忘れないでください。

PlayMarket IAP テストの有効化

PlayMarket 側の統合は、この時点で設定する必要があります。

PlayFab タイトルを設定する

最後の手順では、PlayFab タイトルを構成して製品を反映させ、Google 課金 API と統合します。

  1. [アドオン] を選択します。
  2. 次に、Google 追加コンテンツを選択します。

PlayFab Google 追加コンテンツを開く

  1. [パッケージ ID] に入力します。
  2. 前のセクションで取得した [Google App License Key (Google アプリのライセンス キー)] を入力します。
  3. [Install Google (Google のインストール)] を選択して変更を確定します。

次の手順では、PlayFab で 100 個のダイヤモンド バンドルを反映します。

  1. 新しい経済カタログ (V2) 通貨を作成します。

  2. タイトルを編集し、説明 (例: DiamondsOur in-game currency of choice.) を追加します。

  3. フレンドリ ID を追加して、通貨 diamonds を簡単に見つけられるようにします。

  4. [保存して発行] を選択して変更を完了します。

  5. [通貨] リストで通貨を確認します。

  6. 次に、新しいエコノミー カタログ (V2) バンドルを作成します。

  7. タイトルを編集し、説明 (例: 100 Diamonds BundleA pack of 100 diamonds to spend in-game.) を追加します。

    {
        "NEUTRAL": "100 Diamonds Bundle",
        "en-US": "100 Diamonds Bundle",
        "en-GB": "100 Diamonds Bundle",
        "de-DE": "100 Diamantenbüschel"
    }
    

    注意

    このデータは、Play マーケット アイテムのタイトルおよび説明とは何の関係もありません。独立したものであることに注意してください。

  8. コンテンツ タイプを使用して、バンドル (例: appstorebundles) を整理できます。 コンテンツ タイプは ⚙️ > [タイトルの設定] > [エコノミー (V2)] で管理できます。

  9. Display プロパティにローカライズされた価格を追加して、実際の価格を追跡します。

    {
        "prices": [
            "en-us": 0.99,
            "en-gb": 0.85,
            "de-de": 0.45
        ]
    }
    
  10. バンドルに新しいアイテムを追加します。 フィルターで [通貨] を選択し、前のセットで作成した通貨を選択します。 このバンドルで販売する通貨の量と一致するように数量を設定します。

  11. "GooglePlay" Marketplace 用の新しいプラットフォームを追加します。 GooglePlay Marketplace をまだお持ちでない場合は、[エコノミー設定] ページで作成できます。 前のセクションで作成した Google Play Console 製品 ID と一致するように Marketplace ID を設定します。

  12. [保存して発行] を選択して変更を完了します。

  13. バンドルの一覧でバンドルを確認します。

次に、プレイヤーが PlayFab ストアで通貨を使ってゲーム内 NPC ベンダーを表すように、ゲーム内購入を設定できます。

  1. 新しいエコノミー カタログ (V2) アイテムを作成します。
  2. タイトルを編集し、説明を追加します。たとえば、"Golden Sword"、"A sword made of gold."。
  3. プレイヤーがストアでアイテムを見つけるのに役立つローカライズされたキーワードを追加できます。 タグとコンテンツ タイプを追加して、後で API を使用して取得できるようにアイテムを整理するのに役立ちます。 Display プロパティを使用して、アーマー値、アートアセットへの相対パス、ゲームに格納する必要があるその他のデータなどのゲーム データを格納します。
  4. 新しい価格を追加し、前の手順で作成した通貨を選択します。 [金額] を既定で設定する価格に設定します。 価格は、後で作成した任意のストアでオーバーライドできます。
  5. [保存して発行] を選択して変更を完了します。
  6. Items リスト内のアイテムを確認します。
  7. 最後に、新しいエコノミー カタログ (V2) ストアを作成します。
  8. タイトルを編集し、説明 (例: Villager StoreA humble store run by a humble villager.) を追加します。
  9. 検索を容易にするために、villagerstore などのフレンドリ ID を指定します。
  10. 前の手順で作成したアイテムをストアに追加します。 1 つのストアに複数のアイテムを追加し、必要に応じて既定の価格をオーバーライドできます。
  11. [保存して発行] を選択して変更を完了します。
  12. Stores の一覧でストアを確認します。

PlayFab タイトルのセットアップが完了しました。

テスト

テスト目的で、アルファ版/ベータ版 リリースを使用してアプリをダウンロードします。

  • テスト アカウントと実際の Android デバイスを使用してください。
  • アプリを開始すると、IAP が初期化されたことがわかり、アイテムを表す 1 つのボタンが表示されます。
  • そのボタンを選択します。

テスト アプリ - [100 個のダイヤモンドを購入] ボタン

IAP の購入が開始されます。 正常に購入が完了するまで、Google Play の指示に従います。

アプリのテスト - Google Play - 支払いの完了

最後に、PlayFab ゲーム マネージャーのダッシュボードでタイトルに移動し、[New Events (新しいイベント)] を見つけます。

購入が提供、検証され、PlayFab エコシステムにパイプされたことを確認します。

UnityIAP と Android Billing API を PlayFab アプリケーションに正常に統合しました。

次の手順

  1. デモ IMGUI ディスプレイを置き換えるために、購入用の Unity UI Toolkit インターフェイスを構築します。
  2. PlayFab エラーを処理してユーザーに表示するカスタム Unity Logger を作成します。
  3. アイコン画像を PlayFab Items Images フィールドに追加して、Unity UI に表示します。