1. 开篇

背单词让人痛苦的点从来不是"找不到内容",而是"开始太难"。打开手机、解锁、找 App、点进词库——这几个动作足以让大多数人在犹豫中放弃。

楼主想做一款不需要手机的背词工具。 Rokid Glasses 给了我一个机会:通过 AIUI 智能体直接进入闯关卡片,用镜腿按键完成答题、提示、斩词。用户整个闯关过程只靠镜腿按键完成,不需要语音持续介入。

这就是单词地下城的起点。


2. 输入层:镜腿按键的单入口

在眼镜上,我选择了物理按键作为唯一的交互入口:

  • 镜腿按键:上下键移动焦点,确定键确认选择。整个闯关过程只靠按键完成,不需要语音持续介入,也不打扰周围环境。

用户在智能体打开那一刻:单词卡片一旦打开,后续所有操作都由镜腿按键完成。

这种交互让用户几乎不需要改变日常姿态:站在地铁里、走在路上、甚至在厨房做饭时,都能顺便完成一轮背词。


3. 技术架构

整个应用的技术实现出乎意料地轻量。楼主没有采用复杂的多页面架构,而是用两张卡片承载所有功能:

  • 闯关卡片:负责所有的游戏逻辑——出题、答题、提示、斩词、连击统计、回合总结。

  • 战绩面板:负责数据展示——正确率、最佳连击、已斩单词、错词本。

在 AIUI 智能体中,卡片的生命周期与对话绑定,用户用完即走,不需要持久化的页面栈。两张卡片足以覆盖"学习 → 复习 → 统计"的完整闭环。

项目结构也因此保持了极致的简洁:

agent1/
├── AGENTS.md              # 智能体清单
├── app.json               # 应用配置
├── app.js                 # 应用入口
├── utils/
│   ├── wordbook.js        # 词库工具
│   ├── words.js           # 本地词库
│   └── store.js           # 本地存储
└── pages/
    ├── index/index.ink    # 闯关卡片
    └── stats/index.ink    # 战绩面板

4. 核心循环:闯关逻辑

如果把背单词看作一场小型 RPG,那么每一轮闯关就是一个"关卡"。我在闯关卡片里实现了一个有限状态机,用 phase 字段控制当前显示的 UI 和可接收的输入:

loading → quiz → answered → quiz → ... → summary → (revenge / restart) → quiz

阶段解析

  • loading:旋转的"斩"字动画,给本地词库 2 秒的冷启动时间。

  • quiz:展示单词、音标、4 个释义选项,以及"给点提示"和"斩 ⚔"两个辅助操作。

  • answered:用户选择后立即反馈对错,展示完整释义、例句,支持发音和译文切换。

  • summary:本轮战报——正确率、最佳连击、斩词数、错词列表。如果有错词,提供"错词复仇"入口。

  • empty:词库已空或错词本已清空时的特殊状态,给予正向反馈。

整个状态流转由一个 _session 对象驱动,它保存了当前轮次的全部上下文:

this._session = {
  mode: 'quiz',      // quiz | revenge
  queue: [],         // 单词队列
  index: 0,          // 当前索引
  combo: 0,          // 当前连击
  bestCombo: 0,      // 本轮最佳
  correct: 0,
  wrong: 0,
  zhan: 0,
  wrongList: [],     // 答错的单词
  hintUsed: false
};

这种设计让代码的"当前状态"一目了然,调试时只需要 inspect _session 就能知道用户卡在哪一步。


5. 数据层:离线设计

楼主刻意没有做任何网络请求。词库内嵌在项目中,学习进度存储在设备本地。

原因很简单:背单词这件事不需要实时联网。用户可能在通勤路上、在会议间隙、在飞机上,网络并不总是可靠。离线设计让"开始闯关"这个动作的成本降到最低——不需要等待加载,不需要担心流量,打开就用。

存储层面,使用了 wx.getStorageSync / wx.setStorageSync 按天分组组织数据,但核心数据结构非常直白:

// 战绩
{
  answered: 42,          // 总答题数
  correct: 35,           // 正确数
  bestCombo: 8,          // 最佳连击
  zhanWords: ['apple', 'banana'],  // 已斩单词
  wrongWords: [          // 错词本
    { word: 'cherry', meaning: '...', wrongCount: 2 }
  ]
}

"斩词"是一个关键设计:它不仅将单词加入已掌握列表,还会从错词本中移除。这意味着用户对某个词的操作(斩)会永久改变它的复习优先级,系统不会再浪费时间在已经掌握的内容上。


6. 交互细节

眼镜上没有触摸屏,但我采用的解决方案是模拟键盘焦点:用上下键移动高亮边框,确定键触发操作。

这个系统的核心是两个方法的配合:

moveFocus(delta) {
  const count = this.getFocusCount();
  const next = (this.data.focusedIndex + delta + count) % count;
  this.setData({ focusedIndex: next });
},

getFocusCount() {
  switch (this.data.phase) {
    case 'quiz':
      return this.data.options.length + 3; // 4 选项 + 提示 + 发音 + 斩词
    case 'answered':
      return 3; // 发音、译文、下一词
    case 'summary':
      return this.data.summary?.wrongCount > 0 ? 2 : 1;
    default:
      return 1;
  }
}

