经济 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 UnityEngine;
using UnityEngine.Purchasing;
using UnityEngine.Purchasing.Extension;

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

/// <summary>
/// Unity behavior that implements the the Unity IAP Store interface.
/// Attach as an asset to your Scene.
/// </summary>
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 readonly PlayFabEconomyAPIAsync s_economyAPI = new();

    private static IStoreController s_storeController;

    // TODO: This callback is for illustrations purposes, you should create one that fits your needs
    public delegate void PlayFabProcessPurchaseCallback(PurchaseProcessingResult result);

    /// <summary>
    /// Event that is triggered when a purchase is processed.
    /// </summary>
    /// <remarks>
    /// TODO: Subscribe to this event in your game code to handle purchase results.
    /// </remarks>
    public event PlayFabProcessPurchaseCallback PlayFabProcessPurchaseEvent;

    /// <summary>
    /// True if the Store Controller, extensions, and Catalog are set.
    /// </summary>
    public bool IsInitialized => s_storeController != null
                             && _googlePlayCatalog != null
                             && _storefrontCatalog != null;

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

    /// <summary>
    /// Attempts to log the player in via the Android Device ID.
    /// </summary>
    private void Login()
    {
        // TODO: it is better to use LoginWithGooglePlayGamesService or a similar platform-specific login method for final game code.

        // SystemInfo.deviceUniqueIdentifier 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.LoginWithCustomID(new LoginWithCustomIDRequest()
        {
            CreateAccount = true,
            CustomId = SystemInfo.deviceUniqueIdentifier
        }, result => RefreshIAPItems(), PlayFabSampleUtil.OnPlayFabError);
    }

    /// <summary>
    /// Queries the PlayFab Economy Catalog V2 for updated listings
    /// and then fills the local catalog objects.
    /// </summary>
    private async void RefreshIAPItems()
    {
        _googlePlayCatalog = new Dictionary<string, PlayFab.EconomyModels.CatalogItem>();
        SearchItemsRequest googlePlayCatalogRequest = new()
        {
            Count = 50,
            Filter = "AlternateIds/any(t: t/type eq 'GooglePlay')"
        };

        SearchItemsResponse googlePlayCatalogResponse;
        do
        {
            googlePlayCatalogResponse = await s_economyAPI.SearchItemsAsync(googlePlayCatalogRequest);
            Debug.Log("Search response: " + JsonUtility.ToJson(googlePlayCatalogResponse));

            foreach (PlayFab.EconomyModels.CatalogItem item in googlePlayCatalogResponse.Items)
            {
                _googlePlayCatalog.Add(item.Id, item);
            }

        } while (!string.IsNullOrEmpty(googlePlayCatalogResponse.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 s_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 s_economyAPI.GetItemsAsync(itemsCatalogRequest);
        foreach (PlayFab.EconomyModels.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();
    }

    /// <summary>
    /// Initializes the Unity IAP system for the Google Play Store.
    /// </summary>
    private void InitializePurchasing()
    {
        if (IsInitialized) return;

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

        foreach (PlayFab.EconomyModels.CatalogItem item in _googlePlayCatalog.Values)
        {
            string googlePlayItemId = item.AlternateIds.FirstOrDefault(item => item.Type == "GooglePlay")?.Value;
            if (!string.IsNullOrWhiteSpace(googlePlayItemId))
            {
                builder.AddProduct(googlePlayItemId, ProductType.Consumable);
            }
        }

        UnityPurchasing.Initialize(this, builder);
    }

    /// <summary>
    /// Draw a debug IMGUI for testing examples.
    /// Use UI Toolkit for your production game runtime UI instead.
    /// </summary>
    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 (PlayFab.EconomyModels.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 (PlayFab.EconomyModels.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"
                )))
            {
                Task.Run(() => PlayFabPurchaseItemById(item.Id));
            }
        }
    }

    /// <summary>
    /// Integrates game purchasing with the Unity IAP API.
    /// </summary>
    public void BuyProductById(string productId)
    {
        if (!IsInitialized)
        {
            Debug.LogError("IAP Service is not initialized!");
            return;
        }

        s_storeController.InitiatePurchase(productId);
    }

    /// <summary>
    /// Purchases a PlayFab inventory item by ID.
    /// See the <see cref="PlayFabEconomyAPIAsync"/> class for details on error handling
    /// and calling patterns.
    /// </summary>
    async public Task<bool> PlayFabPurchaseItemById(string itemId)
    {
        if (!IsInitialized)
        {
            Debug.LogError("IAP Service is not initialized!");
            return false;
        }

        _lastAPICallResult = new();

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

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

        GetItemRequest getVillagerStoreRequest = new()
        {
            AlternateId = new CatalogAlternateId()
            {
                Type = "FriendlyId",
                Value = "villagerstore"
            }
        };

        GetItemResponse getStoreResponse = await s_economyAPI.GetItemAsync(getVillagerStoreRequest);
        if (getStoreResponse == null || string.IsNullOrEmpty(getStoreResponse?.Item?.Id))
        {
            _lastAPICallResult.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()
        {
            Amount = 1,
            Item = new InventoryItemReference()
            {
                Id = itemId
            },
            PriceAmounts = new List<PurchasePriceAmount>
            {
                new()
                {
                    Amount = price.Amount,
                    ItemId = price.ItemId
                }
            },
            IdempotencyId = _purchaseIdempotencyId,
            StoreId = getStoreResponse.Item.Id
        };

        PurchaseInventoryItemsResponse purchaseInventoryItemsResponse = await s_economyAPI.PurchaseInventoryItemsAsync(purchaseInventoryItemsRequest);
        if (purchaseInventoryItemsResponse == null || purchaseInventoryItemsResponse?.TransactionIds.Count < 1)
        {
            _lastAPICallResult.Error = "Unable to purchase. Try again in a few minutes.";
            return false;
        }

        _purchaseIdempotencyId = "";
        _lastAPICallResult.Message = "Purchasing!";
        return true;
    }

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

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

        extensions.GetExtension<IGooglePlayStoreExtensions>().RestoreTransactions((result, error) => {
            if (result)
            {
                Debug.LogWarning("Restore transactions succeeded.");
            }
            else
            {
                Debug.LogWarning("Restore transactions failed.");
            }
        });
    }

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

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

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

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

    /// <summary>
    /// Callback for Store purchases. Subscribe to PlayFabProcessPurchaseEvent to handle the final PurchaseProcessingResult.
    /// <see href="https://docs.unity3d.com/Packages/com.unity.purchasing@4.8/api/UnityEngine.Purchasing.PurchaseProcessingResult.html"/>
    /// </summary>
    /// <remarks>
    /// This code does not account for purchases that were pending and are
    /// delivered on application start. Production code should account for these cases.
    /// </remarks>
    /// <returns>Complete immediately upon error. Pending if PlayFab Economy is handling final processing and will trigger PlayFabProcessPurchaseEvent with the final result.</returns>
    public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs purchaseEvent)
    {
        if (!IsInitialized)
        {
            Debug.LogWarning("Not initialized. Ignoring.");
            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);
        GooglePurchase purchasePayload = GooglePurchase.FromJson(purchaseEvent.purchasedProduct.receipt);
        RedeemGooglePlayInventoryItemsRequest request = new()
        {
            Purchases = new List<GooglePlayProductPurchase>
            {
                new()
                {
                    ProductId = purchasePayload.PayloadData?.JsonData?.productId,
                    Token = purchasePayload.PayloadData?.JsonData?.purchaseToken
                }
            }
        };

        PlayFabEconomyAPI.RedeemGooglePlayInventoryItems(request, result =>
        {
            Debug.Log("Processed receipt validation.");

            if (result?.Failed.Count > 0)
            {
                Debug.Log($"Validation failed for {result.Failed.Count} receipts.");
                Debug.Log(JsonUtility.ToJson(result.Failed));
                PlayFabProcessPurchaseEvent?.Invoke(PurchaseProcessingResult.Pending);
            }
            else
            {
                Debug.Log("Validation succeeded!");
                PlayFabProcessPurchaseEvent?.Invoke(PurchaseProcessingResult.Complete);
                s_storeController.ConfirmPendingPurchase(purchaseEvent.purchasedProduct);
                Debug.Log("Confirmed purchase with Google Marketplace.");
            }
        },
        PlayFabSampleUtil.OnPlayFabError);

        return PurchaseProcessingResult.Pending;
    }
}

