Files
SoundGame/research/platform/02-input-parity.md
T
xsl 3704cd0792 research(platform): 无感用户系统 + PC/手机双端一致 三路调研(身份/键位与语音/数据同步)+ 汇总方案
- 01-identity:推荐自动匿名档案(首访 0 交互)+ Netflix 式切换器 + 配对码跨设备;passkey 因 LAN HTTP 不可用否决;审计出 7/8 游戏跨设备只 PUT 不 GET、大厅零 aria、玩家名不可朗读三个存量问题
- 02-input-parity:方向键/WASD+空格+回车(双击即时)键位(audiogames 社区惯例同构);语音双套资产路线(153 句含操作词,306 TTS/37min);MVP ~200 行 1-2 人日
- 03-data-sync:设备 token 即档案、localStorage 按 profileId 分桶(修同设备串档)、桶级 LWW + baseUpdatedAt 守卫、claim 可选认领 + PIN scrypt;公网前 8 条清单
- 00-synthesis:需求映射 + 架构收敛 + P0/P1 分期 + 3 个待拍板决策点
2026-09-09 23:09:24 +08:00

320 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 02 · PC 键盘操作与双端一致体验(Input Parity)调研
> 2026-09-09 · 交互调研 · 范围:PC 浏览器键盘操作方案、引擎适配、提示(画面+语音)随平台适配、混合设备、测试管线影响。
> 红线遵守:本文只做调研与设计,不改任何代码。所有数字均从仓库实测/统计得出,来源随文标注。
## 0. 结论速览
| 决策点 | 推荐 | 一句话理由 |
|---|---|---|
| 键位方案 | 方向键=四向滑动、**空格=单击、回车=双击**、按住空格=长按(WASD 作方向别名) | 与 A Blind Legend / audiogames.net 社区惯例完全同构,双击走专用键避免把 300ms 延迟带进键盘 |
| 语音路线 | **路线 a:双套资产**(`voice/<gender>/key/<id>` 变体目录,复用 male/female 机制) | 与路线 b 重录成本相同(306 文件/37 分钟音频),但增量落地不动已验收资产、两端语气都精确 |
| 最小可用(MVP) | 引擎键盘通道 + 遮罩键盘解锁 + 画面提示自适应,**语音暂不动** | ~200 行改动(gesture.js/audio.js/settings.js/index.html),1~2 人日;语音资产独立分期 |
---
## 1. 现状审计
### 1.1 gesture.js(web/engine/gesture.js,323 行):三通道中的两个,无键盘
- 输入通道:**touch 事件**(主通道,全 preventDefault 防滚动/缩放/合成鼠标)+ **mouse 事件**(桌面调试通道,带 700ms `recentlyTouched()` 守卫防触摸后的合成鼠标误判)。**没有任何 keydown/keyup 监听**——全仓库 grep `keyboard|keydown` 零命中。
- 手势模型:`up/down/left/right/tap/doubletap/longpress/longpressend`(+immediateTap 模式的 `tapcancel`)。核心状态机是**指针生命周期**(down→move→up)+ 长按 450ms 定时器 + 双击 300ms 窗。
- 关键内部函数:`handleDown(x,y)` / `handleMove(x,y)` / `handleUp(x,y)` / `handleCancel()`——**坐标只用于位移判定**(tapSlop/swipeMinDistance),点按与长按判定本身与坐标值无关。这意味着键盘通道可以把"虚拟指针"喂给同一状态机:坐标传元素中心、永不 move,天然只会走出 tap/longpress/doubletap 分支,永远不会误判滑动。
- `attachGestures(element, onGesture, config)` 是唯一接线点,10 个游戏 index.html 全部经它接 `game.onGesture`。**键盘层加在 attachGestures 内部 = 游戏代码零改动**。
- immediateTap(rhythm 独占,`web/rhythm/index.html:33`):按下即发 tap、双击补发改判。键盘空格若复用 handleDown/handleUp,rhythm 的"空格=kick 演奏"自动成立,零改动。
- 长按旁白快进(1.7x):靠 window 事件 `soundgame:longpress/longpressend`(audio.js 监听)。键盘长按若复用同一状态机,这两个事件照发,快进零改动。
### 1.2 启动遮罩(web/engine/audio.js showStartOverlay):只认触摸和鼠标 click
```js
overlay.addEventListener('touchstart', e => e.preventDefault(), {passive:false});
overlay.addEventListener('touchend', e => { e.preventDefault(); onTap(); }, {passive:false});
overlay.addEventListener('click', onTap);
```
- 9 个游戏页 + template 全部传 `text: '轻触开始'`(audio.js 默认值同)——**画面提示的第一个适配点**,且键盘玩家目前**无法用键盘解锁音频**(keydown 不是合法解锁手势路径),必须伸手去摸鼠标。注:浏览器规范里 keydown 属于 user activation 事件,`AudioContext.resume()` 在 keydown 回调内同步发起即可解锁,技术可行,只是遮罩没监听。
- 遮罩期间 gesture.js 尚未 attach(index.html 先 `await showStartOverlay()` 再 `attachGestures`),所以键盘解锁必须改在 audio.js 里。
### 1.3 画面提示现状:游戏页几乎无视觉文案,集中在三处
1. 启动遮罩 hint(audio.js,`轻触开始`×10 页)
2. 大厅 footer(web/index.html:209-211):「操作方式:上 / 下 / 左 / 右滑动、单击、双击」
3. 各游戏 index.html 只有 cover.png 封面 + 遮罩,**无其他操作文案**——画面提示适配的面积非常小。
### 1.4 语音提示现状:操作词烘焙在 TTS 资产里(核心约束)
以 `tools/gen_voice_dual.py` 台词表(8 游戏全量 651 句,TTS 实际数据源)统计含「滑/单击/双击/长按」的句子(regex 实测,2026-09-09):
| 游戏 | 台词总数 | 含操作词句数 |
|---|---|---|
| storyline | 41 | 21 |
| escape | 80 | 26 |
| simon | 78 | 22 |
| rhythm | 40 | 9 |
| space | 59 | 24 |
| rpg | 123 | 16 |
| radio | 103 | 18 |
| dungeon | 127 | 17 |
| **合计** | **651** | **153(23.5%)** |
(注:任务书里「space 27 / dungeon 22 / radio 19」来自对 `docs/design/*-voice-ids.md` 的表格行 grep;voice-ids 文档只覆盖 5 个游戏且部分行多句,本文以 gen 脚本台词表为准——它是 TTS 管线的真实输入。)
受影响资产实测量(ffprobe 逐文件探测,全部存在):
- 153 句 × 2 性别 = **306 个音频文件**,双格式(.ogg+.m4a)共 612 个文件、**49.4 MiB**;
- 音频总时长:male 1078.8s + female 1139.8s = **2218.6s ≈ 37.0 分钟**。
双性别资产先例:`audio/voice/<gender>/<id>.{ogg,m4a}` 整目录切换 + 平铺回退(audio.js `_voiceCandidates`),设置项 `voice`(settings.js,枚举校验 + localStorage + 服务器 `_settings` 同步)——**input 变体目录可完整复用这套机制**。
### 1.5 测试管线现状(web/harness/live/)
- 手势注入:control.mjs `/gesture` → cdp.mjs `Input.dispatchTouchEvent`(touchStart/Move/End 序列,swipe 用 8 步插值 200px 位移满足 64px+400px/s 判定)。**没有键盘通道**。
- 注入同时打标 `__live.events.push({ev:'gesture', type})` 供审计。voice 验收对照 `web/harness/voice_text.json`(8 游戏期望文本)——**语音资产任何措辞变更都要同步此文件**,否则 ASR 相似度判定失真。
---
## 2. 键位方案(含业界依据)
### 2.1 业界惯例证据
**audiogames.net 社区(盲人音频游戏事实标准社区)**——数据库各游戏控制描述呈现出稳定模式:方向键=方位/移动([Night of Parasite](http://fog.audiogames.net/db.php?action=view&id=Night%2520of%2520Parasite)「arrow keys represent the four compass directions」、[AudioSpeed](http://fog.audiogames.net/db.php?action=view&id=AudioSpeed) arrows to steer、[Rebound](http://fog.audiogames.net/db.php?action=view&id=Rebound) 左右方向键移动);**空格=第一主操作**([Technoshock](http://fog.audiogames.net/db.php?action=view&id=technoshock) space 射击、[PacMan Talks](http://fog.audiogames.net/db.php?action=view&id=pacmantalks) space 查距离);回车=菜单确认;**"按住"与"连按两下"是既有表达**(论坛档案「I: fire your laser, hold down for continuous fire」「F: press twice to reactivate」——[audiogames.net 论坛存档](https://audiogames7.rssing.com/chan-63749684/all_p5.html))。
**A Blind Legend**(首批纯音频动作冒险,双端:移动端滑动手势 / PC Steam 键盘)——[Steam 社区键位参考](https://steamcommunity.com/sharedfiles/filedetails/?id=3192446890):←/→ 旋转、↑ 前进(按住持续)、**空格呼叫向导**、Esc 菜单、回车确认。它是"同一游戏、触屏手势与键盘两套措辞并行"的直接先例(移动端教学说的是手势,PC 版说的是按键)。
**Blind Drive**(音频游戏奖项得主,双端)——[官网](https://blinddrivegame.com/)自称「2-button arcade gameplay」,PC 版键盘(+手柄)只做左右闪避([Steam 页](https://store.steampowered.com/app/1300600/Blind_Drive/)、[评测](https://traxion.gg/blind-drive-review/))——证明音频游戏在 PC 上会把触屏/体感操作压缩成极简键盘映射。
**结论**:音频游戏键盘惯例 = **方向键表方位、空格表主操作、回车表确认/第二操作、按住表持续**。SoundGame 的 6 手势可以无损映射进这个惯例。
### 2.2 推荐键位表(主方案)
| 手势(触屏语义) | 键盘主键 | 备用 | 说明 |
|---|---|---|---|
| 左滑 / 右滑 | ← / → | A / D | keydown 即发,**零延迟**(无距离/速度判定) |
| 上滑 / 下滑 | ↑ / ↓ | W / S | 同上 |
| 单击 | 空格 | — | 走现有 tap 状态机:**快速点按**=tap(保留 ~300ms 双击窗延迟,与触屏同感);immediateTap 游戏(rhythm)按下即响 |
| 双击 | **回车** | 空格连按两下 | 专用键**即时**触发(不等 300ms 窗);空格×2 走状态机也能判出 doubletap(第一次的 pending tap 被取消,行为与触屏双击完全一致) |
| 长按 | **按住空格 ≥450ms** | — | 松开=longpressend;voice 播放中即旁白 1.7x 快进(复用 window 事件) |
设计要点与理由:
1. **双击给专用键(回车)而不是只靠连按空格**——这是本项目最重要的键位决策。若键盘只允许"空格×2",单击就必须等 300ms 双击窗(gesture.js 文件头自己写着这是要避开的坑)。回车=双击让"确认类"操作即时响应(A Blind Legend 回车=confirm 同构);同时保留空格×2 兼容,触屏肌肉记忆用户无学习成本。
2. **长按复用空格**而非另设键:长按的语义就是"按住那个确认键",且 audio.js 快进通道长按期间持续生效,独立键反而多一个要教的键位。
3. **WASD 作别名默认开启**:单手党/游戏手玩家习惯;不与箭头冲突,成本为零。
4. 不映射 Esc(下滑已覆盖菜单;Esc 留给未来退出全屏),不占 F 键/数字键(避开浏览器与读屏快捷键域)。
### 2.3 键位冲突分析
- **浏览器默认行为**:箭头键与空格会滚动页面、回车在焦点元素上触发 click——全部在 keydown 里 `preventDefault()` 解决(页面本就 `overflow:hidden` + `touch-action:none`,见 style.css;preventDefault 是双保险,[MDN keyboard-navigable widgets](https://developer.mozilla.org/en-US/docs/Web/Accessibility/Guides/Keyboard-navigable_JavaScript_widgets) 的标准做法)。
- **表单目标守卫**:游戏页无输入框,但大厅有(用户名/PIN `<input>`)——键盘层遇 `INPUT/TEXTAREA/SELECT/contenteditable` 目标直接放行不拦截,避免大厅打不了字。
- **读屏软件**:NVDA/争渡在"浏览模式"会先吃掉箭头键([Stack Overflow: Screen Reader Blocks Keyboard Events](https://stackoverflow.com/questions/72538936/web-development-screen-reader-blocks-keyboard-events))。本项目游戏页无 DOM 文本可浏览,惯例解法是遮罩/根元素加 `role="application"` 促使读屏直通按键——列为实现时的验证项,不是阻塞项(本项目视障玩家主要在手机端,PC 视障玩家属加分场景)。
- **与游戏内快捷键**:现有游戏无键盘快捷键,无冲突。
---
## 3. 引擎适配设计(文件级)
### 3.1 总体思路:并行通道,不做先抽象后重写
在 `attachGestures()` 内**并列**增加键盘通道,与 touch/mouse 共享同一状态机与回调——不做"统一 InputChannel 抽象层"(323 行引擎重写风险大、收益为零;现有三通道已经证明并列式可行)。
### 3.2 gesture.js:新增键盘通道(预计 ~120 行)
```
attachGestures(element, onGesture, config) 内新增:
KEY_MAP = { ArrowUp:'up', ArrowDown:'down', ArrowLeft:'left', ArrowRight:'right',
KeyA:'left', KeyD:'right', KeyW:'up', KeyS:'down' } // WASD 别名
TAP_KEY = 'Space'; DBL_KEY = 'Enter';
window.addEventListener('keydown', onKeyDown) / 'keyup', onKeyUp) // detach 时移除
onKeyDown(e):
1. e.repeat → return // 方向键 repeat 抑制(按住↑只发一次 up)
2. e.target 是表单元素 → return // 大厅输入框放行
3. 仅拦截映射键;其余键不动 // 不吞 Tab(无障碍:别做键盘陷阱)
4. e.preventDefault() // 抑制滚动/焦点激活
5. tracking 时 return // 单飞行指针语义(与现有多指忽略一致,
// 见 onTouchStart 的 e.touches.length>1 分支)
6. 方向键 → vibrate('swipe'); onGesture(dir) // 即时,无位移判定
回车 → vibrate('confirm'); onGesture('doubletap') // 即时,同时 cancelPendingTap()
空格 → handleDown(centerX, centerY) // 进状态机:起 450ms 长按定时器;
// immediateTap 游戏立即发 tap
onKeyUp(e):
空格 → handleUp(centerX, centerY) // 复用收尾:长按→longpressend;快按→tap/双击窗
// (连按两下空格自然判 doubletap)
```
实现细节:
- **坐标取元素中心**(`getBoundingClientRect()` 缓存),永不 move——键盘路径必然只走 tap/longpress/doubletap 分支。
- **450ms 长按判定**:完全复用 `handleDown` 里的 `longPressTimer`,无新逻辑。
- **300ms 双击窗**:完全复用 `lastTapTime/pendingTapTimer`,键盘空格与触摸 tap 共享窗口(混合设备下"触屏点一下+空格点一下"也会判双击——语义正确:同一虚拟指针)。
- **互斥**:`tracking` 标志天然互斥(touch start 或 Space down 先到先得),与现有多指忽略语义一致,不需要 per-channel 状态。
- **焦点**:键盘事件挂 `window`(键盘事件目标是焦点元素而非手势根;游戏页焦点在 body,冒泡到 window 必达)。
- **通道来源标记(可选但建议)**:onGesture 前记 `lastInputChannel='key'|'touch'|'mouse'` 并导出 `getActiveChannel()`——§4 提示自适应与 §6 测试都要用。审计事件 `soundgame:input`(CustomEvent)亦可,供 harness 打标。
### 3.3 audio.js:showStartOverlay 键盘解锁 + 自适应文案(~20 行)
```js
// 键盘解锁:回车/空格(keydown 属 user activation,同步发起 resume 即可)
const onKey = (e) => { if (e.code === 'Enter' || e.code === 'Space') { e.preventDefault(); onTap(); } };
overlay.addEventListener('keydown', onKey);
// (overlay 需 tabindex="0" 并 focus(),keydown 才会落在它上面;或直接挂 window,
// resolve 后移除监听)
```
- 文案自适应:`text` 参数缺省时按 `isTouchLikely()`(§4.1)选「轻触开始」/「按回车或点击开始」;显式传 text 的调用照旧(全部 10 页现在都显式传 `'轻触开始'`——改成不传即自动,属可选的各页 1 行清理,不传也能跑)。
### 3.4 settings.js + 大厅:新增 `input` 设置(~25 行)
- `input: 'auto' | 'touch' | 'key'`(默认 `auto`),进 VALID 枚举;`_settings` 服务器同步机制零改动自动带上。
- 大厅设置面板加一个 `<select>`(自动 / 触屏优先 / 键盘优先)。
- 语义:`auto` = 三通道全开 + 提示跟随最近使用的通道;`touch`/`key` 只影响**提示文案与语音变体选择**,**不关闭另一通道**(永远双通道在线,混合设备友好,见 §5)。
- footer 文案随 input/检测切换。
### 3.5 游戏代码改动边界
| 项 | 改动 |
|---|---|
| 8 个游戏 game.js 的 onGesture 逻辑 | **零改动**(7 种事件语义不变) |
| rhythm immediateTap | **零改动**(空格按下即 kick,长按=双通道一致) |
| 旁白长按快进 | **零改动**(window 事件照发) |
| 游戏页 index.html | 可选各 1 行(遮罩 text 改自适应);不改也能跑 |
| gesture.js / audio.js / settings.js / 大厅 index.html | 引擎侧统一改(上述 ~185 行) |
---
## 4. 提示适配机制
### 4.1 平台检测:能力检测 + 运行时跟随,不做一次性硬判
依据:[MDN @media/pointer](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@media/pointer)、[@media/any-pointer](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@media/any-pointer)、[web.dev Interaction](https://web.dev/learn/design/interaction)、[CSS-Tricks 对混合设备的警告](https://css-tricks.com/interaction-media-features-and-their-potential-for-incorrect-assumptions/)。
- **初始值**:`isTouchLikely() = navigator.maxTouchPoints > 0 && matchMedia('(pointer: coarse)').matches`——触屏手机=true;纯桌面=false;**触屏笔记本**(pointer: fine 主指针 + 有触屏)=false(初始按键鼠提示,运行时纠正)。
- **运行时跟随**:gesture.js 记录 `lastInputChannel`;收到键盘事件后本会话提示切到键盘版,收到触摸切回。混合设备无需玩家配置即可用对提示(settings.input 手动覆盖只是兜底)。
- **手动覆盖**:settings.input(§3.4)。
### 4.2 画面提示文案表
| 位置 | 触屏版 | 键盘版 |
|---|---|---|
| 启动遮罩(audio.js) | 轻触开始 | 按回车或单击开始 |
| 大厅 footer(index.html) | 操作方式:上 / 下 / 左 / 右滑动、单击、双击 | 操作方式:方向键移动、空格单击、回车双击、按住空格长按 |
| 手势教学文案(游戏内 HTML,目前无) | 向左滑 | 按 ← 键 |
| 大厅 footer 第二行(300ms 延迟说明) | 保留 | 键盘版改为「回车=双击即时;空格单击有约 300ms 延迟(与手机一致)」 |
实现:一个 `inputHints.js` 小工具(~40 行)导出 `hintTable()` 按当前通道返回文案对象,audio.js 与大厅共用;CSS 侧如需图标/布局差异走 `@media (pointer: coarse)`。
### 4.3 语音提示三路线对比(核心决策)
先回答调研问题:**视障用户在 PC 上听「向左滑」会困惑吗?** 会,但程度有限且不该依赖它。audiogames.net 社区是 PC 键盘文化的大本营(键盘是默认、触屏是移动端补充);盲人社区对"手势↔键盘等价命令"的映射有明确预期——[AppleVis: Braille Display Users Deserve Better From Blindfold Games](https://applevis.com/blog/braille-display-users-deserve-better-blindfold-games) 明确批评过"只在触屏上讲手势、不给键盘等价命令"的做法。NVDA 用户知道触屏手势存在键盘等价物这个**概念**,但游戏语音说「向左滑」时,键盘玩家必须自己完成一次"向左滑≈按←"的翻译——**首次听不懂、需要猜**,违背本项目「开场交互教学」理念(教学句本身就在这 153 句里,如 storyline help 全句是操作说明)。学术侧同样有「默认键位不合用户习惯是音频游戏主要可用性问题」的记录([Design Guidelines for Audio Games](https://www.academia.edu/61440259/Design_Guidelines_for_Audio_Games))。**路线 c 不能作终态。**
#### 路线 a:双套语音资产(input 变体目录,像双性别一样)
机制:`voice/<gender>/key/<id>.{ogg,m4a}`,audio.js `_voiceCandidates` 追加一跳(key 目录 → gender 目录 → 平铺回退),id 集合只需覆盖 153 句(其余回退现有资产,不存在双份)。
| 维度 | 评估 |
|---|---|
| 资产成本 | **新增 153 句 × 2 性别 = 306 个 TTS 生成**(措辞改写:「向左滑」→「按左方向键」、「单击屏幕」→「按一下空格」、「双击」→「按回车」);落盘 612 文件(ogg+m4a)、**+49.4 MiB** 仓库增量;新音频 **37.0 分钟**(实测同 id 现有资产时长,措辞微调后误差 <10%) |
| GPU 时长 | Qwen3-TTS 本地串行(flock)。参照系:R1 全量双性别管线 286 文件/43.5 分钟音频一个晚间会话完成(docs/audio/round1-report.md);本路线 306 文件/37 分钟与之同量级——**单晚 GPU 串行一轮 + --check 复检 + 少量离群重跑,≤ 半个 GPU 晚**。管线零改动支持:台词表加 `(fid, instruct, text, input='key')` 维度或直接新增 `*_KEY_LINES` 表,断点续跑/--only/--check/--convert 全部照用 |
| 体验 | 最优:两端教学语气精确(「单击屏幕,继续剧情」→「按一下空格,继续剧情」,指令动词与输入设备严格一致) |
| 风险 | ①153 句键盘措辞要策划逐句改写+试听(与 b 相同);②台词表长期 ×2 维护(改一句要过两个变体);③voice_text.json 需按 input 变体分集(live 管线 session 加 input 字段选期望文本集);④旧资产不动、回退安全,**增量落地可灰度** |
#### 路线 b:平台无关措辞重录(一套通吃)
机制:153 句全部改成不含输入方式的措辞(「向左滑:先检查收音机」→「向左:先检查收音机」),**替换**现有 306 文件。
| 维度 | 评估 |
|---|---|
| 资产成本 | 同样 **306 个 TTS 生成**(替换而非新增),37 分钟音频重录,仓库体积持平(措辞略短或 -5%);GPU 时长与 a 完全相同 |
| 额外成本 | ①**已验收资产全部作废重验**(8 游戏 R1~R6 轮次里这 153 句的试听结论失效);②voice_text.json 153 句期望文本全量重对齐;③逐句审查"去操作词后语义仍完整"(如 storyline e2「单击屏幕,重新开始」→「单击,重新开始?」教学力度弱化) |
| 体验 | 两端通用但**两端都变抽象**:触屏新手失去"滑"这个动词(本项目核心新手群体恰是手机端),键盘玩家仍要做一次"单击=空格"的映射(单击一词对键盘玩家仍非按键名)。属于两端各让一步 |
| 风险 | 替换式落地无法灰度;ASR 对照/多音字清单/验收记录全要重跑 |
#### 路线 c:语音不动,PC 靠画面文字提示
| 维度 | 评估 |
|---|---|
| 成本 | 零 TTS、零资产改动;只有 §4.2 的画面文案工作 |
| 体验 | 纯音频游戏玩家不看画面(本项目立项前提),PC 视障玩家**听到的仍是「向左滑」**,见上文社区证据——键盘等价命令缺失是被盲人社区明确批评过的可及性缺陷 |
| 定位 | **只允许作为 MVP 过渡态**(PC 先上线、语音资产下一轮补),且要在 PC 版开场遮罩/大厅用文字+后续一句通用语音说明「键盘操作:方向键/空格/回车」兜底 |
#### 三路线速比
| | a 双套 | b 重录通用 | c 不动语音 |
|---|---|---|---|
| TTS 生成量 | 306 文件 / 37min | 306 文件 / 37min | 0 |
| 仓库增量 | +49.4 MiB | ~0 | 0 |
| 策划文案工作 | 153 句新写 | 153 句改写+全量重审 | 0 |
| 触屏端体验 | 不变(原资产不动) | 轻微弱化 | 不变 |
| PC 端体验 | 最优 | 中 | 差(视障不可用级) |
| 落地方式 | 增量可灰度 | 替换不可灰度 | — |
| 长期维护 | 台词表 ×2 | 台词表 ×1 | ×1(欠账) |
**推荐 a**:与 b 成本几乎相同但风险低一档(不动已验收资产、可灰度、触屏端零感知),且两端教学语气都保住精确性;机制上完整复用 male/female 双性别目录的成熟先例(引擎解析、管线、QC、设置项全套现成)。
---
## 5. 混合设备(触屏笔记本 / 平板外接键盘)
- **运行策略:三通道永远并行在线,不做模式切换。** gesture.js 的 `tracking` 单飞行指针语义天然防跨通道干扰(触摸进行中空格 down 被忽略,反之亦然——与现有多指忽略一致)。
- **提示跟随最近通道**(§4.1 运行时跟随):玩家把平板接上键盘敲了一下方向键,遮罩外的教学提示文案本会话即切键盘版;摸了屏又切回。无需重启页面、无需设置项。
- **语音变体**(路线 a 落地后)同理跟随 `lastInputChannel`;settings.input='auto' 之外的手动值仅用于"我知道我在用什么,别自动切"的用户。
- **一个已知边界**:混合设备上"触摸单击 + 键盘空格单击"共享同一个双击窗(同一虚拟指针),300ms 内跨通道两下会判 doubletap——语义上合理(玩家意图就是双击),文档写明即可,不做隔离。
---
## 6. 测试影响(playtest / live 管线)
- **live 管线加键盘通道(建议,~30 行)**:control.mjs `/gesture` 接受 `{"type":"keyup","code":"ArrowLeft"}` 之类或新增 `/key` 端点 → cdp.mjs 增加 `Input.dispatchKeyEvent`(rawKeyDown/keyUp;比 `Runtime.evaluate` 派发合成 KeyboardEvent 更拟真——不过 gesture 层挂 window、合成事件也够用,CDP 通道留作高拟真档)。审计事件沿用 `{ev:'gesture', type}` 打标,附 `source:'key'`。
- **解锁路径必须测键盘版**:会话第一步 tap 解锁遮罩(现状 README 明确"第一个动作必须是 tap");键盘通道上线后补一条"第一个动作 = Enter 解锁"的用例,验证 keydown 路径的 AudioContext resume。
- **双通道回归**:现有触摸用例零改动可跑(键盘层不改变触摸路径);新增少量键盘用例重点覆盖:方向键 repeat 抑制(按住 1s 只发一次)、空格 300ms 双击窗(快按两下=doubletap、慢按=tap)、按住 450ms=longpress+旁白快进(`__live` 的 boost 事件)、大厅输入框打字不被拦截。
- **voice_text.json**:路线 a 落地时按 input 变体分集(session settings 已支持传 `voice/speed/bgm/guide`,加 `input` 字段即可);路线 b 则要全量重对齐——这也是推荐 a 的测试侧理由。
- **playtest 脚本(sessions/)**:若某轮 playtest 主题是"PC 玩家体验",直接用键盘通道跑既有剧本即可,无需新 harness。
---
## 7. 推荐组合与分期
### MVP(最小可用,PC 能完整玩全部 8 游戏)——约 1~2 人日
| 改动 | 文件 | 量级 |
|---|---|---|
| 键盘输入通道(含 repeat 抑制/表单守卫/滚动抑制/通道标记) | web/engine/gesture.js | ~120 行 |
| 遮罩键盘解锁 + 自适应默认文案 | web/engine/audio.js | ~20 行 |
| input 设置项(auto/touch/key) | web/engine/settings.js + web/index.html | ~25 行 |
| 大厅 footer 文案切换 + inputHints 工具 | web/index.html(+新 inputHints.js ~40 行) | ~45 行 |
| live 管线键盘通道 + 解锁/双通道用例 | web/harness/live/{control,cdp}.mjs | ~30 行 |
语音此时不动(路线 c 过渡态):PC 遮罩文字提示写明「键盘:方向键/空格/回车/按住空格」,教学语音里的「向左滑」暂由玩家自行映射——**过渡期建议不超过一个语音资产轮次**。
### 完整版(语音双端精确)——策划 153 句键盘措辞 + 半个 GPU 晚 + 工程半日
1. 策划:153 句键盘版措辞改写(台词表新增 key 变体行,参照 15/16 号 voice-ids 文档格式交付);
2. 资产:gen_voice_dual.py 加变体维度,`--game all` 串行一轮(306 生成/612 落盘/+49.4 MiB),`--check` 复检;
3. 工程:audio.js voice 解析加 key 目录跳(~15 行)、harness voice_text.json 分集(~20 行);
4. 验收:live 管线 `input:'key'` 会话跑 8 游戏开场教学段,ASR 对照键盘版文本。
### 依赖与顺序
MVP 与语音路线完全解耦,可先行;完整版的变体目录机制依赖 MVP 的 `lastInputChannel`(决定加载哪套语音)。混合设备体验在 MVP 即达标(双通道并行+提示跟随),完整版补齐语音一致性。
---
## 附:本文数字复现方式
- 台词/操作词统计:`tools/gen_voice_dual.py` 各 `*_LINES` 表,regex `[滑|单击|双击|长按]`(2026-09-09 实测 153/651 句);
- 资产时长/体积:对受影响 306 个 .ogg ffprobe 求和(male 1078.8s / female 1139.8s),文件级 du 合计 49.4 MiB(612 文件含 .m4a);
- 业界链接均已在 §2/§4 正文内嵌。
## 附:业界依据来源
- [A Blind Legend — Steam 社区键位参考](https://steamcommunity.com/sharedfiles/filedetails/?id=3192446890)、[Steam 商店页](https://store.steampowered.com/app/437530/)
- [Blind Drive 官网](https://blinddrivegame.com/)、[Steam 页](https://store.steampowered.com/app/1300600/Blind_Drive/)、[Traxion 评测](https://traxion.gg/blind-drive-review/)、[PCGamingWiki](https://www.pcgamingwiki.com/wiki/Blind_Drive)
- audiogames.net 数据库:[Night of Parasite](http://fog.audiogames.net/db.php?action=view&id=Night%2520of%2520Parasite)、[Technoshock](http://fog.audiogames.net/db.php?action=view&id=technoshock)、[PacMan Talks](http://fog.audiogames.net/db.php?action=view&id=pacmantalks)、[Rebound](http://fog.audiogames.net/db.php?action=view&id=Rebound)、[AudioSpeed](http://fog.audiogames.net/db.php?action=view&id=AudioSpeed)、[论坛存档](https://audiogames7.rssing.com/chan-63749684/all_p5.html)
- [AppleVis — Braille Display Users Deserve Better From Blindfold Games(手势↔键盘等价命令)](https://applevis.com/blog/braille-display-users-deserve-better-blindfold-games)
- [MDN — @media/pointer](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@media/pointer)、[@media/any-pointer](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@media/any-pointer)、[Keyboard-navigable JavaScript widgets](https://developer.mozilla.org/en-US/docs/Web/Accessibility/Guides/Keyboard-navigable_JavaScript_widgets)
- [web.dev — Interaction](https://web.dev/learn/design/interaction)、[CSS-Tricks — Interaction Media Features 混合设备陷阱](https://css-tricks.com/interaction-media-features-and-their-potential-for-incorrect-assumptions/)
- [Stack Overflow — Screen Reader Blocks Keyboard Events](https://stackoverflow.com/questions/72538936/web-development-screen-reader-blocks-keyboard-events)
- [Design Guidelines for Audio Games](https://www.academia.edu/61440259/Design_Guidelines_for_Audio_Games)