为什么这个玩法不能只写成演示
玩家在自动化仓库里推动电池箱,把它们送到充电底座。某些箱子很重,只能推不能拉;某些地板是传送带,会在回合结束后移动箱子;还有压力板控制门。规则不复杂,但组合后很容易失控。
推箱关卡必须可撤销、可重放、可验证。若箱子移动直接写在 Sprite tween 完成回调里,撤销和死锁检测都会变得困难。规则层应先计算一回合的所有变化,表现层再播放动画。 本文按一个可上线的小系统拆解,重点不是罗列 Phaser API,而是把输入、规则、表现、调试和内容配置的边界说明白。只要这些边界清楚,后续加关卡、加活动、加存档或加移动端适配,都不会反复推倒。
核心架构
flowchart TD
N1["PushIntent"] --> N2["MoveResolver"]
N3["GridState"] --> N2["MoveResolver"]
N2["MoveResolver"] --> N4["MechanismSystem"]
N4["MechanismSystem"] --> N3["GridState"]
N3["GridState"] --> N5["UndoStack"]
N3["GridState"] --> N6["DeadlockChecker"]
N7["AnimationQueue"] --> N8["Phaser 表现层"]
这套结构的关键是让 GridState、PushIntent、MoveResolver、MechanismSystem、UndoStack、DeadlockChecker、AnimationQueue 各司其职。输入层提交意图,规则层产出确定结果,Phaser 层负责把结果演出来。不要让 Tween 完成回调、Sprite 是否可见或某个音效是否播放成为规则事实。规则事实必须能被序列化、测试和回放。
格子状态是唯一真相
GridState 保存墙、地板、目标点、箱子、玩家位置和机关状态。Sprite 只表现 GridState。每次移动前复制或记录 patch,移动后得到新状态。这样撤销、重放、关卡验证和存档都能共享同一套数据。
实现时建议先用调试图形把这部分规则跑通,再接正式美术。比如先画命中范围、路径、候选区域、分数来源或状态机阶段,确认数据没有问题后,再加入粒子、音效、镜头和 UI 动效。这样做看似慢,实际会减少大量返工。
推动意图要先校验
PushIntent 包含方向和玩家位置。MoveResolver 判断目标格、箱子后方格、箱子重量和特殊地板。若不能推动,返回失败原因。失败也应该进入反馈,比如播放轻微撞击,而不是无声忽略输入。
实现时建议先用调试图形把这部分规则跑通,再接正式美术。比如先画命中范围、路径、候选区域、分数来源或状态机阶段,确认数据没有问题后,再加入粒子、音效、镜头和 UI 动效。这样做看似慢,实际会减少大量返工。
机关在回合末结算
压力板、传送带、门、升降台应在玩家推动后统一结算。MechanismSystem 可以循环处理,直到状态稳定或达到最大迭代次数。这样传送带推动箱子压到压力板再开门的链式反应能被完整记录。
实现时建议先用调试图形把这部分规则跑通,再接正式美术。比如先画命中范围、路径、候选区域、分数来源或状态机阶段,确认数据没有问题后,再加入粒子、音效、镜头和 UI 动效。这样做看似慢,实际会减少大量返工。
撤销保存补丁
UndoStack 不必保存整张地图,也可以保存移动前后的差异:玩家位置、箱子位置、机关变化。关卡小的时候保存全状态更简单,关卡大时 patch 更省。重点是撤销后动画和规则状态要一致。
实现时建议先用调试图形把这部分规则跑通,再接正式美术。比如先画命中范围、路径、候选区域、分数来源或状态机阶段,确认数据没有问题后,再加入粒子、音效、镜头和 UI 动效。这样做看似慢,实际会减少大量返工。
死锁检测帮助设计
箱子被推到角落且不是目标点,通常就是死锁。更复杂的死锁包括两个箱子并排卡墙、目标点不可达等。DeadlockChecker 不一定要在正式版阻止玩家,但开发工具和提示系统可以用它判断关卡是否已经无解。
实现时建议先用调试图形把这部分规则跑通,再接正式美术。比如先画命中范围、路径、候选区域、分数来源或状态机阶段,确认数据没有问题后,再加入粒子、音效、镜头和 UI 动效。这样做看似慢,实际会减少大量返工。
动画队列不能改规则
规则层一帧内已经算完所有变化,AnimationQueue 按顺序播放玩家移动、箱子滑动、机关触发。玩家输入在动画期间可以缓存或锁定,但不能让动画中途再次修改 GridState。
实现时建议先用调试图形把这部分规则跑通,再接正式美术。比如先画命中范围、路径、候选区域、分数来源或状态机阶段,确认数据没有问题后,再加入粒子、音效、镜头和 UI 动效。这样做看似慢,实际会减少大量返工。
关卡验证要自动跑
发布前用求解器或半自动脚本检查目标数量、箱子数量、初始死锁、机关循环和最少步数范围。推箱关卡内容多,靠人工试玩很容易漏掉某个无解配置。
实现时建议先用调试图形把这部分规则跑通,再接正式美术。比如先画命中范围、路径、候选区域、分数来源或状态机阶段,确认数据没有问题后,再加入粒子、音效、镜头和 UI 动效。这样做看似慢,实际会减少大量返工。
TypeScript 实现骨架
interface Pos { x: number; y: number }
interface Box { id: string; pos: Pos; heavy?: boolean }
interface GridState { player: Pos; boxes: Box[]; walls: Set<string>; goals: Set<string> }
const key = (p: Pos) => `${p.x},${p.y}`;
function add(p: Pos, d: Pos): Pos { return { x: p.x + d.x, y: p.y + d.y }; }
function resolvePush(state: GridState, dir: Pos) {
const next = add(state.player, dir);
if (state.walls.has(key(next))) return { ok: false, reason: "wall" };
const box = state.boxes.find(b => key(b.pos) === key(next));
if (!box) return { ok: true, player: next };
const boxNext = add(box.pos, dir);
if (state.walls.has(key(boxNext)) || state.boxes.some(b => key(b.pos) === key(boxNext))) return { ok: false, reason: "blocked-box" };
return { ok: true, player: next, boxId: box.id, boxPos: boxNext };
}
这段代码只展示核心边界。真实项目里还需要配置加载、错误码、事件总线、对象池、存档字段和测试夹具。原则是核心系统不依赖 Scene,Scene 只把玩家输入和系统结果连接到 Phaser 的显示对象。
落地步骤
- 第一步,先把 GridState 和 PushIntent 写成纯数据模型,准备两三个最小样例。
- 第二步,给 MoveResolver 增加调试可视化,确保中间状态能被看见。
- 第三步,把 Phaser 动画接到规则结果上,而不是让动画反过来提交规则。
- 第四步,补齐失败原因、暂停恢复、重复点击保护和读档恢复。
- 第五步,用正常流程、边界流程、错误配置三类夹具做校验。
常见坑
- 把画面当作状态来源。显示对象可能被对象池回收、被镜头隐藏或被动画临时改值,不能作为规则真相。
- 只为第一关写逻辑。第一关对象少、节奏慢,很多问题不会暴露;内容扩张后,重复触发和配置错误会一起出现。
- 失败反馈太笼统。玩家需要知道是条件不满足、资源不足、路径不可达、输入太晚,还是系统正在等待确认。
- 调试面板缺失。复杂玩法没有中间状态可视化,后期只能靠录屏和猜测定位。
运行时观测
记录每关撤销次数、失败推动原因、触发死锁后的继续时长和提示使用率。若某关撤销次数极高,可能不是难,而是机关反馈不清楚。 这些指标不一定都要上报到线上,但至少应该在开发版能导出。玩法系统越依赖手感和解释,越需要用数据区分规则问题、表现问题和关卡配置问题。
边界测试与移动端验证
仓库推箱关卡 在桌面浏览器里跑通,只能说明主流程成立,还不能说明它适合发布。建议为它准备一组专门的边界测试:箱子被推到角落、传送带连锁推动、压力板和门同帧变化、撤销机关状态、动画未播完时玩家继续输入。这些测试不用都做成复杂自动化,至少要有可重复的调试入口,让开发、策划和 QA 能在同一状态下观察同一个问题。每次测试都要记录 GridState、UndoStack、MechanismSystem 和 AnimationQueue,否则失败只会变成“刚才好像不对”。如果玩法会出现在移动端,还要额外检查触控误差、浏览器切后台、低电量降频、横竖屏切换和音频恢复。很多 Phaser 小游戏不是输在核心规则,而是输在这些边界恢复上。
移动端验证还要关注触摸反馈和文字密度。按钮按下后要有立即响应,即使规则结果需要等待;长文本提示要能在窄屏换行;关键数值不能只靠颜色表达。若系统在低端机关闭粒子、阴影或轨迹后仍能保持同样的规则结果,就说明表现层和规则层边界清楚。发布前把这些用例整理成清单,后续每次改配置、换美术或加活动,都可以快速回归。
发布前检查
发布前至少确认四件事:第一,所有配置引用的 id 都存在;第二,核心状态能存档并恢复;第三,快速输入和跳过动画不会重复结算;第四,低端机可以关闭高成本表现但不改变规则。若系统涉及奖励、货币或排行榜,还要确认事件 id 幂等,避免重复发放或重复扣除。
机关组合的可读性
推箱变体常见问题是机关太聪明,玩家看不懂。压力板开门、传送带移动、激光锁定和升降台变化都可以存在,但每个机关都要有清楚的输入和输出。玩家推动箱子后,先看到压力板被压下,再看到门打开,顺序要稳定。若多个机关同帧触发,AnimationQueue 可以按因果分组播放,而不是所有动画同时爆发。这样玩家能学习规则,不会把关卡当成黑盒。
关卡作者的死锁提示
DeadlockChecker 不一定要直接告诉玩家答案,但应该服务关卡作者。编辑器里可以在箱子进入永久死锁时把格子染红,并显示原因:角落死锁、双箱贴墙、目标不可达。作者能马上调整墙体或目标点。正式版则可以在玩家连续撤销多次后给轻量提示,例如“这个箱子似乎没有回头路”。
推箱关卡还要给输入节奏留余地。玩家常会连续按方向键,如果动画期间完全丢弃输入,手感会迟钝;如果全部接受,又可能破坏回合顺序。可以只缓存最后一个方向,动画结束后重新校验再执行。
验收标准
这个系统的第一版不需要覆盖所有商业化变化,但至少要能回答三类问题:玩家为什么成功,玩家为什么失败,内容配置为什么非法。验收时可以让一名没有参与开发的人按测试清单操作,如果他能从画面反馈和调试面板里解释当前状态,就说明系统边界基本成立。若每次都需要开发者口头说明,说明 UI、日志或规则命名还不够清楚。
结语
仓库推箱变体:箱子状态、撤销、机关联动和关卡验证 的价值在于可解释。Phaser 可以把反馈做得很快,但真正决定项目能不能持续扩展的,是规则层是否稳定、表现层是否服从结果、调试层是否能讲清楚每一次失败。把这些边界立住,玩法才能从一个好看的 Demo 变成可维护的系统。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。