经济 v2、Unity 和 Android 入门

重要

Economy v2 现已正式发布。 有关支持和反馈,请转到 PlayFab 论坛

本教程介绍如何使用 PlayFab、Unity + IAP 服务和 Android Billing API 设置应用内购买 (IAP)。

开始之前

Android Billing API 和 PlayFab 共同协作以为客户提供 IAP 体验:

PlayFab 经济 v2 - 兑换时间线

首先通过 PlayMarket 设置产品 ID价格。 开始时,所有产品都面貌不明 - 它们是玩家可以购买的数字实体 - 但对于 PlayFab 玩家没有任何意义。

要使这些实体有用,我们需要在 PlayFab 物品目录中对它们进行镜像。 PlayFab 使“面貌不明”的实体成为捆绑包、容器和独立的物品。

每个物品都具有自己独特的面貌:

  • 游戏
  • 说明
  • 标记
  • 类型
  • 图片
  • 行为

所有物品都通过共享 ID 链接到市场产品。

访问可供购买的实际货币物品的最佳方式是使用GetItems

物品 ID 是 PlayFab 和任何外部 IAP 系统之间的链接。 因此,我们向 IAP 服务传递物品 ID。

此时,购买过程开始。 玩家与 IAP 界面交互 - 如果购买成功 - 则会获得收据。

PlayFab 验证收据并注册购买,向 PlayFab 玩家授予其刚刚购买的物品。

设置客户端应用程序

本节介绍了如何配置应用程序,从而测试使用 PlayFab、UnityIAP 和 Android Billing API 的 IAP。

先决条件:

  • 一个 Unity 项目。
  • 已导入PlayFab Unity SDK并将其配置为处理游戏。
  • Visual Studio等编辑器已安装并配置为使用 Unity 项目。

第一步是设置 UnityIAP:

  1. 导航到 “服务”
  2. 确保选择 Services 选项卡。
  3. 选择 “统一服务” 配置文件或组织。
  4. 选择 “创建” 按钮。

设置 UnityIAP 服务

  1. 接下来,导航到 “应用内购买 (IAP)” 服务。

导航到 UnityIAP 服务

  1. 通过设置 简化跨平台的IAP 切换,请确保启用 “服务”

  2. 然后选择 “继续” 按钮。

启用 UnityIAP 服务

会显示插件列表页面。

  1. 选择 “导入” 按钮。

UnityIAP 服务 - 导入插件

继续 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. 新建名为代码GameObject
  2. 向其添加AndroidIAPExample组件(单击并拖动或)。
  3. 确保保存场景。

UnityIAP 创建示例游戏对象

最后,导航到 “编译设置”

  1. 确认场景是否已添加到 “编译中的场景” 区域。
  2. 请确保已选择 Android 平台。
  3. 转到 “玩家设置” 区域。
  4. 指定 “程序包名称”

注意

请务必提供 自己 的程序包名称,以免造成任何 PlayMarket 冲突。

UnityIAP 添加示例游戏对象

最后,像往常一样生成应用,并确保有 APK。

为了进行测试,我们需要配置 PlayMarket 和 PlayFab。

为 IAP 设置 PlayMarket 应用程序

本节介绍如何为 PlayMarket 应用程序启用 IAP 的具体信息。

注意

设置应用程序本身已超出本教程的范围。 我们已假设 一个应用程序,它配置为至少发布 Alpha 版本。

启用 PlayMarket 应用程序

