用 Phaser 4 造一个空间解谜游戏:纯函数棋盘引擎、透明画布分层与轻量双语

在线体验:zhimalab.tech/games/redbl... 一个纯前端实现的空间推理解谜游戏------「赤金秘径 Gilded Secret Path」。玩家操控一块有双面能力的赤金方块,在悬空浮岛上规划路线、踩踏机关、收集钥匙,最终抵达金色祭坛。没有后端,没有 React,一个静态 HTML + 一段 TypeScript + Phaser 4 就完成了棋盘引擎、双语界面、存档和结算动画。

最近给儿童启蒙站点(芝麻园地)做空间思维训练的小游戏。对标 Math Playground 的 Red Block Returns 系列:操控方块一格一格走,绕过虚空、踩机关、到祭坛。这个品类核心乐趣是**「想清楚再走」**------高思考密度、低操作频率,特别适合做给孩子。

为了追求极致的加载速度和可维护性,这一版我做了一个比较"克制"的技术选型:不引 React,不用状态库。整页是静态 HTML,Phaser 负责渲染,棋盘逻辑抽成一个纯函数引擎,UI 用一个极简的发布订阅 store 驱动。下面把关键设计拆开讲讲。

一、架构总览:静态骨架 + Phaser 渲染,零框架依赖

页面是一个 .astro 静态页面:主菜单、选关、HUD、结算层全部是写死的 HTML 骨架,游戏脚本在 DOMContentLoaded 后创建 Phaser 实例:

ts 复制代码
const config: Phaser.Types.Core.GameConfig = {
  type: Phaser.AUTO,
  parent: 'game-container',
  scale: {
    mode: Phaser.Scale.FIT,
    autoCenter: Phaser.Scale.CENTER_BOTH,
  },
  render: { transparent: true, antialias: true },
  input: { activePointers: 2 },
  scene: [BootScene, BoardScene],
};

三个关键点:

  • transparent: true:Phaser 画布是透明的,星空、星云、浮岛这些氛围背景全部用页面 CSS 画在画布下面。棋盘像浮在夜空中,视觉效果丰富,却一点不占 Phaser 的渲染开销。
  • activePointers: 2:多点触控支持,孩子和家长可以同时操作。
  • 没有任何 UI 库 :菜单/选关/结算都是 DOM,用 classList.toggle('hidden') 控制显示。好处是状态切换肉眼可见、好调试,E2E 也好写。

二、核心:纯函数棋盘引擎

棋盘逻辑我特意写成与 Phaser 完全解耦的纯函数模块 ,方便单测和回溯。createState(level) 构建初始状态,move(state, dir) 返回新状态,computeStars(state) 算星级:

ts 复制代码
export function move(state: EngineState, dir: Dir): MoveOutcome {
  if (state.status !== 'playing') return { ok: false, reason: 'already-over' };
  const { dx, dy } = DIR_DELTA[dir];
  const tx = state.blockX + dx, ty = state.blockY + dy;
  const target = cellAt(state, tx, ty);

  // 锁门未开:挡住,不算步数
  if (target && target.type === 'door' && !target.doorOpen) {
    return { ok: false, reason: 'blocked' };
  }
  // 虚空 / 出界 → 死亡
  if (!isWalkable(target)) {
    return { ok: true, next: { ...state, status: 'dead', deaths: state.deaths + 1, steps: state.steps + 1, deathReason: 'fell' } };
  }
  // 浅拷贝 grid,只改被影响的格子
  const grid = state.grid.map((row) => [...row]);
  const next = { ...state, grid, blockX: tx, blockY: ty, steps: state.steps + 1 };
  // ... 落地处理:开关激活门 / 拾钥匙 / 塌陷砖离开即塌 / 到祭坛胜利
}

设计要点:

  • 状态不可变 :每次移动返回全新状态,天然支持撤销------把旧状态压进历史栈,undo() 出栈即可。
  • 撤销限次 :每关 5 次(UNDO_LIMIT = 5),给容错但不至于无脑试。
  • 塌陷砖不可逆 :踩上去能站,离开后立即 crumbled = true,变回虚空,不许回头------"每一步都算数"的紧张感就来自这里。

星级评价沿用经典三条件:

ts 复制代码
export function computeStars(state: EngineState): StarRating {
  if (state.status !== 'won') return 0;
  let stars: StarRating = 1;
  if (state.steps <= state.level.par_steps) stars = 2;
  if (state.level.star_conditions.no_death && state.deaths === 0) stars = 3;
  return stars;
}

三、提示系统:BFS 寻路,只告诉你"下一步"

怕孩子卡关,做了提示功能。但提示不直接帮走,而是从当前位置 BFS 寻路到祭坛,返回"下一步"该踩的格子------保留思考空间:

