Godot 商店购买确认流程:货币、折扣和二次确认都要经得起误触

设计 Godot 游戏内商店购买确认流程,处理价格展示、折扣、二次确认、余额变化和失败回滚。

游戏内商店的购买按钮很敏感。玩家可能用金币、钻石、活动券或多种货币购买道具,商品可能限购、打折、绑定礼包、动态刷新。一次误触、一次价格显示错误、一次失败后 UI 状态没回滚,都可能引发信任问题。

Godot 里的商店 UI 可以很快做出来,但购买确认流程不能只是按钮点击后发请求。它需要价格快照、二次确认、余额校验、服务器结果、奖励展示和失败回滚。尤其移动端和手柄环境下,误触和焦点默认值也要考虑。

项目里的真实问题

某次活动商店中,商品显示 80 钻折扣价,但玩家点击购买时服务器按 100 钻扣款,因为客户端价格配置和服务器活动配置不同步。另一次测试中,玩家余额不足后按钮仍显示可购买,连续点击产生多个失败 Toast。还有玩家在确认框中默认焦点落在购买按钮,连按确认误买了礼包。

商店购买要把“展示价格”和“提交价格”绑定成快照。玩家确认的应该是某个商品、某个价格、某个货币、某个限购状态,而不是 UI 当前随时变化的文本。

设计目标

  • 价格可信:确认框展示服务器或可信快照价格,不使用过期 UI 文本。
  • 误触可防:高价值商品、消耗稀有货币时有二次确认和安全焦点。
  • 状态可回滚:购买失败后余额、限购和按钮状态恢复正确。
  • 结果可对账:服务器发放结果驱动奖励展示,不由客户端猜测。

目标不是把一个小功能做成庞大平台,而是让它进入真实项目后仍然可维护。Godot 的 Node、信号和 Resource 很适合快速验证,但功能一旦要覆盖多个页面、多个平台和多次版本更新,就必须把状态、配置、失败路径和观测方式拆清楚。下面的方案都围绕一个原则:业务脚本提交意图,系统层做决策,表现层只消费快照。

推荐架构

flowchart TD
    A["购买意图"] --> B["ShopPurchaseFlow"]
    B --> C["价格快照"]
    B --> D["二次确认"]
    B --> E["服务器提交"]
    B --> F["结果展示"]
    C --> G["状态快照"]
    D --> G
    E --> G
    F --> G
    G --> H["UI反馈/日志/回滚"]

这张图里的模块可以按项目规模合并。小团队可以用一个 Autoload 管理,大团队可以拆成配置 Resource、Service、ViewModel 和调试面板。关键是调用方向要稳定:场景和 UI 不直接修改底层状态,而是提交意图并订阅快照。这样测试、灰度和回滚才有抓手。

关键实现细节

购买流程从 PurchaseIntent 开始:商品 id、数量、入口、客户端展示版本。请求确认前先拿 PurchaseQuote,包含价格、货币、折扣、限购、有效期和服务器 quote_id。确认框显示 quote 内容。提交购买时带 quote_id,服务器判断是否仍有效。
确认框默认焦点要保守。普通金币小额购买可以默认确认,高价值钻石、不可退款礼包、限购商品应默认取消或要求长按确认。手柄和触屏都要防误触。
余额变化要以服务器结果为准。提交后本地可以进入 pending,禁用按钮并显示处理中。成功返回后更新余额和限购,展示实际获得物;失败返回后刷新商品状态并显示具体原因。
折扣倒计时要和 quote 有效期区分。UI 上活动还有 10 分钟,不代表当前 quote 永远有效。确认框停留太久后,提交前应重新报价或提示价格已变化。

失败处理和恢复路径

服务器返回 price_changed 时,关闭旧确认框,展示新价格并要求玩家重新确认。不要在后台自动按新价格购买。
余额不足时,提示缺少哪种货币,并提供获取入口,但不要自动跳转打断当前流程。
购买请求超时后,进入查询订单状态。不能简单显示失败,因为服务器可能已经扣款发货。

数据契约和协作接口

PurchaseQuote 包含 quote_id、item_id、quantity、currency、price、discount_label、expires_at、limit_state。
RewardPresenter 只展示服务器 purchase_result 中的 grants,不从商品配置猜奖励。
商店按钮订阅商品快照和 pending 状态,不自己维护可买状态。

GDScript 接口草图

class_name ShopPurchaseFlow
extends Node

signal snapshot_changed(snapshot: Dictionary)
signal rejected(reason: String, payload: Dictionary)

var _snapshot := {}
var _op_version := 0

func apply_intent(intent: Dictionary) -> void:
    _op_version += 1
    var version := _op_version
    _snapshot = {"phase": "checking", "intent": intent}
    emit_signal("snapshot_changed", _snapshot)
    _execute(intent, func(result: Dictionary):
        if version != _op_version:
            return
        if not result.get("accepted", false):
            emit_signal("rejected", result.get("reason", "unknown"), result)
            return
        _snapshot = result
        emit_signal("snapshot_changed", _snapshot)
    )

func snapshot() -> Dictionary:
    return _snapshot.duplicate(true)

接口草图保留了版本号,是因为很多客户端问题来自异步乱序:玩家快速切换页面、网络请求晚返回、资源加载被取消后又完成。如果旧结果可以覆盖新状态,问题会非常隐蔽。实际项目里还要补超时、取消、错误码和日志字段。

分阶段落地

