Phaser 铁路调度解谜:轨道岔路、信号灯和碰撞预测

讲解铁路调度类解谜玩法的轨道图、列车时刻、岔路切换、信号灯、碰撞预测、回放和关卡验证。

为什么值得单独做成系统

调度解谜游戏里,三列火车从不同入口进入小站。玩家要切换岔路、控制信号灯,让货车进仓库,客车停站,维修车避开主线。火车速度不快,但每一次误判都会造成连锁堵塞。

铁路调度的核心是图和时间。画面上的轨道只是表现,规则层需要知道节点、边、岔路、占用、信号和未来几秒的预测。没有预测,玩家只能靠试错;没有验证,关卡很容易不可解。 本文会按一个可上线的小系统来拆,不追求炫技,而是把数据结构、状态流、玩家反馈、调试工具和发布检查说清楚。Phaser 的优势是让画面和交互快速成型,但越是快速,越需要把规则层和表现层分开。

核心架构

flowchart TD
  N1["TrackGraph"] --> N2["TrainSchedule"]
  N3["SwitchState"] --> N1["TrackGraph"]
  N4["SignalState"] --> N2["TrainSchedule"]
  N2["TrainSchedule"] --> N5["OccupancyMap"]
  N5["OccupancyMap"] --> N6["CollisionPredictor"]
  N6["CollisionPredictor"] --> N7["WarningUI"]
  N2["TrainSchedule"] --> N8["ReplayTimeline"]

这张图的重点是单向流动。TrackGraph、SwitchState、SignalState、TrainSchedule、OccupancyMap、CollisionPredictor、ReplayTimeline 不应该互相随意读写。输入或场景事件进入模型,模型输出快照或事件,Phaser 表现层再根据结果更新 Sprite、Graphics、Sound 和 UI。只要这条边界稳定,后续加内容、加难度、加存档或加多人同步,都不会把系统推倒重写。

轨道先是图结构

每段轨道是边,车站、岔路、入口和出口是节点。列车沿边移动,遇到岔路时根据 SwitchState 选择下一条边。不要用 Sprite 路径点直接决定规则,否则换皮肤或调整曲线时会破坏调度逻辑。Phaser 的曲线只负责让列车沿轨道好看地移动。

实现时建议先用最简单的调试图形验证规则,再接正式美术。比如先画出区域、方向、时间轴、占用格或检测范围,确认数据正确后再添加粒子、镜头、音效和过渡动画。这样做不花哨,但能避免很多“看起来对,规则其实错”的问题。

岔路切换要有时机

玩家点击岔路后,不应瞬间改变正在通过列车脚下的轨道。SwitchState 可以有 locked 状态:列车占用岔路区域时禁止切换,或者切换请求排队到空闲后执行。UI 要显示锁定原因,否则玩家会以为点击失效。

实现时建议先用最简单的调试图形验证规则,再接正式美术。比如先画出区域、方向、时间轴、占用格或检测范围,确认数据正确后再添加粒子、镜头、音效和过渡动画。这样做不花哨,但能避免很多“看起来对,规则其实错”的问题。

信号灯控制进入许可

信号灯不是装饰,它决定列车是否能进入下一段 block。红灯让列车在停止点减速停下,绿灯允许通过。SignalState 应参与 TrainSchedule,而不是只在画面上换颜色。这样碰撞预测也能知道列车会不会停。

实现时建议先用最简单的调试图形验证规则,再接正式美术。比如先画出区域、方向、时间轴、占用格或检测范围,确认数据正确后再添加粒子、镜头、音效和过渡动画。这样做不花哨,但能避免很多“看起来对,规则其实错”的问题。

占用图是碰撞预测基础

OccupancyMap 记录未来时间窗口内每列车会占用哪些 block。CollisionPredictor 检查同一时刻两个列车是否占用同一 block,或是否在单线区相向而行。预测窗口可以是 10 到 20 秒,足够给玩家提示。

实现时建议先用最简单的调试图形验证规则,再接正式美术。比如先画出区域、方向、时间轴、占用格或检测范围,确认数据正确后再添加粒子、镜头、音效和过渡动画。这样做不花哨,但能避免很多“看起来对,规则其实错”的问题。

预警要可操作

如果系统只在即将撞车时尖叫,玩家来不及修正。预警 UI 应显示冲突发生位置和预计时间,并允许玩家暂停或慢速思考。简单关卡可以自动暂停,高级关卡只给信号提示。难度来自调度,不应来自信息不足。

实现时建议先用最简单的调试图形验证规则,再接正式美术。比如先画出区域、方向、时间轴、占用格或检测范围,确认数据正确后再添加粒子、镜头、音效和过渡动画。这样做不花哨,但能避免很多“看起来对,规则其实错”的问题。

关卡目标要数据化

每列车有目标:到达某站、在某站停留、按顺序出站、不能延误超过多少秒。目标数据化后,关卡验证器可以跑自动模拟,检查是否存在基本可行解,也能在玩家完成时统一结算。

实现时建议先用最简单的调试图形验证规则,再接正式美术。比如先画出区域、方向、时间轴、占用格或检测范围,确认数据正确后再添加粒子、镜头、音效和过渡动画。这样做不花哨,但能避免很多“看起来对,规则其实错”的问题。

回放帮助学习

失败后给一个短回放,显示哪次岔路或信号导致冲突。ReplayTimeline 保存玩家操作和列车状态,不必录制视频。玩家能看到自己早切还是晚切,下一次更容易改进。

