Phaser 合成与工作台系统:配方、材料校验和队列失败回滚要闭环

讲解 Phaser RPG 和生存游戏中的合成系统设计,包括配方数据、材料校验、工作台队列、时间消耗、UI 反馈和幂等保护。

合成系统不是材料够了就扣

Phaser RPG、生存建造、农场和放置游戏都常见合成系统。玩家把木头和石头放进工作台,得到斧头;把草药和瓶子合成药水;把低级材料升级成高级材料。看起来只是检查材料、扣材料、加物品,但真实项目会很快变复杂:配方需要解锁,工作台有等级,合成需要时间,队列可取消,材料可能来自背包或仓库,活动期间产物翻倍,网络失败要回滚,玩家连续点击不能重复扣料。合成是经济系统的一部分,必须严谨。

合成系统的核心是一个事务:验证条件、锁定或扣除材料、创建任务、完成后发放产物。Phaser Scene 只负责展示配方列表、材料缺口、进度条和完成动画。不要让 UI 按钮直接修改背包数组。只要涉及资源变化,就要通过 CraftingService 或 EconomyService 处理,保证可测试和可恢复。

配方数据要表达限制

一个配方至少包含输入材料、输出物品、工作台要求、耗时、解锁条件和标签。输入材料可以支持替代品,比如任意木材;输出可以有主产物和副产物;耗时可以受工作台等级或 Buff 影响。不要把这些限制写死在 UI 文案里。配方数据是合成规则的唯一来源,UI 根据它生成材料列表和提示。

配方还要有版本。经济系统上线后,配方成本会调整。旧的合成队列如果已经扣料,成本变化不能影响它;新的合成使用新成本。创建合成任务时,应保存配方快照或至少保存配方版本。否则玩家昨天开始制作的装备,今天成本改了,完成时可能发错产物。

flowchart TD
  A["玩家选择配方和数量"] --> B["CraftingService 校验解锁、工作台、材料"]
  B --> C{"是否即时合成"}
  C -- "即时" --> D["Economy 扣材料并发产物"]
  C -- "队列" --> E["创建 CraftJob:锁定材料、记录配方版本"]
  E --> F["WorkbenchQueue 推进时间"]
  F --> G["完成:发放产物、释放队列槽"]
  F --> H["取消:按规则返还材料"]
  D --> I["Phaser UI 播放反馈并刷新背包"]
  G --> I
  H --> I

材料校验要和展示同源

配方列表通常会显示“木头 3/5,石头 2/2”。点击合成时,服务又检查一遍材料。如果展示和检查用不同代码,很容易出现按钮显示可合成,点击却失败。建议写一个 explainCraftability 函数,返回是否可合成以及原因列表。UI 用它显示缺口,合成服务也用它做最终校验。原因可以包括材料不足、工作台等级不够、配方未解锁、队列满、活动已结束。

材料来源也要清晰。是只从背包扣,还是背包加仓库,还是附近箱子也能参与?如果支持多来源,扣除顺序必须固定。比如先扣背包,再扣仓库;或优先扣不可交易材料。否则玩家会困惑为什么珍贵材料被先用掉。扣除结果要能展示,尤其是有替代材料时。

队列合成需要任务状态

带时间的合成不应靠一个倒计时 UI。它应该创建 CraftJob,包含 jobId、recipeId、recipeVersion、数量、开始时间、结束时间、材料快照、状态。状态可以是 pending、running、completed、claimed、cancelled。工作台队列保存这些任务。Scene 打开时根据当前时间计算进度,离线回来也能推进。

取消规则要提前定。取消是否返还全部材料?是否扣手续费?已经完成但未领取能否取消?加速是否消耗道具?这些都影响经济。不要让每个工作台写自己的取消逻辑。合成任务是经济合同,状态变化必须统一。

一个配方校验函数

下面的代码展示如何集中解释合成条件。真实项目中,Inventory 可以更复杂,但思想是返回结构化原因。