第一阶段接入报价、确认框和 pending 状态。
第二阶段处理价格变化、限购刷新和订单查询。
第三阶段加入高价值二次确认、手柄焦点策略和购买日志。

自动化验证和人工验收

活动折扣变化时,旧确认框提交返回 price_changed 并要求重新确认。
余额不足、限购已满、商品下架分别显示不同原因。
请求超时后查询订单,避免重复扣款或重复发货。
高价值商品确认框默认焦点在安全选项。

观测指标

  • 购买报价成功率和过期率。
  • 购买失败原因分布。
  • 订单超时查询次数。
  • 确认框取消率和误触反馈数量。

指标不一定全部进入正式服。开发包可以显示完整调试面板,内测包采样关键计数,正式包只保留错误码和聚合趋势。指标的目的不是制造报表,而是让一次异常能被定位到具体阶段、具体配置和具体玩家路径。

上线前检查清单

  • 购买提交使用 quote_id。
  • 确认框显示价格、货币、数量和限购状态。
  • 高价值商品有安全确认策略。
  • 购买结果由服务器 grants 驱动。
  • 超时后查询订单,不直接重试扣款。

检查清单要随着事故复盘不断更新。每次问题暴露后,都问它是否能变成自动检查、灰度指标或人工验收步骤。能沉淀下来的经验,才会在下一次版本里真正保护团队。

工程落地补充

商店还要处理多货币组合。某些礼包可能需要金币加活动券,或者优先消耗免费钻石再消耗付费钻石。确认框必须把扣除顺序说清楚。只显示一个总价,会让玩家不理解为什么某种货币减少。

限购状态也要以服务器为准。客户端可以显示剩余次数,但购买提交后若服务器返回 limit_reached,必须刷新商品列表。不要因为本地显示还有次数,就在失败后继续保持按钮可点。

配置版本也很重要。系统上线后,配置会跟着内容迭代不断变化:新增步骤、新增音频规则、新增安全区 profile、新增商品或新增目标类型。每份配置都应该有 version 和 lastmod,客户端日志里记录当前版本。出现问题时,团队能知道玩家使用的是哪一版配置,而不是只看到一个模糊的功能名。

调试入口要从第一版就准备。不要等问题出现后再临时加日志。开发包至少能显示当前快照、最近一次意图、失败原因和配置来源。QA 报告如果能带上这四个信息,排查效率会比只发截图高很多。对于 UI 类系统,最好能在截图角落显示关键 id,例如 step_id、marker_id、quote_id 或 target_id。

团队协作边界

这类系统通常不是单个程序能独立定完的。策划需要确认规则和文案,美术或 UI 需要确认表现,QA 需要确认验收脚本,服务端或平台同学需要确认接口边界。建议在文章对应的系统落地时,把“谁能改配置、谁能发开关、谁负责看指标”写在 README 或内部文档里。

同时要约定变更流程。新增一个教程步骤、新增一种购买错误码、新增一个目标类型、新增一个音频 ducking 规则,都应该有最小验收样本。没有样本的配置变更,很容易在下一次内容更新时破坏既有路径。把样本保留下来,后续自动化才能逐步建立。

案例复盘

一次活动折扣事故中,客户端缓存旧价格,服务器已恢复原价。旧流程直接提交购买,玩家看到低价却被高价扣款。接入 Quote 后,服务器返回 price_changed,客户端展示新价格并要求重新确认。虽然多了一步,但避免了信任事故。

灰度验收脚本

灰度验收可以模拟价格变化、商品下架、余额不足、服务器超时和订单成功但响应丢失。每个场景都要检查余额、限购、奖励展示和按钮状态。商店流程不能只测成功购买。

验收边界补充

验收时还要覆盖连续购买。玩家快速购买多个低价商品时,余额、限购和按钮状态必须逐单刷新。不能因为上一单 pending,就让下一单使用旧余额或旧 quote。

每次验收都要同时看成功路径和失败路径。成功路径证明功能能跑,失败路径证明系统不会把玩家带进不可理解的状态。对于这类客户端系统,最容易漏测的往往不是主流程,而是取消、超时、配置缺失、目标失效、切场景和重进游戏。把这些边界做成固定脚本,后续内容扩展时才能继续复用。

另外,验收结果要能落到文件或截图里。只说“体感还行”不够,至少要有关键状态快照、调试面板截图或日志片段。系统越复杂,越需要可保存的证据。这样下一次同类问题出现时,团队能对比前后行为,而不是重新凭记忆讨论。

最后落地补充

商店确认流程还要处理赠送和免费领取。价格为 0 不代表可以跳过确认,限时免费礼包、订阅赠品和平台补偿仍需要服务器结果驱动奖励展示,否则很容易和普通购买走出两套状态。

微调补充

另外,确认框停留期间要冻结显示用的 quote。后台商品列表可以刷新,但确认框不应悄悄改价格。价格变化只能关闭旧确认并重新询价。

小团队接入版本

小团队可以先不做复杂订单系统,但至少要做 PurchaseQuote 和 pending 状态。只要玩家确认的是稳定价格,失败后 UI 能刷新,商店就比直接按钮购买安全很多。

交付边界

交付标准是玩家知道自己买了什么、花了多少、是否成功到账。商店购买是信任入口,宁可流程多一次确认,也不要让价格和结果含糊。

结语

Godot 商店购买流程不只是一个按钮。价格快照、二次确认、服务器结果和失败查询共同构成可信交易体验。把这些边界做好,玩家才会相信商店显示的每一个数字。

继续阅读

探索更多技术文章

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

全部文章 返回首页