本文目录
-
- [一、 Windows Store 支付底层运行原理](#一、 Windows Store 支付底层运行原理)
-
- [1. 核心 API:`Windows.Services.Store`](#1. 核心 API:
Windows.Services.Store) - [2. 商品的生命周期与类型映射](#2. 商品的生命周期与类型映射)
- [1. 核心 API:`Windows.Services.Store`](#1. 核心 API:
- [二、 Microsoft Partner Center 后台配置步骤](#二、 Microsoft Partner Center 后台配置步骤)
-
- [1. 新建加载项 (Add-on)](#1. 新建加载项 (Add-on))
- [2. 获取并登记 12 位 Store ID](#2. 获取并登记 12 位 Store ID)
- [三、 Unity IAP 4.x (Windows Store) 完整接入实践](#三、 Unity IAP 4.x (Windows Store) 完整接入实践)
-
- [1. 数据模型与接口定义](#1. 数据模型与接口定义)
- [2. 映射表定义](#2. 映射表定义)
- [3. 底层适配器核心实现:`UnityIAP4PurchaseService`](#3. 底层适配器核心实现:
UnityIAP4PurchaseService) - [4. 业务管理层单例实现:`PurchaseManager`](#4. 业务管理层单例实现:
PurchaseManager)
- [四、 编译构建与真机联调](#四、 编译构建与真机联调)
-
- [1. 配置 AppxManifest 权限](#1. 配置 AppxManifest 权限)
- [2. 导出与部署](#2. 导出与部署)
- [3. 真实的真机联调测试方案(私有发布与 Package Flighting 流程)](#3. 真实的真机联调测试方案(私有发布与 Package Flighting 流程))
-
- [方案 A:以私有方式发布应用 (Private Visibility)](#方案 A:以私有方式发布应用 (Private Visibility))
- [方案 B:创建 Package Flighting (程序包外部测试)](#方案 B:创建 Package Flighting (程序包外部测试))
- 结语
在将 Unity 游戏发布至微软应用商店(Microsoft Store / Windows Store)时,游戏内购(In-App Purchase,简称 IAP)的接入是一项核心任务。相比于 iOS 的 App Store 或 Android 的 Google Play,Windows 平台的支付生态在 API 体系和后台配置上有着自身独特的逻辑。
本文将以 Unity IAP 4.x (以 4.15.1 为核心实例) 为例,结合当前主流的架构设计,从 底层通信原理 、Microsoft Partner Center 后台配置 到 Unity 工程全套代码实现,为你梳理出一条清晰的 Windows 平台内购落地路线。
一、 Windows Store 支付底层运行原理
在开始写代码前,我们有必要先理解 Windows Store IAP 的底层运转机制。
Windows 平台上的应用统称为 UWP (Universal Windows Platform) 。当我们在 Unity 中使用 Unity IAP 4.x 并且打包平台选择为 Universal Windows Platform 时,Unity IAP 底层会通过原生 C++ 桥接层与 Windows 系统的 Windows.Services.Store 命名空间进行交互。
1. 核心 API:Windows.Services.Store
微软在 Windows 10/11 中引入了全新的 Windows.Services.Store 体系(取代了过时的 Windows.ApplicationModel.Store)。
- StoreContext :这是整个微软商店支付的入口类。无论是获取商品目录、发起购买交易,还是获取已拥有的商品执照,都必须通过
StoreContext.GetDefault()取得上下文。 - StoreProduct:代表在微软合作伙伴中心配置的商品元数据(包括标题、价格、描述以及商品类型)。
- StorePurchaseResult :表示发起交易后的返回结果,通过其
Status字段(如Succeeded、AlreadyPurchased、NotPurchased、NetworkError等)来确认支付是否完成。
2. 商品的生命周期与类型映射
在微软商店中,内购商品被称为 Add-on (加载项/附加商品) 。它与 Unity IAP 的 ProductType 映射关系如下:
| 微软应用商店商品类型 (Add-on Type) | Unity IAP 商品类型 (ProductType) |
消费确权与核销逻辑 |
|---|---|---|
| Durable (持久商品) | NonConsumable (非消耗品) |
购买一次,永久拥有。由系统托管执照,无法重复购买。如:去广告、解锁关卡。 |
| Store-Managed Consumable | Consumable (商店托管消耗品) |
由微软商店托管其余额。例如:玩家买 100 金币,微软会在云端记下余额。必须显式调用核销 API (ReportConsumableFulfillmentAsync) 才能扣减其余额并允许再次购买。 |
| Developer-Managed Consumable | Consumable (开发者托管消耗品) |
微软不记录余额,仅授予购买执照。一旦购买成功,必须立刻核销 ,所有余额和累加逻辑由开发者服务器或本地存档(如 PlayerPrefs)管理。 |
| Subscription (订阅) | Subscription (订阅品) |
具有固定有效期的计费商品。由微软自动续期。需要解析收据中的过期时间(ExpirationDate)。 |
!IMPORTANT
Windows Store 最特殊的机制:12位 Store ID (Add-on ID)
微软商店中,每个 Add-on 被创建后,系统都会为它自动生成一个由 12 位大写字母与数字组成的唯一标识符 (例如:
9NR9LNF6RR7X)。在配置 Unity IAP 时,我们不能 直接用自定义的字符串(如
removeads)去请求微软商店,而必须在初始化时将游戏内自定义的 ID 显式地关联映射到这 12 位 Store ID 上。
二、 Microsoft Partner Center 后台配置步骤
为了使 Unity 游戏能够顺利请求到商品信息,必须先在 微软合作伙伴中心 (Microsoft Partner Center) 完成商品的登记。
1. 新建加载项 (Add-on)
- 登录 Microsoft Partner Center,选择你的应用,进入左侧菜单的 "游戏设置" -> "加载项" (Add-ons)。
- 点击 "新建加载项"。
- 选择加载项类型:
- 开发者托管的消耗品 (Developer-managed consumable) ------ 最推荐,适用于各种游戏币。
- 持久性 (Durable) ------ 适用于一次性解锁。
- 订阅 (Subscription) ------ 适用于月卡、周卡。
- 输入你的 加载项 Product ID 。注意:此处的 Product ID 可以是你自定义的字符串(例如:
remove_ads_tier1),但一旦创建无法修改。
2. 获取并登记 12 位 Store ID
提交你的加载项并审核通过(或处于草稿准备就绪状态)后,返回加载项列表页。
- 在加载项的**"产品标识"** (Product Identity) 页面下,你会看到一个名为 "Store ID" 的字段,形式为
9XXXXXX。 - 保存这个 12 位的字符串,因为我们需要把它写入 Unity 的代码中。
三、 Unity IAP 4.x (Windows Store) 完整接入实践
在 Unity 中,为了保证代码结构清晰且易于维护,推荐使用 面向接口的设计。通过定义统一的支付适配接口,将底层 Unity IAP 的复杂回调与游戏业务逻辑进行解耦。
1. 数据模型与接口定义
首先,我们需要定义一个表示微软商店商品的元数据类,以及对外暴露的支付服务接口:
csharp
using System;
namespace Assets.Scripts.Purchasing
{
[Serializable]
public sealed class WindowsStoreProductInfo
{
/// <summary>
/// 游戏逻辑内部使用的商品唯一标识 ID(例如 "removead")。
/// </summary>
public string InternalProductId;
/// <summary>
/// 微软应用商店后台配置的商品 12 位 ID(形如 "9NR9LNF6RR7X")。
/// </summary>
public string WindowsStoreProductId;
/// <summary>
/// 本地化商品名称。
/// </summary>
public string Title;
/// <summary>
/// 本地化商品描述。
/// </summary>
public string Description;
/// <summary>
/// 本地化且格式化好的价格字符串(例如 "$0.99", "¥6.00")。
/// </summary>
public string FormattedPrice;
/// <summary>
/// 玩家当前是否拥有该商品。
/// </summary>
public bool IsOwned;
/// <summary>
/// 商品的物理类型("Durable", "Consumable", "Subscription")。
/// </summary>
public string ProductKind;
/// <summary>
/// 若为订阅商品,则代表其具体的过期日期与时间。
/// </summary>
public DateTime ExpirationDate;
}
}
csharp
using System;
using System.Collections.Generic;
namespace Assets.Scripts.Purchasing
{
public interface IPurchaseService
{
bool IsInitialized { get; }
event Action<string> OnPurchaseSucceeded;
event Action<string, string> OnPurchaseFailed;
event Action<string> OnPurchaseRestored;
event Action<string, string> OnPurchasePendingOrUnknown;
event Action OnProductsLoaded;
void Initialize();
void Buy(string productId);
void RestorePurchases();
bool IsPurchased(string productId);
List<WindowsStoreProductInfo> GetProductInfos();
}
}
2. 映射表定义
将游戏内的商品代码常量化,方便管理:
csharp
namespace Assets.Scripts.Purchasing
{
public static class PurchaseProductIds
{
public const string RemoveAds = "removead";
public const string Coins100 = "coins100";
public const string Coins500 = "coins500";
public const string Coins1000 = "coins1000";
public const string MonthSub = "monthsub";
}
}
3. 底层适配器核心实现:UnityIAP4PurchaseService
核心逻辑在于:在 Initialize 方法中,使用 ConfigurationBuilder 配置产品线时,必须调用 builder.AddProduct 并传入包含 12 位 Windows Store ID 的 IDs 实例。
csharp
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Purchasing;
using UnityEngine.Purchasing.Extension;
namespace Assets.Scripts.Purchasing
{
public sealed class UnityIAP4PurchaseService : IPurchaseService, IStoreListener, IDetailedStoreListener
{
public bool IsInitialized { get; private set; }
public event Action<string> OnPurchaseSucceeded;
public event Action<string, string> OnPurchaseFailed;
public event Action<string> OnPurchaseRestored;
public event Action<string, string> OnPurchasePendingOrUnknown;
public event Action OnProductsLoaded;
private IStoreController m_StoreController;
private IExtensionProvider m_StoreExtensionProvider;
private readonly List<WindowsStoreProductInfo> productInfos = new();
/// <summary>
/// 异步初始化连接微软应用商店
/// </summary>
public void Initialize()
{
if (IsInitialized) return;
Debug.Log("[UnityIAP4][Service] 正在启动 Unity IAP 4.x 初始化流程...");
try
{
// 获取配置构建器实例
var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
// 【关键步骤】注册产品,并显式将游戏内部 ID 与微软后台的 12 位 Store ID 进行原生映射绑定
builder.AddProduct(PurchaseProductIds.RemoveAds, ProductType.NonConsumable, new IDs
{
{ "9NR9LNF6RR7X", "WindowsStore" }
});
builder.AddProduct(PurchaseProductIds.Coins100, ProductType.Consumable, new IDs
{
{ "9N8TG2RN9G94", "WindowsStore" }
});
builder.AddProduct(PurchaseProductIds.Coins500, ProductType.Consumable, new IDs
{
{ "9NK3RS58CPKR", "WindowsStore" }
});
builder.AddProduct(PurchaseProductIds.Coins1000, ProductType.Consumable, new IDs
{
{ "9N2R065S51P3", "WindowsStore" }
});
builder.AddProduct(PurchaseProductIds.MonthSub, ProductType.Subscription, new IDs
{
{ "9P69PTL5Z8PS", "WindowsStore" }
});
// 调用 UnityPurchasing 执行初始化
UnityPurchasing.Initialize(this, builder);
}
catch (Exception ex)
{
Debug.LogError($"[UnityIAP4][Service] 初始化发生异常: {ex.Message}");
IsInitialized = false;
}
}
/// <summary>
/// 发起商品购买
/// </summary>
public void Buy(string productId)
{
if (!IsInitialized || m_StoreController == null)
{
Debug.LogError($"[UnityIAP4][Service] 购买失败:服务未初始化。ProductId: {productId}");
OnPurchaseFailed?.Invoke(productId, "Store not initialized");
return;
}
#if UNITY_EDITOR
// 为了在 Unity 编辑器中获得顺畅的模拟体验,可以使用 Native Dialog 进行弹出提示
var product = m_StoreController.products.WithID(productId);
string prodName = product != null ? product.metadata.localizedTitle : productId;
string prodPrice = product != null ? product.metadata.localizedPriceString : "$0.99";
bool confirmed = UnityEditor.EditorUtility.DisplayDialog(
"Mock Microsoft Store - Checkout",
$"商品名称: {prodName}\n价格: {prodPrice}\n\n是否确认购买该商品?",
"确认购买 (Buy)",
"取消交易 (Cancel)"
);
if (confirmed)
{
ExecuteActualBuyFlow(productId);
}
else
{
Debug.LogWarning($"[UnityIAP4][Service] 用户取消了支付流程。ProductId: {productId}");
OnPurchaseFailed?.Invoke(productId, "User canceled");
}
#else
// 非编辑器环境下直接拉起微软原生内购支付窗口
ExecuteActualBuyFlow(productId);
#endif
}
private void ExecuteActualBuyFlow(string productId)
{
Debug.Log($"[UnityIAP4][Service] 正在通过 Unity IAP 发起购买,内部 ProductId: {productId}");
m_StoreController.InitiatePurchase(productId);
}
/// <summary>
/// 恢复历史拥有的非消耗品/订阅商品执照
/// </summary>
public void RestorePurchases()
{
if (m_StoreController == null)
{
Debug.LogError("[UnityIAP4][Service] 恢复购买失败:商店控制器未就绪!");
return;
}
Debug.Log("[UnityIAP4][Service] 正在拉取用户在应用商店的已购买执照...");
int restoreCount = 0;
// Windows Store 的执照恢复由 IStoreController 的商品清单本地收据状态托管
foreach (var product in m_StoreController.products.all)
{
if (product.hasReceipt && product.definition.type != ProductType.Consumable)
{
Debug.Log($"[UnityIAP4][Service] 检测到有效的非消耗品激活执照: {product.definition.id},恢复中...");
var info = productInfos.Find(p => p.InternalProductId == product.definition.id);
if (info != null)
{
info.IsOwned = true;
}
OnPurchaseRestored?.Invoke(product.definition.id);
restoreCount++;
}
}
Debug.Log($"[UnityIAP4][Service] 历史执照恢复完毕,已成功恢复 {restoreCount} 项权益。");
RefreshProductInfosAndNotify();
}
public bool IsPurchased(string productId)
{
var info = productInfos.Find(p => p.InternalProductId == productId);
return info != null && info.IsOwned;
}
public List<WindowsStoreProductInfo> GetProductInfos()
{
return productInfos;
}
#region IStoreListener 核心回调钩子 (Unity IAP 4.x)
public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
{
m_StoreController = controller;
m_StoreExtensionProvider = extensions;
IsInitialized = true;
Debug.Log("[UnityIAP4][Service] Unity IAP 4.x 成功初始化,已连通底层 Windows Store Commerce API!");
RefreshProductInfosAndNotify();
}
public void OnInitializeFailed(InitializationFailureReason error)
{
Debug.LogError($"[UnityIAP4][Service] 商店初始化失败。原因: {error}");
IsInitialized = false;
}
public void OnInitializeFailed(InitializationFailureReason error, string message)
{
Debug.LogError($"[UnityIAP4][Service] 商店初始化失败。原因: {error}, 错误信息: {message}");
IsInitialized = false;
}
/// <summary>
/// 当购买流程最终确认交易成功时,Unity IAP 底层会触发此核心回调
/// </summary>
public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs purchaseEvent)
{
var product = purchaseEvent.purchasedProduct;
string productId = product.definition.id;
Debug.Log($"[UnityIAP4][Service] 收到支付成功回调!内部商品 ID: {productId}, 微软交易 ID: {product.transactionID}");
var info = productInfos.Find(p => p.InternalProductId == productId);
if (info != null)
{
info.IsOwned = true;
if (product.definition.type == ProductType.Subscription)
{
ParseSubscriptionExpiration(product, info);
}
}
// 触发成功事件,供上层业务层(如给玩家分发金币等)进行确权
OnPurchaseSucceeded?.Invoke(productId);
// 返回 Complete 代表我们已经成功处理了此笔交易,Unity IAP 将向底层微软商店确认核销
return PurchaseProcessingResult.Complete;
}
void IStoreListener.OnPurchaseFailed(Product product, PurchaseFailureReason failureReason)
{
Debug.LogError($"[UnityIAP4][Service] 商品购买失败!内部 ID: {product.definition.id}, 原因: {failureReason}");
OnPurchaseFailed?.Invoke(product.definition.id, failureReason.ToString());
}
void IDetailedStoreListener.OnPurchaseFailed(Product product, PurchaseFailureDescription failureDescription)
{
Debug.LogError($"[UnityIAP4][Service] 商品购买失败!内部 ID: {product.definition.id}, 原因: {failureDescription.reason}, 描述: {failureDescription.message}");
OnPurchaseFailed?.Invoke(product.definition.id, failureDescription.reason.ToString());
}
#endregion
#region 私有辅助方法
private void RefreshProductInfosAndNotify()
{
if (m_StoreController == null) return;
productInfos.Clear();
foreach (var product in m_StoreController.products.all)
{
var info = new WindowsStoreProductInfo
{
InternalProductId = product.definition.id,
WindowsStoreProductId = product.definition.storeSpecificId,
Title = product.metadata.localizedTitle,
Description = product.metadata.localizedDescription,
FormattedPrice = product.metadata.localizedPriceString,
IsOwned = product.hasReceipt,
ProductKind = product.definition.type.ToString(),
ExpirationDate = DateTime.MinValue
};
if (product.definition.type == ProductType.Subscription)
{
if (product.hasReceipt)
{
ParseSubscriptionExpiration(product, info);
}
else
{
info.IsOwned = false;
}
}
productInfos.Add(info);
Debug.Log($"[UnityIAP4][Service] 加载商店商品目录成功: 内部 ID: {info.InternalProductId} | 价格: {info.FormattedPrice} | 已拥有: {info.IsOwned}");
}
OnProductsLoaded?.Invoke();
}
/// <summary>
/// 解析订阅有效性及过期日期
/// </summary>
private void ParseSubscriptionExpiration(Product product, WindowsStoreProductInfo info)
{
try
{
// 使用 Unity 提供的 SubscriptionManager 提取底层的订阅收据信息
var subscriptionManager = new SubscriptionManager(product, null);
var subInfo = subscriptionManager.getSubscriptionInfo();
info.IsOwned = subInfo.isSubscribed() == Result.True;
DateTime expireDate = subInfo.getExpireDate();
if (Application.isEditor || expireDate == DateTime.MinValue || expireDate.Ticks == 0)
{
// 编辑器模拟,赋予一个 30 天后的默认到期时间
info.ExpirationDate = DateTime.Now.AddDays(30);
}
else
{
info.ExpirationDate = expireDate.ToLocalTime();
}
}
catch (Exception ex)
{
Debug.LogWarning($"[UnityIAP4][Service] 订阅收据解析失败 (在编辑器中属正常情况): {ex.Message}");
info.IsOwned = product.hasReceipt;
if (info.IsOwned)
{
info.ExpirationDate = DateTime.Now.AddDays(30);
}
}
}
#endregion
}
}
4. 业务管理层单例实现:PurchaseManager
作为暴露给游戏 UI 层及广告管理器等业务系统进行调用的 Manager,除了控制初始化和购买外,还会负责本地的持久化缓存(如"去广告"状态的记录)。
csharp
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Assets.Scripts.Purchasing
{
[DisallowMultipleComponent]
public sealed class PurchaseManager : MonoBehaviour
{
public static PurchaseManager Instance { get; private set; }
[Header("设置")]
[Tooltip("如果启用,支付服务将在 Awake 时自动初始化。")]
[SerializeField] private bool initializeOnAwake = true;
private IPurchaseService purchaseService;
private bool isProcessingPurchase;
public bool IsInitialized => purchaseService != null && purchaseService.IsInitialized;
public IPurchaseService ActiveService => purchaseService;
#region 外部监听事件
public event Action<string> OnPurchaseSucceeded;
public event Action<string, string> OnPurchaseFailed;
public event Action<string> OnPurchaseRestored;
public event Action<string, string> OnPurchasePendingOrUnknown;
public event Action OnProductsLoaded;
#endregion
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
InitializeService();
}
private void InitializeService()
{
Debug.Log("[UnityIAP4][Manager] 正在为当前平台初始化支付服务适配器...");
// 实例化我们刚才封装的适配器
purchaseService = new UnityIAP4PurchaseService();
// 监听事件绑定
purchaseService.OnPurchaseSucceeded += HandlePurchaseSucceeded;
purchaseService.OnPurchaseFailed += HandlePurchaseFailed;
purchaseService.OnPurchaseRestored += HandlePurchaseRestored;
purchaseService.OnPurchasePendingOrUnknown += HandlePurchasePendingOrUnknown;
purchaseService.OnProductsLoaded += HandleProductsLoaded;
if (initializeOnAwake)
{
purchaseService.Initialize();
}
}
public void Initialize()
{
if (purchaseService != null && !purchaseService.IsInitialized)
{
purchaseService.Initialize();
}
}
public void Buy(string productId)
{
if (purchaseService == null)
{
Debug.LogError("[UnityIAP4][Manager] 购买失败:支付服务未初始化!");
OnPurchaseFailed?.Invoke(productId, "支付服务不可用。");
return;
}
if (isProcessingPurchase)
{
Debug.LogWarning($"[UnityIAP4][Manager] 另一个购买流程正在进行中,已忽略此次对 {productId} 的购买请求。");
return;
}
isProcessingPurchase = true;
purchaseService.Buy(productId);
}
public void RestorePurchases()
{
if (purchaseService == null)
{
Debug.LogError("[UnityIAP4][Manager] 恢复购买失败:支付服务未初始化!");
return;
}
purchaseService.RestorePurchases();
}
public bool IsPurchased(string productId)
{
if (purchaseService == null) return false;
return purchaseService.IsPurchased(productId);
}
public List<WindowsStoreProductInfo> GetProductInfos()
{
if (purchaseService == null) return new List<WindowsStoreProductInfo>();
return purchaseService.GetProductInfos();
}
#region 内部事件接收处理器 (本地确权发货业务逻辑)
private void HandlePurchaseSucceeded(string productId)
{
isProcessingPurchase = false;
Debug.Log($"[UnityIAP4][Manager] 支付回调成功,已购商品 ID: {productId}");
// 本地游戏业务层确权发货
if (productId == PurchaseProductIds.RemoveAds)
{
SaveLocalRemoveAdsState(true);
}
else if (productId == PurchaseProductIds.Coins100)
{
AddCoins(100);
}
else if (productId == PurchaseProductIds.Coins500)
{
AddCoins(500);
}
else if (productId == PurchaseProductIds.Coins1000)
{
AddCoins(1000);
}
OnPurchaseSucceeded?.Invoke(productId);
}
private void HandlePurchaseFailed(string productId, string reason)
{
isProcessingPurchase = false;
Debug.LogWarning($"[UnityIAP4][Manager] 支付回调失败,商品 ID: {productId},原因: {reason}");
OnPurchaseFailed?.Invoke(productId, reason);
}
private void HandlePurchaseRestored(string productId)
{
Debug.Log($"[UnityIAP4][Manager] 历史订单恢复成功,商品 ID: {productId}");
if (productId == PurchaseProductIds.RemoveAds)
{
SaveLocalRemoveAdsState(true);
}
OnPurchaseRestored?.Invoke(productId);
}
private void HandlePurchasePendingOrUnknown(string productId, string status)
{
isProcessingPurchase = false;
OnPurchasePendingOrUnknown?.Invoke(productId, status);
}
private void HandleProductsLoaded()
{
Debug.Log("[UnityIAP4][Manager] 商店元数据加载完毕事件已触发。");
// 自动同步非消耗品"去广告"的本地状态缓存
if (IsPurchased(PurchaseProductIds.RemoveAds))
{
SaveLocalRemoveAdsState(true);
}
OnProductsLoaded?.Invoke();
}
#endregion
#region 本地游戏持久化逻辑辅助 (PlayerPrefs 模拟)
private void SaveLocalRemoveAdsState(bool isRemoved)
{
PlayerPrefs.SetInt("Game_RemoveAds_Unlocked", isRemoved ? 1 : 0);
PlayerPrefs.Save();
Debug.Log($"[UnityIAP4][Manager] 本地已更新去广告状态为: {isRemoved}");
}
public bool AreAdsRemoved()
{
return PlayerPrefs.GetInt("Game_RemoveAds_Unlocked", 0) == 1 || IsPurchased(PurchaseProductIds.RemoveAds);
}
public int GetCoinsBalance()
{
return PlayerPrefs.GetInt("Game_Coins_Balance", 0);
}
public void AddCoins(int amount)
{
int current = GetCoinsBalance();
PlayerPrefs.SetInt("Game_Coins_Balance", current + amount);
PlayerPrefs.Save();
Debug.Log($"[UnityIAP4][Manager] 已添加 {amount} 金币。当前新金币余额为: {GetCoinsBalance()}");
}
#endregion
}
}
四、 编译构建与真机联调
1. 配置 AppxManifest 权限
在 Unity 的 Player Settings > Publishing Settings 或者 Visual Studio 的 Package.appxmanifest 中,确保勾选了 Internet (Client) / 互联网(客户端) 权限。否则游戏被沙盒隔离后,无法发起任何网络请求去微软服务器拉取价格。
2. 导出与部署
- 在 VS 中,将上方工具栏的编译平台配置为 x64 (不要选 ARM 或 ARM64,除非在特定移动端/Xbox),配置类型选择 Debug 或 Release。
- 点击 Local Machine (本地计算机) 开始部署。
3. 真实的真机联调测试方案(私有发布与 Package Flighting 流程)
在微软 Partner Center 中,普通的 Windows 应用开发者无法像其他平台那样直接创建简单的沙盒测试账号 。因为在微软体系中,"创建沙盒测试账号"是专属于 Xbox Live Services (Xbox 开发者计划 / ID@Xbox) 的高级权限,一般应用账号申请会直接提示"没有权限创建"。
针对常规 UWP 应用,目前业界验证最成功的零成本真机联调与支付测试方案主要有以下两种:
方案 A:以私有方式发布应用 (Private Visibility)
- 在 Partner Center 应用后台中,进入 Pricing and availability (定价和可用性) -> Visibility (可见性)。
- 将其设置为 Private audience (私有受众)。
- 创建一个 Customer group (测试用户组),并将你的开发测试用微软账号 (Email) 添加到该组中。
- 提交发布。普通公众用户在微软商店中无法通过搜索或直接链接发现该应用,但只要是处于 Customer group 组内的微软账号,登录本机的微软商店 (Microsoft Store) 后即可下载该应用并安全地调起测试交易。
方案 B:创建 Package Flighting (程序包外部测试)
- 在 Partner Center 对应的应用导航栏中选择 Package flights (程序包外部测试)。
- 创建一个新的外部测试组,例如命名为
AlphaTest。 - 创建或关联一个包含测试人员微软账号的 Customer group (客户群)。
- 上传你的 UWP 应用包并提交外部测试。测试人员接受邀请后,他们的系统账号即获得了专属的内购测试权。
!NOTE
测试交易弹出标志 :当测试账号通过上述方式安装应用并点击购买时,弹出的微软原生内购支付框会明确标注**"这是一笔测试交易,不会扣除真实费用"**(或者以
0元直接确认结算)。这代表你的联调测试大功告成!
⚠️ 极其关键:为什么 VS 关联应用后"直接本地部署运行"无法完成内购?(开发机授权激活桥梁)
仅仅在 Visual Studio 中关联商店应用并直接本地部署运行(F5 部署),是压根无法完成内购的。
- 原因 :微软 WinRT
StoreContext的内购 API 必须依靠本地 Windows OS 内注册的合法应用许可证书(Entitlement/Licensing Context) 。当你在 VS 中直接本地部署时,虽然 Package 身份匹配,但你的微软账号在本地系统上从未在该应用下成功建立合法的商店下载/交易注册关系 。此时 OS 无法定位该 Package Family Name 的购买凭证,会导致StoreContext拉取不到目录详情,或者结算直接抛出 HRESULT 异常。 - 终极解决方法(授权激活桥) :
- 必须首先通过商店下载安装一次 :在你的测试开发机上,使用测试用的微软账号通过你的 Package Flight (外部测试链接) 或者 Private 私有发布链接 (在 Microsoft Store 客户端中)正式下载并安装一次该应用。这一步的目的是为了让 Windows 系统注册"该微软账号已拥有该 Package 许可证"的注册表记录。
- VS 本地覆盖调试 :完成商店版的初次下载安装后,直接在 Visual Studio 中运行你的本地编译部署/覆盖调试(或 F5 调试) 。Visual Studio 会自动用你的本地开发代码覆盖掉刚刚下载的商店版本,但会完全保留并共享该 Package 相同的合法授权环境!
- 顺利测试 :此时,在你的 VS 调试运行版中,再次触发
StoreContext内购代码,微软的"这是一笔测试交易"结算框就会如期完美弹出! - 🚫 极其关键的网络限制:关闭本机的网络代理 / VPN :在本地 VS 部署运行调试时,必须完全关闭本机的 VPN、游戏加速器或各类全局/系统网络代理 。这是因为 UWP 应用处于 AppContainer 沙盒安全网络限制中,大部分 VPN 的系统级路由无法正确穿透此沙盒,或者会将连接区域强行切换(例如定位到海外 IP,导致与你微软账号实际绑定的市场区域发生冲突),这将直接导致
StoreContext初始化完全卡死、无法获取商店目录,或根本无法调起微软的内购结账弹窗。
结语
通过将 微软合作伙伴中心的 12 位 Add-on ID 映射 、Unity IAP 4.x 的标准化侦听器实现 以及 面向接口设计的单例管理器层 进行三位一体的结合,我们便能在 Windows 平台上构建一套极其健壮、抗干扰、可维护性强的应用内购系统。这不仅能极大地提升玩家的付款成功率,还能有效防范各类非法刷单和本地断网等特殊情况。