interface Ingredient {
  itemId: string;
  count: number;
}

interface Recipe {
  id: string;
  version: number;
  ingredients: Ingredient[];
  output: Ingredient;
  requiredWorkbenchLevel: number;
}

interface CraftContext {
  inventory: Map<string, number>;
  workbenchLevel: number;
  unlockedRecipeIds: Set<string>;
  queueFreeSlots: number;
}

export function explainCraftability(recipe: Recipe, amount: number, ctx: CraftContext) {
  const reasons: string[] = [];
  if (!ctx.unlockedRecipeIds.has(recipe.id)) reasons.push("recipe_locked");
  if (ctx.workbenchLevel < recipe.requiredWorkbenchLevel) reasons.push("workbench_level_low");
  if (ctx.queueFreeSlots <= 0) reasons.push("queue_full");

  for (const ing of recipe.ingredients) {
    const need = ing.count * amount;
    const have = ctx.inventory.get(ing.itemId) ?? 0;
    if (have < need) reasons.push(`missing:${ing.itemId}:${need - have}`);
  }

  return { ok: reasons.length === 0, reasons };
}

UI 可以把 missing:wood:3 转成“还差 3 个木头”,服务可以直接拒绝。注意,不要把用户可见文案塞进核心函数,核心函数返回稳定错误码,本地化层负责翻译。

失败回滚要覆盖每个阶段

合成流程有多个失败点。校验通过后扣材料可能失败,扣材料成功后创建任务可能失败,任务完成时发放产物可能失败,客户端播放动画时 Scene 可能被销毁。每个阶段都要有回滚或补偿策略。纯本地游戏可以先在内存中构造新状态,全部成功后一次保存;有服务端时,可以使用一次请求完成扣料和创建任务,避免客户端中间态。

如果合成任务已经创建,材料通常应视为已锁定。取消时按规则返还。不要让材料既从背包扣掉,又没有任务记录。持久化时,先写任务再写背包或使用统一存档快照,避免浏览器中途关闭导致状态撕裂。IndexedDB 比 localStorage 更适合存较复杂的经济状态。

UI 要显示“为什么不能合成”

灰掉按钮但不解释,是合成系统常见坏体验。玩家需要知道缺什么、去哪里获取、工作台差几级。材料图标可以显示拥有数量和需求数量;点击缺失材料可以打开来源提示;队列满时可以跳到工作台队列;配方未解锁时显示解锁条件。不要让玩家在背包和工作台之间来回猜。

完成反馈也要克制。一次批量合成 50 个木板,不需要播放 50 次飞行动画。可以合并产物展示,重要装备用特殊动画,普通材料用简短提示。移动端上,工作台 UI 要防止连点。点击合成后按钮进入处理中,服务返回后再恢复。

多工作台和协作边界

后期可能有熔炉、料理台、炼金台、装备台。它们可以共享 CraftingService,只是工作台类型和配方池不同。不要为每个工作台复制一套合成代码。工作台实例保存队列和等级,配方配置声明需要的工作台类型。这样活动期间新增“节日烤炉”也只是新增工作台类型和配方池。

如果游戏有联机或好友协作,合成状态更要权威。多个客户端同时操作同一个工作台时,服务端必须串行处理。Phaser 客户端可以乐观展示,但最终以服务端状态为准。领取附件、扣材料、加产物都需要幂等 id,避免重复点击和重试造成双倍收益。

上线前检查清单

确认配方数据包含输入、输出、工作台、耗时、解锁和版本;确认展示和校验使用同一个解释函数;确认材料扣除顺序固定;确认队列任务有明确状态;确认取消、加速、完成、领取都有统一规则;确认失败时不会出现扣料无任务或任务无产物;确认 UI 能解释不可合成原因;确认批量合成不会刷屏;确认工作台类型复用同一服务;确认合成日志记录 jobId、recipeVersion 和材料快照。