/// <summary>
/// Utility classes for the sample.
/// </summary>
public class PlayFabEconomyAPIAsyncResult
{
    public string Error { get; set; } = null;

    public string Message { get; set; } = null;
}

public static class PlayFabSampleUtil
{
    public static void OnPlayFabError(PlayFabError error)
    {
        Debug.LogError(error.GenerateErrorReport());
    }
}

/// <summary>
/// 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.
/// </summary>
public class PlayFabEconomyAPIAsync
{
    /// <summary>
    /// <see href="https://learn.microsoft.com/rest/api/playfab/economy/catalog/get-item"/>
    /// </summary>
    public Task<GetItemResponse> GetItemAsync(GetItemRequest request)
    {
        TaskCompletionSource<GetItemResponse> getItemAsyncTaskSource = new();
        PlayFabEconomyAPI.GetItem(request, (response) => getItemAsyncTaskSource.SetResult(response), error => 
        {
            PlayFabSampleUtil.OnPlayFabError(error);
            getItemAsyncTaskSource.SetResult(default);
        });
        return getItemAsyncTaskSource.Task;
    }

    /// <summary>
    /// <see href="https://learn.microsoft.com/rest/api/playfab/economy/catalog/get-items"/>
    /// </summary>
    public Task<GetItemsResponse> GetItemsAsync(GetItemsRequest request)
    {
        TaskCompletionSource<GetItemsResponse> getItemsAsyncTaskSource = new();
        PlayFabEconomyAPI.GetItems(request, (response) => getItemsAsyncTaskSource.SetResult(response), error => 
        {
            PlayFabSampleUtil.OnPlayFabError(error);
            getItemsAsyncTaskSource.SetResult(default);
        });
        return getItemsAsyncTaskSource.Task;
    }

