Phaser 自动化生产站:配方队列、搬运节拍和缺料反馈

面向工厂与生存建造玩法,拆解 Phaser 客户端中的自动化生产站、输入输出缓冲、配方队列、搬运节拍和 UI 调试。

为什么要把它当成系统来做

生存建造游戏的中期,玩家把矿石放进熔炉,传送带把铁锭送到装配台,装配台每 6 秒产出齿轮。看起来像一串动画,真正的核心是离散节拍、缓冲区和缺料原因。

自动化系统如果依赖 Tween 完成回调推进生产,离线收益、暂停、倍速、掉帧和存档都会出问题。需要把模拟时间和表现时间分开,让生产站按固定规则结算。 本文不把它写成一个一次性 Demo,而是按可上线、可维护的小系统拆开。重点不是堆 API,而是回答几个真实问题:数据从哪里来,谁有权修改状态,失败时玩家看到什么,调试时程序能看到什么,内容增加后系统还能不能承受。

核心架构

flowchart TD
  Ne59bbae5ae["固定 Tick"] --> N5374617469["StationModel"]
  N5374617469["StationModel"] --> Ne8be93e585["输入缓冲"]
  Ne8be93e585["输入缓冲"] --> N5265636970["RecipeQueue"]
  N5265636970["RecipeQueue"] --> Ne7949fe4ba["生产进度"]
  Ne7949fe4ba["生产进度"] --> Ne8be93e587["输出缓冲"]
  Ne8be93e587["输出缓冲"] --> Ne690ace8bf["搬运端口"]
  Ne690ace8bf["搬运端口"] --> N5068617365["Phaser 表现层"]

这张图的关键,是把 StationModel、RecipeQueue、InventoryBuffer、TickScheduler、TransportPort、StationView 放在单向流里。玩家输入或系统 tick 进入核心模型,模型产出结果,Phaser 再把结果转成动画、粒子、声音和界面。不要让显示对象反向决定规则。只要核心模型能在没有 Canvas 的环境中运行,就能写测试、做编辑器预览,也能在以后接入服务端校验或云存档。

生产站先是模型再是机器

每个生产站至少有输入缓冲、输出缓冲、当前配方、进度、状态和端口。Sprite 上的火焰、滚轮和机械臂只是状态的表现。暂停时模型不推进,动画也应停;倍速时模型按 tick 消化,动画可用时间缩放。不要让一段 6000ms 的 Tween 决定齿轮是否产出。

在实现这一点时,建议先写一组可以复现的输入数据,再接 Phaser 场景。先让控制台打印出正确状态,再让 Graphics、Sprite、Text 和 Tween 去表现它。这样的顺序不炫,但能减少非常多“画面看起来对,规则其实错”的问题。

配方队列要报告缺什么

玩家最烦的是机器停了却不知道为什么。配方系统应能返回 blockedReason:缺少铁锭、输出满、端口方向不对、电力不足或前置科技未解锁。UI 面板可以把原因显示成小图标,调试模式显示完整数据。缺料判断要发生在模型层,不能靠 UI 猜。

在实现这一点时,建议先写一组可以复现的输入数据,再接 Phaser 场景。先让控制台打印出正确状态,再让 Graphics、Sprite、Text 和 Tween 去表现它。这样的顺序不炫,但能减少非常多“画面看起来对,规则其实错”的问题。

输入输出缓冲要有容量

如果输入无限、输出无限,第一版很爽,后面经济会失控。每个缓冲区都应有容量和堆叠规则,搬运器每次只移动固定数量。输出满时生产站应停在 readyToOutput 或 blockedOutput,不要继续吞材料。这样玩家能通过布局和升级解决瓶颈,而不是被隐藏规则坑到。

在实现这一点时,建议先写一组可以复现的输入数据,再接 Phaser 场景。先让控制台打印出正确状态,再让 Graphics、Sprite、Text 和 Tween 去表现它。这样的顺序不炫,但能减少非常多“画面看起来对,规则其实错”的问题。

离散 tick 比真实时间更好测

自动化玩法需要暂停、快进、离线结算和回放,固定 tick 更可靠。可以每 100ms 推进一次模拟,每个生产站累积 delta,达到配方耗时后结算。帧率低时一次补多个 tick,但要限制单帧最大补偿,避免回到游戏后瞬间卡死。

在实现这一点时,建议先写一组可以复现的输入数据,再接 Phaser 场景。先让控制台打印出正确状态,再让 Graphics、Sprite、Text 和 Tween 去表现它。这样的顺序不炫,但能减少非常多“画面看起来对,规则其实错”的问题。

搬运节拍要可视化

传送带、机械臂、无人车和管道都在争夺端口。调试层应显示端口方向、下一次搬运时间、目标缓冲和失败原因。很多自动化 bug 不是生产站错误,而是搬运器把物品塞错端口或在同一帧被两个消费者抢走。

在实现这一点时,建议先写一组可以复现的输入数据,再接 Phaser 场景。先让控制台打印出正确状态,再让 Graphics、Sprite、Text 和 Tween 去表现它。这样的顺序不炫,但能减少非常多“画面看起来对,规则其实错”的问题。

存档只存模型状态

存档保存站点 id、配方、缓冲物品、进度、端口配置和最后结算时间。不要保存动画帧或 Tween 状态。读档后根据模型状态重建 Phaser 对象,进度条跳到对应比例,粒子按状态开关。这样版本迁移也能操作数据,而不是处理一堆显示对象。

在实现这一点时,建议先写一组可以复现的输入数据,再接 Phaser 场景。先让控制台打印出正确状态,再让 Graphics、Sprite、Text 和 Tween 去表现它。这样的顺序不炫,但能减少非常多“画面看起来对,规则其实错”的问题。

性能优化从脏标记开始

