第16章 - 踩坑记录与常见问题

16.1 踩坑记录总表

CHAGS 的开发过程中积累了大量「现象 → 原因 → 处置」的记录。它们分散在各模块的代码注释与项目文档里,这里按类别集中。每一条都对应代码里的一处修复或一条 lint/测试规则。

A. Vue × Phaser 集成

# 现象 原因 处置
A1 持有 Phaser 实例后卡顿甚至抛错 ref 会深度代理整个 Phaser 对象图(场景、纹理、WebGL 上下文) 一律 shallowRef / markRaw;写进 code review 清单
A2 路由切走后后台仍跑着 canvas 未 destroy 或容器被 <KeepAlive> / <Transition> 保留 onBeforeUnmount 里 await unmount()(内部 game.destroy(true));不放 KeepAlive
A3 开发热更新后越玩越卡、有多个画面 HMR 热替换叠加出多个 canvas 编辑 Phaser 场景时整页刷新;宿主侧 disposed 标记防迟到挂载

B. Phaser 4 引擎行为

# 现象 原因 处置
B1 点在 A 上却抓到了 80 像素外的 B Container 的 setInteractive(hitArea) 局部坐标约定与直觉不符(按钮那样「点正中心」的场景察觉不到,拖拽一上来就暴露) 自己写 drag.ts:按下时抓住半径内最近的目标,既确定又对小朋友更友好
B2 CI 与慢手机上「按住 3.4 秒到不了 45%」 Phaser 低帧率时钳制 delta,按帧间隔累加的长按进度失真 长按改按墙钟计时(记起点,now - holdStartedAt);补慢设备用例
B3 长按模式(夸父)永远不提示 卡住提示只写在连点分支里;孩子只会点不会按,点一下只涨 30,要 60 下才满 提示逻辑移出模式分支,两种模式都生效;长按「按住 420ms 才算会了」才收小手,卡住时把小手找回来
B4 射出的箭「穿过」了目标没命中 箭一帧能飞很远,按点判定会漏 按「这一帧扫过的线段」判定(点到线段距离),并取沿弹道最先碰到的目标

C. 内容与数据

# 现象 原因 处置
C1 某种数值配比下永远点不满 / 凑不出 内容数值组合错误 schema 层 refine:tap 强制整除、substitution 用有界完全背包(canReach)校验「容差内可通关」
C2 同一个洞补两次算两个 完成判定按「归位次数」计数 改为按槽位去重计数(filledCount),回归用例覆盖
C3 把里程碑的暂停删掉、把三条并成一条,测试全绿 里程碑不可观测 发 beat:enter:milestone:<id> 事件;e2e 断言「到河边时尚未通关」「三条里程碑按序各出现一次」
C4 新增第 8 个典故全绿,用户点进去 404 典故 id 抄在四个地方(内容 JSON、索引、注册表、e2e),抄漏无测试发现 registry 一致性单测(三方对账);e2e 的 id 列表改为枚举内容目录

D. 双语与版面

# 现象 原因 处置
D1 英文版典故卡糊成一团:标题出框、星星压寓意、出处压原文 卡片用固定纵坐标排版;中文五个字 vs 英文三十个字母 改先量后排:统一按卡片内宽换行、量高排布、卡片高度由内容决定、超限整体等比缩小;按钮位置固定不动
D2 英文长句时,故事页小手压住最后一行正文 小手固定在 H-150 小手按正文实测高度定位,放不下挪到右下角;正文起点 985 → 950
D3 提示气泡的句号被挤成孤零零的第三行 换行宽度 W-260 太窄 放宽到 W-180;分幕正文宽度 560 → 596(44px 汉字一行 13 个字)
D4 英文译文把 unit 写成 baskets 却能通过所有检查,玩家打开才报错 game.stages[].levelScript 在内容 schema 里是 unknown,只查内容 schema 不够 content:check 在合并译文后重新过一遍模板 schema;另加 CJK 泄漏扫描抓漏翻字段

E. 无障碍与可读性

一次全仓审查集中处理了可读性问题(修复前 → 修复后):