实现时建议先用最简单的调试图形验证规则,再接正式美术。比如先画出区域、方向、时间轴、占用格或检测范围,确认数据正确后再添加粒子、镜头、音效和过渡动画。这样做不花哨,但能避免很多“看起来对,规则其实错”的问题。

TypeScript 实现骨架

interface TrackEdge { id: string; from: string; to: string; length: number; block: string }
interface Train { id: string; edgeId: string; offset: number; speed: number; stopped: boolean }
function advanceTrain(train: Train, edge: TrackEdge, dt: number) {
  if (train.stopped) return;
  train.offset += train.speed * dt / 1000;
  if (train.offset > edge.length) train.offset = edge.length;
}
function predictBlock(train: Train, edges: Map<string, TrackEdge>, seconds: number) {
  const edge = edges.get(train.edgeId)!;
  const future = train.offset + train.speed * seconds;
  return { trainId: train.id, block: edge.block, atEnd: future >= edge.length };
}
function findConflicts(predictions: Array<{ trainId: string; block: string }>) {
  const seen = new Map<string, string>();
  const conflicts: string[] = [];
  for (const p of predictions) {
    const other = seen.get(p.block);
    if (other && other !== p.trainId) conflicts.push(`${other}:${p.trainId}:${p.block}`);
    seen.set(p.block, p.trainId);
  }
  return conflicts;
}

这段代码只展示核心边界,不是完整项目代码。真实项目里还需要补配置加载、错误码、事件派发、对象池、性能采样和测试。关键是让核心规则能独立运行,Phaser 层只是把规则结果变成玩家能感知的反馈。

落地步骤

  1. 第一,先把 TrackGraph 和 SwitchState 写成普通 TypeScript 模型。不要让它们依赖 Phaser Scene、Sprite 或 Camera。核心模型越普通,越容易写测试、做编辑器预览和复现玩家问题。
  2. 第二,Phaser 层只做适配:接收输入、播放动画、更新图形、触发音效。它可以很薄,但必须清楚。只要某段规则开始读取 Sprite 的 visible、alpha 或动画状态,就说明边界正在变脏。
  3. 第三,给 SignalState 或同等复杂的中间结果做调试显示。开发模式里能看到状态、阈值、候选对象、失败原因和耗时,后续调参才不会靠猜。
  4. 第四,准备三组测试夹具:正常流程、边界流程、错误配置。正常流程验证体验,边界流程验证稳定性,错误配置验证系统会报出人能看懂的问题。

检查清单

  • 确认 TrackGraph 的状态可以序列化,能写入存档或调试日志。
  • 确认 SwitchState 的配置有默认值、版本号和校验错误。
  • 确认快速点击、暂停、切后台、读档和切场景不会重复提交关键事件。
  • 确认失败反馈足够具体,玩家能知道是条件不足、输入中断、资源不够还是规则禁止。
  • 确认低端机有降级策略,尤其是粒子、音效、动态对象和调试图层。
  • 确认开发模式可以导出最近关键事件,方便复现玩家反馈。

常见误区

第一类误区,是把表现当成事实。动画播完、按钮亮着、Sprite 存在,都只能说明表现层当前长什么样,不能说明规则已经完成。规则事实应该存在于模型和事件里。

第二类误区,是只为了第一个关卡写逻辑。第一个关卡对象少、输入慢、节奏简单,临时判断很难暴露问题。等内容增加,重复触发、配置错误和性能峰值会一起出现,早期的边界会决定后期成本。

第三类误区,是没有设计失败路径。复杂系统一定会遇到失败,好的失败路径会告诉玩家和开发者发生了什么;坏的失败路径只会留下一句操作失败,甚至什么都不显示。

发布前验证

发布前至少跑一次规则级测试和一次运行时冒烟。规则级测试不需要启动浏览器,直接喂数据,断言状态和事件。运行时冒烟则在 Phaser 场景里验证输入、反馈、暂停、重开和边界情况。若系统涉及经济、存档或排行榜,还要记录 requestId 或事件 id,保证重复提交不会造成重复奖励或重复扣费。

额外实践建议

  • 轨道图和美术曲线要分开。
  • 碰撞预测要能显示未来冲突,不只处理已经发生的事故。
  • 玩家操作要进入回放时间线,失败复盘才有价值。

运行时观测与调参

铁路调度关卡要记录玩家操作时间线:第几秒切换了哪个岔路,哪个信号灯被改成红灯,哪列车因此等待,最终冲突是否发生。只记录成功或失败不够,因为调度解谜的学习来自过程。若大量玩家在同一个岔路频繁来回切,可能是轨道走向不清;若玩家总是在预警后仍来不及处理,可能是预测窗口太短或 UI 没有指向冲突位置。关卡验证器也可以跑若干随机操作,确保错误状态能被预测系统提前捕捉,而不是等列车重叠后才算失败。

对于高难关卡,建议记录第一次冲突发生前玩家是否看到预警。如果预警出现但被忽略,可能是玩家决策问题;如果预警根本没有进入视野,就是 UI 责任。

结语

铁路调度解谜:轨道岔路、信号灯和碰撞预测 的关键不是某个 API,而是把可解释的规则交给模型,把可感知的反馈交给 Phaser。只要这条线清楚,项目就能持续扩展;如果所有逻辑都塞进 Scene 回调,第一版越快,后面的维护压力越大。

继续阅读

探索更多技术文章

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

全部文章 返回首页