EP4 把变现的命脉——“金币随玩家进度(最高分)缩放”——的算法做出来并验证了。但我也诚实地说了:那一集只有算法,没有商城、没有真内购。这一集(EP5)把它落地:一份可购买的 SKU 目录,一条购买发放的路径,外加 Unity IAP 的诚实接线。

先看真机跑起来的商城(这一集做完的样子,Unity 里实拍)——左边是新手(最高分 1),右边是封顶(最高分 8338),两张价格栏一模一样,金币栏整体差约 118 倍

真机商城 · 新手(最高分 1)

真机商城 · 封顶(最高分 8338)

$1.99 那一档,新手到账 2,200 金币,封顶到账 260,000——同一个商品、同一个价格,价值随玩家成长自己往上长。下面把这套是怎么实现的拆开讲。

这一集有一个我特别想讲清楚的工程取舍——怎么在一台没配商店的开发机上,把"内购发放"这件事验证到 9 条断言全绿

SKU 目录:数据资产,金币不写死

商城的"商品表"是一份 ScriptableObject(可服务端下发)。但注意——金币数量不在表里。表里只放一个 coef(经济系数),运行时喂给 EP4 的 EconomyConfig 按玩家当前进度(最高分)算。道具数量倒是定额(道具不随进度缩放)。

[CreateAssetMenu(menuName = "BlockBlast/ShopConfigDatabase")]
public class ShopConfigDatabase : ScriptableObject
{
    [System.Serializable]
    public struct Sku
    {
        public string id;            // 内部 id
        public string productId;     // 商店内购 productId
        public string displayName;
        public float priceUsd;
        public float coef;           // 经济系数(喂给 EconomyConfig,不是写死的金币数)
        public int grantUndo, grantBomb, grantSwap;   // 定额道具
    }
    public Sku[] skus;
}

配了 7 个 SKU:5 个纯金币包($0.99 ~ $19.99,coef 递增)+ 2 个含道具的礼包。

商城逻辑:展示价按进度实时算

商城面板要显示"这个包能拿多少金币"。这个数不是静态的——同一个 SKU,新手看到的和老玩家看到的不一样,因为它现算:

public int DisplayCoins(string skuId)
{
    if (!Shop.TryGet(skuId, out var sku)) return 0;
    return Econ.CoinsFor(sku.coef, UserDataModel.Instance.curLevel.Value);
}

这就是 EP4 那条经济曲线第一次真正被"用"起来:商城的每一格价签,都是 CoinsFor(coef, 当前进度) 的实时结果(curLevel 是内部进度入参,喂的是玩家最高分)。

关键设计:发放路径与 IAP 解耦

这是这一集最重要的一个决定。真实内购要装 com.unity.purchasing、配商店后台、真机联调——一台干净的开发机上根本跑不起来。如果发放逻辑写在 IAP 的成功回调里,那它就永远无法在开发期验证

所以我把"发放"抽成一个与 IAP 无关的口子 GrantPurchase真商店成功回调和 mock 测试都调它

public int GrantPurchase(string skuId)
{
    if (!Shop.TryGet(skuId, out var sku)) return 0;
    int coins = Econ.CoinsFor(sku.coef, UserDataModel.Instance.curLevel.Value);
    if (coins > 0) CoinModel.Instance.ChangeCoin(coins, "iap:" + skuId);
    if (sku.grantUndo > 0) BoosterModel.Instance.Add(BoosterType.Undo, sku.grantUndo);
    if (sku.grantBomb > 0) BoosterModel.Instance.Add(BoosterType.Bomb, sku.grantBomb);
    if (sku.grantSwap > 0) BoosterModel.Instance.Add(BoosterType.Swap, sku.grantSwap);
    UserDataModel.Instance.lastPayTime.Value = NowUnix();
    MsgController.Send(new PurchaseGrantedMsg { skuId = skuId, coins = coins });
    return coins;
}

真 IAP 接线层用 #if UNITY_PURCHASING 守卫——装了 IAP 包就走 IStoreListener.ProcessPurchase,没装就是个会打警告的占位类,保证编译。而那个 ProcessPurchase 里干的唯一一件实事,就是回调 GrantPurchase(sku.id)

public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs args)
{
    foreach (var sku in _db.skus)
        if (sku.productId == args.purchasedProduct.definition.id)
        { ShopController.Instance.GrantPurchase(sku.id); break; }
    return PurchaseProcessingResult.Complete;
}

收钱归 IAP 包管,发放归我管且可测。这就是为什么下面 9 条断言覆盖的,是真机上也会跑的同一段发放代码。

