经济 v2、Unity 和 Android 入门
重要
Economy v2 现已正式发布。 有关支持和反馈,请转到 PlayFab 论坛。
本教程介绍如何使用 PlayFab、Unity + IAP 服务和 Android Billing API 设置应用内购买 (IAP)。
开始之前
Android Billing API 和 PlayFab 共同协作以为客户提供 IAP 体验:
首先通过 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:
- 导航到 “服务”。
- 确保选择 Services 选项卡。
- 选择 “统一服务” 配置文件或组织。
- 选择 “创建” 按钮。
- 接下来,导航到 “应用内购买 (IAP)” 服务。
通过设置 简化跨平台的IAP 切换,请确保启用 “服务”。
然后选择 “继续” 按钮。
会显示插件列表页面。
- 选择 “导入” 按钮。
继续 Unity 安装和导入过程,直至导入所有插件。
- 确认插件是否已就位。
- 然后,创建一个名为 AndroidIAPExample.cs 的新脚本。
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;
}
}
- 新建名为代码的GameObject。
- 向其添加
AndroidIAPExample
组件(单击并拖动或)。 - 确保保存场景。
最后,导航到 “编译设置”。
- 确认场景是否已添加到 “编译中的场景” 区域。
- 请确保已选择 Android 平台。
- 转到 “玩家设置” 区域。
- 指定 “程序包名称”。
注意
请务必提供 自己 的程序包名称,以免造成任何 PlayMarket 冲突。
最后,像往常一样生成应用,并确保有 APK。
为了进行测试,我们需要配置 PlayMarket 和 PlayFab。
为 IAP 设置 PlayMarket 应用程序
本节介绍如何为 PlayMarket 应用程序启用 IAP 的具体信息。
注意
设置应用程序本身已超出本教程的范围。 我们已假设 有 一个应用程序,它配置为至少发布 Alpha 版本。
有用的注意事项:
- 要达到目的,需要上传 APK。 请使用我们在上一节中构建的 APK。
- 将 APK 上传为Alpha或Beta应用程序以启用 IAP 沙盒。
- 配置Content Rating涉及有关如何在应用程序中启用 IAP 的问题。
- PlayMarket 不允许发布者使用或测试 IAP。 选择另一个 Google 帐户进行测试,并将其添加为 Alpha/Beta 版本的测试人员。
发布应用程序版本。
从菜单中选择 In-app products。
- 如果要求提供商家帐户,请链接或创建帐户。
选择 Add New Product 按钮。
在新产品屏幕上,选择“托管产品”。
为其提供描述性产品 ID,例如
100diamonds
。选择继续。
PlayMarket 要求填写游戏 (1)和说明 (2),例如
100 Diamonds
和A pack of 100 diamonds to spend in-game
。数据物品数据仅来自 PlayFab 服务,并且只需要 ID 匹配。
进一步滚动并选择 Add a price 按钮。
输入有效的价格(例如“$0.99”)(注意价格是如何针对每个国家/地区/区域独立转换的)。
选择 Apply 按钮。
最后,滚动回到屏幕顶部,将物品的状态更改为 “可用”。
保存许可密钥以链接 PlayFab 与 PlayMarket。
导航到菜单中的 Services & APIs。
然后,找到并保存 “密钥” 的 Base64 版本。
下一步是启用 IAP 测试。 尽管 Alpha 和 Beta 版本自动启用了沙盒,我们还是需要设置获得授权可以测试应用的帐户:
- 导航到“主页”。
- 在左侧菜单中查找并选择 “帐户详细信息”。
- 找到 License Testing 区域。
- 验证 测试账户 是否在列表中。
- 请确保 许可证测试响应 设置为 RESPOND_NORMALLY。
别忘记应用设置!
现在,集成的 Play Market 端的设置就完成了。
设置 PlayFab 游戏
最后一步是配置 PlayFab 作品,以反映我们的产品,并与 Google Billing API 集成。
- 选择 “附加内容”。
- 然后选择 Google 加载项。
- 填入 程序包 ID。
- 填写在上一节中获取的 Google 应用许可证密钥。
- 通过选择 Install Google 按钮提交更改。
下一步是反映 PlayFab 中的 100 颗钻石捆绑包:
新建经济目录 (V2) 货币。
编辑“游戏”并添加“说明” - 例如,
Diamonds
、Our in-game currency of choice.
。添加友好 ID,以更轻松地查找货币
diamonds
。选择“保存并发布”以完成更改。
在“货币”列表中观察你的货币。
接下来,新建经济目录 (V2) 捆绑包。
编辑“游戏”并添加“说明” - 例如,
100 Diamonds Bundle
、A 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" }
注意
请记住,此数据与游戏市场物品标题和说明无关 - 它是独立的。
可以使用内容类型组织捆绑包 ,例如
appstorebundles
。 内容类型在 ⚙️ > 游戏设置 > 经济 (V2) 中管理。向显示属性添加本地化定价以跟踪实际价格。
{ "prices": [ "en-us": 0.99, "en-gb": 0.85, "de-de": 0.45 ] }
将新物品添加到捆绑包。 在筛选器中选择“货币”,然后选择在上一集中创建的货币。 设置数量以匹配要在此捆绑包中销售的货币金额。
为“GooglePlay”市场新增平台。 如果还没有 GooglePlay 市场,可以在“经济设置”页中创建。 设置商城 ID,以匹配在上一部分中创建的 Google Play 控制台产品 ID。
选择“保存并发布”以完成更改。
在捆绑包列表中观察捆绑包。
接下来,我们可以设置游戏内购买,让玩家在 PlayFab 应用商店中花费货币以代表游戏内 NPC 供应商:
- 新建经济目录 (V2) 物品。
- 编辑游戏,并添加说明 - 例如,“金剑”,“一把金制的剑”。
- 可以添加本地化关键字以帮助玩家在应用商店中查找物品。 添加标记和内容类型以帮助整理物品,以便之后通过 API 检索。 使用显示属性存储游戏数据,例如护甲值、艺术资产的相对路径或需要为游戏存储的任何其他数据。
- 新增价格并选择在上一步中创建的货币。 将金额设置为默认要设置的价格。 稍后可以在创建的任何应用商店中替代价格。
- 选择“保存并发布”以完成更改。
- 观察物品列表中的物品。
- 最后,新建经济目录 (V2) 应用商店。
- 编辑“游戏”并添加“说明” - 例如,
Villager Store
、A humble store run by a humble villager.
。 - 为它提供友好 ID以更轻松地检索,例如
villagerstore
。 - 将上一步中创建的物品添加到应用商店。 可以向应用商店添加多个物品,并在必要时替代任何默认价格。
- 选择“保存并发布”以完成更改。
- 观察应用商店列表中的应用商店。
PlayFab 作品的设置到此结束。
测试
为了进行测试,请下载使用 Alpha/Beta 版本的应用。
- 请务必使用测试帐户和真实 Android 设备。
- 启动应用后,应该看到 IAP 初始化,以及一个表示物品的 按钮。
- 选择该按钮。
IAP 购买已启动。 按照 Google Play 说明操作直至购买成功。
最后,在 PlayFab “游戏管理器” 仪表板中导航到作品,找到 “新事件”。
请验证是否已提供、验证购买并将其传送到 PlayFab 生态系统。
你已成功将 UnityIAP 和 Android Billing API 集成到 PlayFab 应用程序中。
后续步骤
- 生成用于购买的 Unity UI 工具包界面,以替换演示 IMGUI 显示。
- 创建自定义 Unity 记录器以处理 PlayFab 错误并将其显示给用户。
- 将图标图像添加到 PlayFab 物品图像字段,以在 Unity UI 中显示。