Phaser 商店、抽卡与奖励流:本地动画要服从权威结果

讨论 Phaser 游戏中商店、抽卡、奖励展示、概率说明、本地表现、服务端权威和失败回滚的完整客户端流程。

抽卡动画不能决定结果

商店和抽卡是很多 H5 游戏的商业核心。Phaser 做奖励展示很容易:按钮、转场、卡面翻开、光效、稀有度音效。但越是表现华丽,越要记住边界:本地动画不能决定奖励结果。奖励结果必须来自权威逻辑,本地只负责把结果清楚、稳定、可恢复地展示出来。

我见过一个活动小游戏,抽奖按钮点击后客户端本地先随机一个动画结果,再向服务端请求发奖。弱网时,玩家先看到了一等奖动画,服务端实际返回三等奖,UI 又刷新成三等奖。客服收到反馈时很难解释,因为玩家录屏确实看到了一等奖。这个事故的根本是表现随机越过了权威边界。

正确流程应该是:玩家点击购买或抽卡,客户端锁定按钮并请求服务端;服务端扣费并返回奖励结果;客户端根据结果播放动画;动画结束后写入背包展示。若要缩短等待,可以播放不含具体结果的加载动画,不能提前展示未确认奖励。

sequenceDiagram
    participant UI as Phaser 商店 UI
    participant API as 服务端
    participant Reward as 奖励展示
    UI->>UI: 点击后锁定按钮
    UI->>API: purchase/draw request
    API->>API: 校验货币/概率/库存/风控
    API-->>UI: 返回权威奖励结果
    UI->>Reward: 播放对应稀有度动画
    Reward->>UI: 展示奖励并刷新资产

商品配置要可校验

商店商品至少包含商品 id、价格、货币类型、限购规则、展示时间、奖励内容、购买条件和渠道信息。抽卡池还要包含概率、保底、UP 规则、稀有度、展示资源和概率说明。配置错了,影响的不只是 UI,而是付费和信任。

客户端要校验配置完整性。商品价格缺失、奖励资源不存在、概率总和不为 100%、限购时间无效、展示图缺失,都应该在加载时报警。对线上配置,客户端至少要能安全隐藏异常商品,而不是显示一个能点但买不了的入口。

概率说明要和服务端规则一致。客户端展示的概率文案不能手写一份长期不更新。最好由服务端或同一份配置生成。若活动期调整概率,说明也要同步变化。抽卡透明度是信任问题,不是简单文案。

购买流程要防重复

商店按钮最怕重复点击。玩家连续点两次,网络重试一次,页面恢复又补发一次,都可能导致重复扣费或重复发奖。客户端按钮禁用只是第一层,服务端请求也要有 requestId,保证幂等。

客户端生成 purchaseIntent,带上商品 id、数量、requestId 和客户端版本。服务端返回处理中、成功、失败、已处理等结果。客户端根据结果更新 UI。超时不代表失败,必须查询订单状态或提示稍后确认。不要在超时后直接恢复按钮让玩家再买一次。

如果接平台支付,流程更复杂:创建订单、拉起支付、平台回调、服务端确认、客户端轮询结果。Phaser UI 要清楚显示当前阶段。玩家最怕付了钱但游戏没反应。每个阶段都要有可诊断状态。

奖励展示要可恢复

奖励动画可能播放到一半页面切后台,或者玩家关闭页面。奖励结果已经发放,展示没播完。下次进入游戏时,应该怎么处理?通常可以在本地保存 pendingRewardDisplay,进入大厅后补展示,或者在背包里显示新增标记。

展示不是发奖本身。服务端确认成功后,资产已经变化;动画只是告知玩家。若动画失败,不应该导致奖励丢失。客户端要能从服务端资产或本地 pending 里恢复展示状态。

多奖励展示要有队列。十连抽可能返回 10 个奖励,任务结算和活动礼包也可能同时发奖。RewardPresenter 负责排队展示、合并低价值奖励、突出稀有奖励。不要让多个弹窗同时争抢屏幕。

稀有度表现要服务信息