合成系统看似是一个菜单,实际是玩家经济循环的核心。Phaser 能把工作台做得很有手感,但扣料、排队、完成和回滚必须严谨。让每一次合成都像一份清楚的订单,玩家才会信任这个世界的资源规则。

配方发现和收藏

很多游戏不会一开始展示全部配方,而是通过探索、任务、等级或 NPC 教学逐步解锁。配方发现本身也要建模。玩家可能看到未知配方的轮廓、已知材料但未知产物,或完全隐藏。UI 应区分“未发现”“已发现但未解锁”“可制作”。不要把这些状态混成灰色按钮。玩家需要知道下一步该去哪里推进。

配方收藏对中后期很有用。玩家常用的药水、弹药、建筑材料可以固定在列表顶部,或者在材料足够时提醒。收藏状态是 UI 偏好,不应影响配方规则。搜索和筛选也要围绕标签:恢复、武器、建筑、任务、活动。合成系统内容越多,信息架构越重要。

材料替代和品质继承

高级合成经常需要“任意木材”“任意同品质宝石”“三件同类装备”。这种替代材料不能用简单 itemId 列表糊弄。配方可以定义 ingredient group,由 InventoryResolver 找出可用候选,并按规则选择。玩家应能手动选择消耗哪种材料,尤其是稀有材料。默认自动选择时,优先消耗低品质、绑定、数量多的材料,避免误用稀有品。

品质继承也要提前设计。用高品质材料合成是否提高产物品质?失败是否返还部分材料?暴击合成是否产生额外产物?这些机制会显著影响经济。它们应该进入 CraftResult 的拆解里,UI 展示概率和影响因素。不要让玩家感觉合成结果像黑箱抽奖,除非这本来就是明确的随机玩法。

合成动画和真实进度

工作台动画可以很丰富:锤子敲击、炉火燃烧、进度条发光。但动画不能决定真实进度。真实进度来自 CraftJob 的开始和结束时间。玩家离开工作台、切场景、离线后回来,动画只是根据 job 状态恢复。若加速道具让任务立刻完成,动画可以快进或播放完成特效,但不能还等原来的 tween 结束才发奖。

队列完成时的通知也要适度。短时间完成多个任务,可以合并为一条“3 个物品制作完成”。如果玩家正在战斗,不要弹大窗口打断。可以在 HUD 或邮箱式通知里提示,回到安全场景后再展示详细产物。合成系统常和长期经营绑定,反馈要稳定但不吵。

任务物品和不可丢弃材料

任务物品参与合成时要格外谨慎。普通材料不足可以提示去采集,任务材料误用会导致任务卡死。配方应声明是否允许使用任务物品、绑定物品、锁定物品。InventoryResolver 默认排除任务锁定材料,除非配方明确要求。玩家手动选择材料时,也要在 UI 上用锁标识说明风险。

不可丢弃材料和限时材料也需要规则。活动材料过期后是否自动删除?用活动材料排队的合成任务在活动结束后是否继续?建议创建任务时保存材料快照,任务已经开始就按当时规则完成;尚未开始的队列可以在活动结束时取消并返还或转换。不要让活动边界悄悄吞材料。

合成失败和概率玩法

有些游戏有概率合成、强化失败、保底和保护符。若引入概率,必须比普通合成更透明。成功率、失败返还、保底计数、保护材料消耗顺序都要展示。随机结果要由可复现随机源或服务端决定,日志记录 roll 值和配置版本。玩家可以接受失败,但不能接受失败原因不明。

Phaser 表现上,概率合成可以有更强动画,但动画不能暗示结果已经决定前的假反馈。比如不要在服务端还没返回时先播放成功光效。可以播放中性制作动画,结果确认后再分支到成功或失败。经济状态永远先于表现。

继续阅读

探索更多技术文章

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

全部文章 返回首页