有用的注意事项:

  • 要达到目的,需要上传 APK。 请使用我们在上一节中构建的 APK。
  • 将 APK 上传为AlphaBeta应用程序以启用 IAP 沙盒。
  • 配置Content Rating涉及有关如何在应用程序中启用 IAP 的问题。
  • PlayMarket 不允许发布者使用或测试 IAP。 选择另一个 Google 帐户进行测试,并将其添加为 Alpha/Beta 版本的测试人员。
  1. 发布应用程序版本。

  2. 从菜单中选择 In-app products

    • 如果要求提供商家帐户,请链接或创建帐户。
  3. 选择 Add New Product 按钮。

    PlayMarket 添加新产品

  4. 在新产品屏幕上,选择“托管产品”。

  5. 为其提供描述性产品 ID,例如100diamonds

  6. 选择继续

    PlayMarket 添加产品 ID

  7. PlayMarket 要求填写游戏 (1)说明 (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. 最后,滚动回到屏幕顶部,将物品的状态更改为 “可用”

    PlayMarket 使产品激活

  12. 保存许可密钥以链接 PlayFab 与 PlayMarket。

  13. 导航到菜单中的 Services & APIs

  14. 然后,找到并保存 “密钥”Base64 版本。

PlayMarket 保存产品许可密钥

下一步是启用 IAP 测试。 尽管 Alpha 和 Beta 版本自动启用了沙盒,我们还是需要设置获得授权可以测试应用的帐户:

  1. 导航到“主页”
  2. 在左侧菜单中查找并选择 “帐户详细信息”
  3. 找到 License Testing 区域。
  4. 验证 测试账户 是否在列表中。
  5. 请确保 许可证测试响应 设置为 RESPOND_NORMALLY

别忘记应用设置!

PlayMarket 启用 IAP 测试

现在,集成的 Play Market 端的设置就完成了。

设置 PlayFab 游戏

最后一步是配置 PlayFab 作品,以反映我们的产品,并与 Google Billing API 集成。

  1. 选择 “附加内容”
  2. 然后选择 Google 加载项。

PlayFab 打开 Google 加载项

  1. 填入 程序包 ID
  2. 填写在上一节中获取的 Google 应用许可证密钥
  3. 通过选择 Install 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"
    }
    

    注意

    请记住,此数据与游戏市场物品标题说明无关 - 它是独立的。

  8. 可以使用内容类型组织捆绑包 ,例如appstorebundles。 内容类型在 ⚙️ > 游戏设置 > 经济 (V2) 中管理。

  9. 向显示属性添加本地化定价以跟踪实际价格。

    {
        "prices": [
            "en-us": 0.99,
            "en-gb": 0.85,
            "de-de": 0.45
        ]
    }
    
  10. 将新物品添加到捆绑包。 在筛选器中选择“货币”,然后选择在上一集中创建的货币。 设置数量以匹配要在此捆绑包中销售的货币金额。

  11. 为“GooglePlay”市场新增平台。 如果还没有 GooglePlay 市场,可以在“经济设置”页中创建。 设置商城 ID,以匹配在上一部分中创建的 Google Play 控制台产品 ID。

  12. 选择“保存并发布”以完成更改。

  13. 捆绑包列表中观察捆绑包。

接下来,我们可以设置游戏内购买,让玩家在 PlayFab 应用商店中花费货币以代表游戏内 NPC 供应商:

  1. 新建经济目录 (V2) 物品。
  2. 编辑游戏,并添加说明 - 例如,“金剑”,“一把金制的剑”。
  3. 可以添加本地化关键字以帮助玩家在应用商店中查找物品。 添加标记和内容类型以帮助整理物品,以便之后通过 API 检索。 使用显示属性存储游戏数据,例如护甲值、艺术资产的相对路径或需要为游戏存储的任何其他数据。
  4. 新增价格并选择在上一步中创建的货币。 将金额设置为默认要设置的价格。 稍后可以在创建的任何应用商店中替代价格。
  5. 选择“保存并发布”以完成更改。
  6. 观察物品列表中的物品。
  7. 最后,新建经济目录 (V2) 应用商店。
  8. 编辑“游戏”并添加“说明” - 例如,Villager StoreA humble store run by a humble villager.
  9. 为它提供友好 ID以更轻松地检索,例如villagerstore
  10. 将上一步中创建的物品添加到应用商店。 可以向应用商店添加多个物品,并在必要时替代任何默认价格。
  11. 选择“保存并发布”以完成更改。
  12. 观察应用商店列表中的应用商店。

PlayFab 作品的设置到此结束。

测试

为了进行测试,请下载使用 Alpha/Beta 版本的应用。

  • 请务必使用测试帐户和真实 Android 设备。
  • 启动应用后,应该看到 IAP 初始化,以及一个表示物品的 按钮
  • 选择该按钮。

测试应用 - 购买 100 颗钻石按钮

IAP 购买已启动。 按照 Google Play 说明操作直至购买成功。

测试应用 - Google Play - 付款成功

最后,在 PlayFab “游戏管理器” 仪表板中导航到作品,找到 “新事件”

请验证是否已提供、验证购买并将其传送到 PlayFab 生态系统。

你已成功将 UnityIAP 和 Android Billing API 集成到 PlayFab 应用程序中。

后续步骤

  1. 生成用于购买的 Unity UI 工具包界面,以替换演示 IMGUI 显示。
  2. 创建自定义 Unity 记录器以处理 PlayFab 错误并将其显示给用户。
  3. 将图标图像添加到 PlayFab 物品图像字段,以在 Unity UI 中显示。