抽卡动画常见问题是特效过多但信息不清。玩家需要知道拿到了什么、稀有度是什么、是否新获得、是否转化为碎片、当前拥有数量。光效和音效可以增强情绪,但不能遮住结果。

结果页要支持跳过。玩家第一次愿意看完整动画,第十次会想快速查看结果。跳过时直接进入结果汇总,不要跳过奖励确认。十连抽可以先播放最高稀有度揭示,再显示完整列表。

重复奖励转化要说明清楚。抽到已拥有皮肤,转化为多少碎片,碎片有什么用。不要只显示“已拥有”,玩家会觉得亏。奖励系统的文案和动画要解释规则,而不只是展示卡面。

本地预览和真实购买分开

商店里可以预览商品和抽卡池,也可以模拟开箱动画,但必须标明是预览。预览不能写入资产,也不能使用真实概率误导玩家。真实购买流程和预览流程要走不同入口。

活动页常会做“试抽一次”或“预览十连”。这种功能要特别谨慎。若结果不是实际奖励,就不要用和真实抽卡完全一样的结算页。可以用“示例奖励”标签,避免玩家误解。

客户端也不要根据本地时间决定限购和活动结束。可以用服务端时间校正,客户端本地只做显示倒计时。真正购买时服务端再次校验。活动结束瞬间,客户端 UI 可能还没刷新,服务端仍要拒绝。

一个购买意图接口

下面示例展示客户端如何提交购买意图。重点是 requestId 和阶段状态。

type PurchaseIntent = {
  requestId: string;
  productId: string;
  count: number;
};

async function buyProduct(intent: PurchaseIntent) {
  shopUi.lock(intent.productId);
  const result = await shopApi.purchase(intent);
  if (result.ok) rewardPresenter.enqueue(result.rewards);
  else shopUi.showReason(result.reason);
  shopUi.unlock(intent.productId);
}

真实项目要处理超时、轮询、支付中断和已处理订单,但接口方向不变。UI 不直接发奖励,RewardPresenter 不直接扣货币,服务端结果是唯一权威。

日志和客服诊断

商店问题必须可诊断。每次购买或抽卡要记录 requestId、productId、客户端版本、配置版本、请求时间、结果码、奖励摘要。玩家反馈时,客服能用 requestId 查服务端订单,客户端也能导出最近购买状态。

不要只告诉玩家“网络错误”。支付创建失败、平台支付取消、服务端发奖失败、背包满、活动已结束、订单处理中,都应该是不同状态。文案越具体,客服压力越小。

抽卡争议也要能查。概率随机在服务端,客户端记录 drawId 和结果展示即可。玩家说“十连没给奖励”时,工程能确认服务端返回了什么,客户端是否展示失败,背包是否已更新。

上线前检查清单

上线前检查:商品配置是否校验,概率说明是否同源,购买是否幂等,按钮是否防重复,超时是否查询订单,奖励展示是否可恢复,十连是否可跳过,重复奖励是否解释,活动结束是否服务端校验,日志是否包含 requestId。

还要测试弱网、支付取消、页面切后台、重复点击、配置下架、背包满、服务端返回已处理、动画播放中刷新页面。商店和抽卡不是普通 UI,它们直接影响玩家资产和信任。

商店 UI 要处理不可购买状态

商品不能买的原因很多:货币不足、限购次数用完、前置条件未达成、活动未开始、活动已结束、渠道不可用、支付 SDK 未就绪。UI 不应该只把按钮变灰。玩家需要知道具体原因,以及是否有办法解决。货币不足可以跳转获取,前置未达成可以显示条件,活动结束则隐藏或标记已结束。

价格显示也要清楚。免费、折扣、原价、限时礼包、多货币组合都需要不同格式。不要把价格拼成一段普通文本。价格组件应接收货币类型、数量、折扣和本地化格式,统一渲染。否则不同商店页会出现格式不一致。

如果商品会刷新,比如每日礼包和限时商店,刷新倒计时要以服务端时间为准。客户端本地时间只用于显示,购买时服务端再次校验。倒计时到 0 后,UI 要进入刷新中或重新拉配置,不要继续允许点击旧商品。