# 位置 修复前对比度 处置
E1 曹冲读数(整局最高潮的数字)压水面 1.48:1,几乎看不见 垫卡片底 → 12.5:1
E2 结算答案数字压天空 2.92:1 换深红 #b03a31 → 4.05:1
E3 界面主按钮(亮红配纸色) 3.42:1 换 #b03a31(5.44:1)等深色变体(青 5.61、金 5.42)
E4 顶栏导航按钮只有 36px 触控目标偏小 py-3 + min-h-12(48px)
E5 典故卡原文与出处只有 13.8 CSS px 手机上 12px 提到 28 设计 px(约 16 CSS px)
E6 360px 窄屏上标题折成三四行 未处理溢出 min-w-0 + truncate,副标题窄屏隐藏
E7 故事页连点第二下跳掉一整幕 第二下落在下一幕刚建好的点击区上 350ms 输入锁

F. 工具链与工程

# 现象 原因 处置
F1 vue-tsc 报 ERR_PACKAGE_PATH_NOT_EXPORTED: './lib/tsc' TypeScript 7 移除了该子路径导出,而 vue-tsc 依赖它 锁 TS 5.9.3(唯一满足三方 peer 的组合);解法记录进方案文档
F2 安卓打包报 Unsupported class file major version 69 Gradle 8.14 不支持 JDK 25 build-apk.mjs 自动挑 JDK 17–24;支持 CHAGS_JDK 覆盖
F3 玩家屏幕上是 zod 报错原文 [{"origin":"string","code":"too_big"...}] 关卡数据校验失败直接透传 界面上改「一句人话 + 回典故馆按钮」;技术详情进控制台与 window.__chags.lastError
F4 玩法被改坏了要等发版才发现 release workflow 只在打 tag 时跑、且不含端到端;体积预算写了「用 size-limit 守卫」却没有东西在守;失败时上传的 artifact 路径根本不存在 新增 ci.yml(check / e2e / audit 三班岗);新增 check-size.mjs 接进 verify;artifact 改传 test-results
F5 Electron 本地加载被 CORS 拒绝 file:// 下 <script type="module" crossorigin> 不可用 改用 Tauri(默认自定义协议)
F6 安卓版全程没有旁白 Android WebView 未实现 Web Speech API 走原生 TTS(Capacitor 插件桥接),Web 端保留 Web Speech
F7 CI 上 ./gradlew 报 exit 126 gradlew 在 Windows 上提交,git 没保留可执行位 git update-index --chmod=+x 修正 + workflow 里再 chmod +x 兜底

16.2 常见问题(FAQ)

Q1:怎么把游戏给孩子玩?

三种方式:手机浏览器打开部署好的网址(最方便);下载 Windows 便携 exe 或安卓 apk(第 14 章,离线可用);本地开发用 pnpm dev。注意浏览器版目前还是在线加载静态资源(PWA 离线缓存属阶段 1)。

Q2:进度会丢吗?存在哪里?

存在设备的 IndexedDB(本地),无账号、不同步。刷新、重开都在;换设备或清浏览器数据会丢。想跨设备要等云存档(换 ProgressStore 实现即可,第 10 章)。

Q3:支持哪些语言?怎么加新语言?

当前中文 + 英文。加 zh-TW 等新语言的完整步骤见第 11 章 11.8:注册 locale、补界面文案表、补 12 份内容翻译、跑 content:check 与 audit:i18n。游戏代码零改动。

Q4:想换美术 / 配音怎么做?

美术:改 games/<id>/src/art.ts 里的绘制函数(换成贴图调用即可),模板与测试不动;分幕插图走 story[].illustration 字段。配音:替换 AudioBus.speak 的实现(按 locale 取音频文件),接口不变。

Q5:复习测验在哪?为什么通关后没有答题?

quiz 模板与 21 道题(每典故 3 道含迁移题)都已就绪,但故意没有挂在「每局结束」——那与「少写字、多动手」冲突。正确入口是「到期复习」:典故卡到复习时间后从图鉴进入小测验。接线路径见第 10 章 10.7。

Q6:能上微信小游戏吗?

架构为它留了门(PlatformServices 是唯一出口),但项目文档的结论很克制:微信小游戏不是「换个适配器就能上」 —— 还需处理 WebGL 上下文差异、包体限制、wx 全局注入与资源加载器替换。决策规则:如果小游戏是硬性首发要求,应先用 Phaser 3.90 + weapp-adapter 做 2–3 天 PoC 验证,或改用 Cocos Creator;如果首发是 H5 / 微信内网页,当前方案最优,小游戏作为后续可选分支。

Q7:可以用它做自己的典故合集吗?