不同阶段,可聚焦的元素数量不同。在 quiz 阶段,用户可以在 4 个选项、提示、发音、斩词之间循环切换;进入 answered 阶段后,焦点数量收缩为 3(发音、译文、下一词)。这种动态调整避免了无效焦点的存在,让操作路径始终保持在最短。

UI 上,当前焦点的元素会获得绿色高亮边框,未聚焦的选项则保持低调。用户不需要精确点击,只需要"接近"目标即可,这大大降低了操作门槛。


7. 视觉系统

AIUI 官方对 AR 眼镜显示有明确规范:单绿色单色显示。纯黑背景上只渲染一种绿色通道,保证在透明镜片上清晰可读、不晕眩。

这不是限制,而是一种设计约束。楼主围绕这个约束建立了一套色彩体系:

  • 主背景 #020402:极深的绿黑,接近纯黑但保留一丝绿色调

  • 主绿色 #40ff5e:高亮、进度条、正确反馈、按钮激活态

  • 正文 #dfffe6:柔和的白绿,长时间阅读不刺眼

  • 标题 #f3fff6:更高的亮度,建立信息层级

  • 连击/复仇 #ffd76a:琥珀色,用于强调游戏化元素

  • 错误/错词 #ff8a8a:柔和的红色,用于负面反馈

所有颜色都围绕单一绿色展开,通过亮度和饱和度区分信息优先级。这种克制反而让界面更加聚焦——用户一眼就能看到单词、选项和当前进度,不会被多余的颜色分散注意力。


8. 性能与体验

Loading 动画的品牌意义

每轮闯关开始前,用户会看到一个旋转的"斩"字。"斩"是这款应用最核心的动作动词,用户每一次看到它,都是在强化"我要去斩词了"的心理暗示。

动画使用 CSS 3D transform 实现 Y 轴旋转,性能开销极低,但在眼镜透明镜片上呈现出足够的立体感。

自动发音的时机

单词渲染后,系统会立即通过 TTS 播报发音。这个时机很关键:太早,页面还没准备好;太晚,用户已经盯着屏幕等着了。我把播报放在 renderCurrent 方法的最后,确保单词文字已经写入页面,TTS 可以立即读取。

斩词的边界条件

"斩词"看似简单,但有几个边界需要处理:

  • 同一轮不能重复斩同一个词(通过 zhanWords 列表判断)

  • 斩词后立即从当前队列移除,不计入答对/答错统计

  • 已斩单词在下一轮会被 pickQuizWords 自动排除,避免重复出现

这些逻辑分散在 recordZhanstartQuiz 中,但组合起来形成了一个完整的"掌握"判定闭环。


9. 开发手记:从 Craft 到真机

开发环境使用 Rokid 官方提供的 AIUI Studio,支持直接导入本地项目并实时预览。这个工具在早期就能验证交互逻辑,而不需要每次都打包上传到眼镜设备。

不过,模拟器和真机之间依然有差异。最典型的是按键事件的延迟:在 AIUI Studio 中,键盘上下键的响应是瞬时的;但在真机上,镜腿按键会有几十毫秒的机械延迟,加上蓝牙传输,焦点移动会显得"肉"一些。我在 moveFocus 中加入了防抖处理,避免一次按键触发多次跳转。

另一个坑是 TTS 的异步特性。wx.speech.playTTS 在某些情况下会返回空字符串而不报错,加了一层防御:

function speakText(text) {
  if (!text) return;
  try {
    const result = wx.speech.playTTS(text);
    if (result === '') {
      console.warn('[TTS] playTTS returned empty string');
    }
  } catch (error) {
    console.error('[TTS] playTTS failed:', error.message);
  }
}

10. 复盘:什么做对了,什么还可以更好

做对的

  1. 离线优先:零网络依赖让学习场景不再受限,用户反馈"打开就能用"是最高的评价。

  2. 镜腿按键导航:虽然实现起来比触摸复杂,但用户学习成本极低。上下键 + 确定键是物理世界的直觉映射。

可以更好的

  1. 焦点系统的可访问性:目前只有上下键和确定键,缺少"返回"或"取消"的语义。用户在 summary 阶段如果想中途退出,没有快捷方式。

  2. 词库的可扩展性:当前词库是静态数组,扩充需要手动编辑。虽然可以通过脚本从 LLM 生成,但还没有集成到 App 内。

  3. 战绩的持久化策略wx.getStorageSync 在数据量大时会有性能瓶颈。未来如果词库扩展到 2000+ 词,战绩数据也需要考虑分片或索引。
     


11. 结语

单词地下城不是一个追求"大而全"的产品。它只做一件事:让背单词变得足够简单,简单到你愿意在碎片时间里随时开始一场闯关。

如果你也在 Rokid Glasses 上试过它,或者对 AIUI 对话式开发有想法,欢迎交流。

项目代码开源,欢迎 Star:https://gitee.com/wangpangzhia/word-dungeon


12. 附录:快速上手

# 克隆项目
git clone https://gitee.com/wangpangzhia/word-dungeon.git

核心文件速览:

文件

职责

app.json

声明lifetime: cut 对话式卡片模式

pages/index/index.ink

闯关卡片:状态机、按键导航、TTS 播报

pages/stats/index.ink

战绩面板:数据展示、错词本管理

utils/wordbook.js

词库工具:抽题、生成干扰项

utils/store.js

本地存储:战绩读写、斩词、错词聚合

更多推荐