奖励入账和展示顺序

有些团队希望先播动画,最后再把奖励写入背包;另一些希望服务端一返回就入账。两种都可以,但玩家感知要一致。比较稳的方式是服务端确认后资产立即入账,本地记录 pending display。这样即使动画中断,奖励也不丢。结果页显示的数量来自入账后的资产快照。

如果奖励很多,可以分组展示:高稀有度单独揭示,普通材料合并,货币显示总量变化。背包满时要提前处理,不要动画播完才告诉玩家奖励放不下。服务端可以返回进入邮件、临时箱或直接失败,客户端按结果解释。

奖励流还要避免和其他弹窗打架。抽卡结果、升级弹窗、活动完成、任务奖励可能同时出现。RewardPresenter 应该和全局 ModalStack 协作,按优先级排队。玩家付费后的奖励展示优先级通常高于普通任务提示。

概率、保底和历史记录

抽卡系统如果有保底,客户端展示必须清楚。距离保底还差多少抽,当前 UP 是否生效,保底是否跨池继承,都应该有明确说明。不要让玩家只能靠客服解释规则。客户端可以展示服务端返回的 pityState,但不要本地自行计算权威保底。

抽卡历史也很重要。玩家抽到什么、什么时候抽、用了哪个池子、是否触发保底,都应该能查看。历史记录来自服务端,客户端可以缓存最近结果用于快速展示。若网络不可用,提示历史暂不可用,不要显示不完整数据误导玩家。

概率说明和历史记录是信任基础。动画越华丽,规则越要透明。尤其是付费抽卡,客户端要避免任何看起来像“先播好结果再改掉”的表现。所有稀有度揭示都应严格基于已确认结果。

失败状态要保持资产安全

商店失败不是一个状态。扣费成功但发奖处理中、支付取消、支付成功但客户端断线、服务端返回重复订单、背包满转邮件,这些都要分开。客户端要避免在状态不明时让玩家再次购买同一订单。

可以在本地保存 pendingPurchase,记录 requestId 和商品 id。重新进入游戏时先查询 pending 状态,再允许继续购买。若服务端确认成功,补发奖励展示;若确认失败,解除锁定并提示原因。这个流程能减少“我刚刚买了但没收到”的客服问题。

对未成年人、地区限制或渠道限制,客户端也要能展示禁用原因。不要等玩家点到最后一步才失败。商店 UI 的责任是减少无效操作,而不是把所有失败都推给接口。

商店还要能处理配置灰度。同一个玩家看到的商品池可能和另一个玩家不同,客户端日志必须记录配置版本和实验分组。否则收入数据异常时,团队很难判断是 UI 问题、价格问题还是灰度策略问题。

灰度期间客服也要能查询玩家命中的商品配置。玩家截图里的价格和客服后台不同,会严重影响沟通效率。

商店的时间显示也要统一。限时礼包、刷新倒计时、活动结束、折扣剩余都可能同时出现。客户端应该使用同一个时间服务和格式化组件,避免一个页面显示“剩余 1 天”,另一个页面显示“23:59:59”。时间口径不一致会让玩家怀疑规则。

时间到期后的按钮状态也要同步刷新,不要继续展示可购买样式。

如果玩家正停留在商店页,刷新应尽量平滑,避免商品突然消失却没有解释。

可以在商品卡上显示“已结束”并短暂停留,再从列表移除。这样玩家能理解发生了什么,而不是误以为界面闪烁。

刷新前后的商品列表也要写入日志,方便排查限时商品争议。

这个日志至少包含商品 id、价格、活动 id、刷新原因和客户端看到的服务器时间。

结语

Phaser 能把商店和抽卡做得很有表现力,但表现必须服从权威结果。本地动画负责情绪,服务端负责结果,奖励展示负责解释,日志负责诊断。任何一层越界,都会变成玩家信任问题。

好的奖励流不是只让稀有卡闪得更亮,而是让每一次点击、扣费、发奖、展示和恢复都可解释。对商业功能来说,稳定和清楚比炫酷更重要。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页