可以,MIT 许可。第 16.3 节给了一份 fork 指南。

Q8:为什么没有排行榜、账号、联机、内购?

主动不做(Anti-scope,第 15 章 15.8):它们抬高成本与合规风险,对「让孩子记住典故」几乎无贡献。每一项都留了触发条件,需要时按既定出口扩展。

Q9:测试现在有多少?

写作本文时:单测 23 个文件、156 项;端到端 41 项(12 部典故的真实操作 + autoplay + 双语 + 慢设备 + 截图)另加双语版面巡检 34 张截图。跑法见第 13 章。

Q10:儿童隐私怎么保证?

无广告 SDK、无社交、无外链诱导、不采集个人信息;遥测默认只做本地缓冲且 flush() 为空实现;进度只存本机。这些不是配置项,而是代码里没有相反的路径(第 9 章)。

Q11:为什么游戏里几乎没有文字,家长会不会觉得没教东西?

设计原话:孩子是「做」出这个办法,而不是被告知(第 15 章三个案例)。教学负载分布在交互、画面、音效、语音里;关卡数据里还有 keyMoment、misconceptions、quiz 三层「记住」的证据链。

Q12:pnpm verify 全绿就可以提交了吗?

改了玩法或交互还要跑 pnpm e2e;改了文案、版面或语言要跑 pnpm audit:i18n。原则:新用例要能失败 —— 写完先故意破坏被测行为,确认变红。

16.3 二次开发指南:做一个自己的合集

假设你不想做典故游戏,而是想做「世界寓言」「科学小实验」之类的合集。可以复用的部分比想象的多:

保留(几乎不用动):

  • 工程骨架:pnpm workspace 三层结构、契约、模板机制、平台层、进度层、工具链(verify / content:check / size / e2e / CI);
  • 三个铁律与对应的 lint 规则、测试范式。

替换(按顺序做):

  1. 品牌层:apps/site/index.html 的标题与描述、messages.ts 的 app.title / app.subtitle、tauri.conf.json 的 productName / 窗口标题、capacitor.config.ts 的 appId / appName、图标(scripts/make-icons.py);
  2. 内容层:清空 packages/content/src/allusions/ 与 i18n/en/,按第 12 章的流程逐份添加;kind 字段的语义(history/myth/legend)按你的题材调整(如 science/story);
  3. 模板层:保留 story / place / tap / sort 等通用骨架;为你的题材新增模板(第 7 章 7.14 清单);
  4. 游戏层:删掉不需要的 games/* 目录,同步清 registry.ts、apps/site/package.json 与 e2e 引用 —— registry.test.ts 会立刻告诉你哪里漏了;
  5. 术语层:界面文案里「典故馆 / 典故卡」等词按题材替换(都在 messages.ts 一处)。

三个不要动的纪律(它们是这套工程的价值所在):

  • pnpm verify 必须是「完成」的定义,不要为了赶进度绕过;
  • content:check 的内容约束(关键抉择、迁移题、出处)不要删 —— 想改规则可以,改成你的题材真正需要的约束,而不是取消约束;
  • 一致性测试(registry)、可观测事件(window.__chags)、autoplay 直达入口,一个都别省。

16.4 路线图与结语

项目自己的路线图(阶段划分):

阶段 内容 状态
阶段 0 · 技术验证 骨架、schema、三模板、曹冲称象、pnpm verify、冒烟 ✅ 已完成
阶段 1 · MVP 十二部典故、双语、打包双平台、CI、体积守卫 ✅ 已完成(本教程描述的状态)
阶段 1 遗留 PWA 离线、家长锁、休息提醒、复习中心接线 ⏳ 待做
阶段 2 · 扩充 模板扩到 8+、典故扩到 20–30、低端机降级 规划
阶段 3 · 平台化 云存档、家长报告、(可选)微信小游戏 PoC 规划

回看整个项目,它最值得学习的地方不是用了 Phaser 或 Vue,而是把三件事做透了:把教学要求写进数据结构(keyMoment / refine 规则 / transfer 题)、把工程纪律写进工具链(verify / content:check / 体积守卫 / CI 三班岗)、把没做完的事如实记录(复习接线、PWA、家长锁)。教程到这里结束,祝你也做出让孩子记住点什么的作品。


本教程基于 znlgis/CHAGS 仓库本地版本写成,代码示例均经精简,接口与行为以源码为准。