ts 复制代码
export function nextHintStep(state: EngineState): { x: number; y: number } | null {
  const queue: Node[] = [{ x: state.blockX, y: state.blockY }];
  visited.add(`${state.blockX},${state.blockY}`);
  while (queue.length > 0) {
    const cur = queue.shift()!;
    for (const d of Object.values(DIR_DELTA)) {
      const nx = cur.x + d.dx, ny = cur.y + d.dy;
      // 虚空不可通过,门视为可通过(避免把玩家锁死在视野内)
      const passable = cell.type !== 'void' && !(cell.type === 'crumble' && cell.crumbled);
      if (!passable) continue;
      const first = cur.first ?? { x: nx, y: ny };
      if (cell.type === 'altar') return first;
      queue.push({ x: nx, y: ny, first });
    }
  }
  return null;
}

每关提示只能用一次(hintUsed 标记),用完即止,逼玩家自己动脑。

四、UI 与引擎之间:极简状态总线

不用 zustand,也不用 React。一个十几行的类搞定:

ts 复制代码
class RedBlockStore {
  private state: RedBlockState = initialState();
  private listeners = new Set<Listener>();
  set(partial: Partial<RedBlockState>): void {
    this.state = { ...this.state, ...partial };
    this.listeners.forEach((fn) => fn(this.state, prev));
  }
  subscribe(fn: Listener): () => void { /* ... */ }
}

UI 写 store.set(...),引擎读订阅。好处是无依赖、无 SSR 水合顾虑,还顺手暴露给 window.__RBC_STORE__ 给 E2E 测试读状态。

五、轻量双语:data-i18n + 一个字典

这个游戏面向中英双语(还能当英语启蒙用),但我不想为一个字典功能上 i18n 框架。方案是:静态文本用 data-i18n 属性,动态文本用 t()/tpl() 函数

ts 复制代码
zh.menuHintDesc = '操控双面赤金方块穿越浮空岛链,踩踏机关、收集钥匙,抵达金色祭坛。';
en.menuHintDesc = 'Guide the two-sided gilded block across floating islands --- step on switches, collect keys, and reach the golden altar.';

function applyStaticI18n(root: ParentNode = document): void {
  root.querySelectorAll<HTMLElement>('[data-i18n]').forEach((el) => {
    const key = el.dataset.i18n as MessageKey | undefined;
    if (key && messages[currentLang][key]) el.textContent = t(key);
  });
  syncDocument(); // 同步 <html lang> / document.title / meta
}

语言选择持久化到 localStorage('redblock.lang'),默认跟随浏览器语言,页面右上角有中 / EN 一键切换。连 document.titlemeta description 都会跟着换------对 SEO 和分享卡片友好。

六、进度存档与结算反馈

  • 进度 :每关最佳星级存 localStorage('redblock.bestStars'),选关页实时显示星数、解锁下一关。
  • 结算动画 :通关后金色粒子飘落、皇冠弹出、三星逐个点亮,再给"步数 / 死亡"统计。死亡则慢下来,用文案告诉你**"哪里走错了"**------fell(掉虚空)、crumble(塌陷砖撑不住)、exhausted(步数耗尽),失败也变成学习信号。

七、移动端:触控方向键 + 手势

PC 用方向键 / WASD,Z 撤销、R 重置。移动端专门加了屏幕方向键(只在 pointer: coarse 的小屏显示),避免误触;同时 prefers-reduced-motion: reduce 时关掉所有动画,照顾动效敏感的孩子。

写在最后

整个项目仍是纯静态:一个 HTML + 十几个 TypeScript 文件 + Phaser 4,没有后端、没有 UI 框架。核心思路是**"逻辑与渲染彻底分离"**------棋盘引擎是纯函数、UI 是静态 DOM、Phaser 只负责把状态画出来。这套组合对做"交互式小游戏/互动教程"这类内容特别顺手,也特别好测。

在线体验(免费,浏览器打开即玩):

目前上线的是第一章「觉醒之石」3 个教学关(纯移动 → 绕虚空 → 塌陷砖),第二章「冰封回廊」的冰面滑行机制已在路上。如果你也在做网页解谜或给孩子的互动内容,欢迎去玩两关提提意见------尤其是"提示要不要再给多一点"这种手感问题,得让真玩家敲键盘说了算。😄

相关推荐
可视之道1 小时前
前端图形编辑器的多选框架:从单选到框选到 Shift 连选
前端
3秒一个大1 小时前
JS 数组去重全方案:从基础到通用万能去重函数
前端·javascript
阿黎梨梨1 小时前
读懂一个 Next.js 全栈笔记应用
前端
古茗前端团队2 小时前
代码越改越乱?来试试前端领域模型驱动设计(DDD)
前端·agent·ai编程
Hilaku2 小时前
为什么跳槽涨薪 30% 的时代彻底结束了?聊聊 2026 前端薪资的天花板
前端·javascript·程序员
愚公搬代码2 小时前
【愚公系列】《Web应用安全》006-HackBar插件的使用
前端·安全
超爱吃香菜的菜鸟2 小时前
关于pnpm 部分使用(一)
前端·vue.js
Vuji2 小时前
Pi 插件解剖|git-checkpoint.ts:只用 53 行,让 fork 恢复代码状态
前端·agent
大家的林语冰3 小时前
✌️ 让 Rust 再次伟大,pnpm 12 抛弃 TypeScript,移植 Rust 原地起飞!
前端·javascript·node.js