命脉可视化:同一份目录,自己重新定价

把 5 个金币包在新手(最高分 1)和封顶(最高分 8338)的到账金币画出来——价格一栏没动,金币栏整体放大约 118 倍

同一份定价目录随进度重新定价

$1.99 这一档,新手买到账 2,200 金币,打到封顶买同样的 $1.99 到账 260,000。整份目录跟着玩家成长自己重新定价——这正是 EP4 那条曲线的商业意义落到商城上的样子:玩家打得越深,同样的钱越值,越值得掏。

验证:9 条断言(覆盖真实发放路径)

     coin_0199 展示金币: 最高分1=2200  最高分8338=260000
PASS  A 展示价有效(>0)
PASS  A 展示价随进度放大(最高分8338 > 最高分1)
PASS  B 发放金币 > 0
PASS  B 余额增加了发放金币
PASS  B 道具 Undo +1
PASS  B PurchaseGrantedMsg 触发且金币一致
PASS  B lastPayTime 已记录(>0)
PASS  C 无效SKU发放==0
PASS  C 无效SKU余额不变
==== 9/9 PASS ====

钉死的几条:展示价随进度放大(同 SKU 老玩家看到更多)、购买后金币与道具精确到账PurchaseGrantedMsg 广播且金币一致(UI/成就/埋点各自订阅)、lastPayTime 记录(留存与付费分层要用)、无效 SKU 不发放不改余额(防脏数据刷币)。因为发放走的是 GrantPurchase 这条真机同款路径,这 9 条绿等于把真实内购的发放逻辑钉死了。

用 MCP 把商城闭环点一遍(不是断言,是真在玩)

断言验的是逻辑,但"商城能不能点、点了金币动不动"得在真游戏里走一遍。这一步全程是 Claude 用 funplay MCP 驱动的——simulate_mouse_click 点 uGUI 按钮、capture_game_view 截图回看,一个人没碰鼠标:

  1. 点开商城simulate_mouse_click(144, 51) → MCP 回 EventSystem: clicked on 'Label',商城面板弹出,7 个 SKU 按新手(最高分 1)经济曲线显示金币(一袋金币 2,200)。就是文章开头那张新手截图。
  2. 把进度(最高分)调到封顶再点开:商城自己重新定价——同样 $1.99 变成 260,000 金币(×118)。价格栏一个字没改,这就是 EP4 那条曲线活在商城里的样子(开头第二张封顶截图)。
  3. 真买一单:金币先归到 50,000,simulate_mouse_click(538, 758) 点"一桶金币 $9.99"的购买键 → MCP 回 Direct button hit: invoked 'Btn'GrantPurchase 跑通,顶栏金币实时跳到 63,000(+13,000)

真机购买:点 $9.99 → 金币 50,000 → 63,000,顶栏经事件总线实时刷新

点击 → GrantPurchaseCoinModel.ChangeCoinCoinChangedMsg → 顶栏订阅刷新——这条链路从 UI 一直贯到 EP4 的货币地基,全程在真 PlayMode 里被 MCP 点出来、截图证下来。这就是"测试游戏闭环"本身:不是我说它对,是它在屏幕上真的动了。

这一集的产物与诚实的话

  • ShopConfigDatabase(SKU 目录,数据资产)+ ShopController(展示价现算 + GrantPurchase 发放)+ BoosterModel(道具库存)+ ShopIapController#if 守卫的真 IAP 接线)。
  • ShopConfigDatabase.asset 放进 Resources(7 个 SKU),运行时加载。
  • 真商城面板(uGUI,7 个 SKU 卡片 + 购买键,接 GrantPurchase),上面三张截图就是它真跑起来的样子。
  • 9 条断言全绿 + 一次 MCP 全程点击实测,覆盖的是真机同款发放路径。

诚实地讲:真实内购需要的"最后一公里"——开发者后台建商品、签约收款、真机 sandbox 联调、收据校验/防作弊——这些我在一台开发机上做不了,需要真实的商店账号和资质。这一集做到的是:把发放逻辑做对、做到可测、把商城界面做出来并用 MCP 点通,并把真 IAP 的接线点留好(一个 GrantPurchase 回调)。剩下的购买动效/粒子会在 EP7 表现层补。

下一篇 EP6:留存系统——离线收益、每日登录、连签、主题皮肤。把"让玩家明天还回来"这件事做出来,它和这一集的变现是一对:留存供给流量,变现把流量变成钱。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