    /// <summary>
    /// <see href="https://learn.microsoft.com/rest/api/playfab/economy/inventory/purchase-inventory-items"/>
    /// </summary>
    public Task<PurchaseInventoryItemsResponse> PurchaseInventoryItemsAsync(PurchaseInventoryItemsRequest request)
    {
        TaskCompletionSource<PurchaseInventoryItemsResponse> purchaseInventoryItemsAsyncTaskSource = new();
        PlayFabEconomyAPI.PurchaseInventoryItems(request, (response) => purchaseInventoryItemsAsyncTaskSource.SetResult(response), error => 
        {
            PlayFabSampleUtil.OnPlayFabError(error);
            purchaseInventoryItemsAsyncTaskSource.SetResult(default);
        });
        return purchaseInventoryItemsAsyncTaskSource.Task;
    }

    /// <summary>
    /// <see href="https://learn.microsoft.com/rest/api/playfab/economy/catalog/search-items"/>
    /// </summary>
    public Task<SearchItemsResponse> SearchItemsAsync(SearchItemsRequest request)
    {
        TaskCompletionSource<SearchItemsResponse> searchItemsAsyncTaskSource = new();
        PlayFabEconomyAPI.SearchItems(request, (response) => searchItemsAsyncTaskSource.SetResult(response), error => 
        {
            PlayFabSampleUtil.OnPlayFabError(error);
            searchItemsAsyncTaskSource.SetResult(default);
        });
        return searchItemsAsyncTaskSource.Task;
    }
}

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

[Serializable]
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>(payload.json);
        return payload;
    }
}

[Serializable]
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 中显示。