经济 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 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;
}
}
- 新建名为代码的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 中显示。