上百台机器不需要每帧刷新 UI。模型 tick 后若状态变化,标记 stationDirty;视图只更新脏站点的进度条、缺料图标和端口颜色。大多数站点处于稳定生产状态,只需要按较低频率更新进度显示。这个优化简单,却能明显降低移动端开销。

在实现这一点时,建议先写一组可以复现的输入数据,再接 Phaser 场景。先让控制台打印出正确状态,再让 Graphics、Sprite、Text 和 Tween 去表现它。这样的顺序不炫,但能减少非常多“画面看起来对,规则其实错”的问题。

TypeScript 实现骨架

interface Stack { item: string; count: number }
interface Recipe { id: string; ms: number; input: Stack[]; output: Stack[] }
type StationState = "idle" | "working" | "blockedInput" | "blockedOutput";
class StationModel {
  progress = 0;
  state: StationState = "idle";
  input = new Map<string, number>();
  output = new Map<string, number>();
  constructor(public recipe: Recipe, private outputCap = 20) {}
  tick(dt: number) {
    if (!this.hasInput()) { this.state = "blockedInput"; return; }
    if (!this.hasOutputSpace()) { this.state = "blockedOutput"; return; }
    this.state = "working";
    this.progress += dt;
    if (this.progress >= this.recipe.ms) {
      this.progress -= this.recipe.ms;
      for (const s of this.recipe.input) this.input.set(s.item, (this.input.get(s.item) ?? 0) - s.count);
      for (const s of this.recipe.output) this.output.set(s.item, (this.output.get(s.item) ?? 0) + s.count);
    }
  }
  private hasInput() { return this.recipe.input.every(s => (this.input.get(s.item) ?? 0) >= s.count); }
  private hasOutputSpace() { return this.recipe.output.every(s => (this.output.get(s.item) ?? 0) + s.count <= this.outputCap); }
}

这段代码只是骨架,真正项目里还要加事件派发、错误码、配置校验和日志。但骨架已经表达了方向:核心概念是普通 TypeScript 对象,Phaser 类型只出现在输入适配或表现需要的地方。若你发现某个函数越来越依赖 Scene、Camera 或 Sprite,就应该停下来判断它是不是被放错层了。

落地步骤

  1. 第一,确认 StationModel 的输入输出是否是纯数据。若需要 Phaser.GameObjects 才能计算结果,说明边界还没有切开。
  2. 第二,给 RecipeQueue 或同等级的核心概念写三个最小样例:正常路径、边界路径、失败路径。样例要能在没有浏览器画面的情况下运行。
  3. 第三,把 UI 上每个可点击动作都映射成明确意图,不要让按钮直接修改深层状态。意图里带 requestId,便于防重复和追踪。
  4. 第四,失败反馈要比成功反馈更早接入。成功时玩家通常愿意接受,失败时才会质疑系统是否可靠。
  5. 第五,内容配置要有默认值和校验脚本。字段缺失时宁可在启动时报错,也不要在玩家操作到一半才静默失败。
  6. 第六,性能指标要提前量化:每帧最多处理多少对象、单次刷新允许多少毫秒、低端机是否需要降级显示。

常见坑

  • 最容易踩的坑,是让表现层过早成为事实来源。比如动画播完才算成功、按钮亮着就代表可用、某个 Sprite 存在就说明状态存在。这些判断在演示机上没问题,一到跳过、暂停、断线、切场景和重连就会变成隐性故障。
  • 第二个坑是只为第一关写逻辑。第一关对象少、路径短、输入慢,任何写法都像是正确的。等内容增加到几十张地图、几百个配置和各种活动修正时,临时判断会互相覆盖。写系统时要假设它会被复用、被误用、被配置错。
  • 第三个坑是没有留下证据。玩家反馈“刚才没生效”时,如果没有事件日志、状态快照或 requestId,只能靠猜。哪怕是单机项目,也可以保留最近 50 条关键事件,开发包里导出文本,定位速度会快很多。

项目里的验证方式

把这套系统放进 Phaser 项目时,我会先建一个不依赖 Scene 的核心目录,例如 src/gameplay/crafting-station-automation。里面只放模型、求解器、状态机和测试夹具。Scene 只负责输入适配、对象池、摄像机、音效和 UI。这个边界看似多写几行代码,但它换来的是可测试、可回放和可迁移。等项目进入内容生产阶段,最值钱的不是某个特效多漂亮,而是当策划说某个状态不对时,程序能在五分钟内复现并解释。

数据格式要尽量像内容团队会填写的表,而不是像程序临时拼出来的对象。每个 id 都要稳定,每个状态都要能序列化,每个失败原因都要有明确枚举。Phaser 的优势是快速把反馈做出来,但反馈越快,越容易掩盖规则层的混乱。先把规则层写清楚,再接动画,后续加新模式、新活动或新平台才不会反复拆墙。

在调试阶段,建议给这个系统加一个小面板:显示当前输入、核心状态、最近事件、最后一次失败原因和关键耗时。面板不需要好看,但要准确。很多客户端问题在日志里只是一句 undefined,在调试面板里却能看到完整链路。尤其是多人、存档、复杂 UI 或长时间运行的玩法,调试可见性直接决定维护成本。

最后检查

做完第一版后,不要只看一次演示是否顺滑。至少准备三组数据:一组正常流程,一组边界流程,一组故意配置错误的流程。正常流程证明体验成立,边界流程证明状态不会漂移,错误流程证明系统会给出可理解的失败原因。自动化生产站:配方队列、搬运节拍和缺料反馈 的质量不取决于第一眼多热闹,而取决于玩家反复操作、内容不断扩张、版本持续迭代时,它还能保持清楚、稳定和可解释。

继续阅读

探索更多技术文章

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

全部文章 返回首页