Files
SoundGame/docs/design/26-audio-interaction-playbook.md
T
2026-09-30 23:51:54 +08:00

730 lines
36 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.
# 26 · 纯音频游戏交互设计:第一性原理与落地手册
> **给开发者的实现要点摘要**
> 1. 纯音频游戏是玩家和游戏之间的一场对话。好对话需要的每样东西都要做出来:知道谁在说、知道轮到我了、我一开口对方就停、我说「啊?」对方会换个说法、对方不啰嗦(§1、§2)。
> 2. 原则共 16 条,分为五组:状态可感、输入可信、语言经济、做中学、动机(§3)。
> 3. 落地靠六个通用模块,全部放在引擎层,所有游戏都能继承:发言权管理器、输入回执、新手手势模式、三档提示、迷茫检测、语音优先级(§4.1~§4.6)。
> 4. 每款游戏都要交三张设计单:状态×手势矩阵、等待点配置表、首 3 分钟逐秒脚本(§4.7~§4.10,模板见附录)。
> 5. 验证分三层:自己闭眼冷启动、乱点机器人、真人冷启动测试。合格指标见 §6。
> 6. 改造现有游戏按 P0~P3 的顺序做,P0 是半天级的小改动,效果最大(§5)。
>
> 本文和 `research/02-methodology/methodology.md` 的关系:那一份是基于文献、经模拟玩家验证的方法论。本文补的是它漏掉的部分——真实新玩家的行为:会乱点、没耐心、不会先听完再操作。两份冲突时,以本文为准。
---
## 1. 第一性原理:七个改变不了的事实
所有设计决策都从这七条推出来。它们是物理规律和认知规律,不是偏好,不能商量。
| # | 事实 | 推论 |
|---|---|---|
| F1 | 声音是串行的,一过就没了,没办法「再瞟一眼」 | 玩家需要的状态信息,要么一直在响,要么随时能查到 |
| F2 | 人同一时间只能听懂一条语言,两句叠在一起两句都听不懂 | 语言通道一次只给一个人用,必须有调度 |
| F3 | 声音记忆只有 2~3 秒,工作记忆大约 4 项 | 一次只说一件事;选项不超过 3 个;关键词放在句首或句尾 |
| F4 | 沉默没有明确含义:可能是卡了、在等我、在处理、故意留白 | 每一段沉默都要被设计出来,而且能听出是哪一种 |
| F5 | 看不见「哪里能点」,屏幕是一整块空白 | 可以做什么,只能靠「试一下就知道」来发现,试错必须没有代价 |
| F6 | 手指离开屏幕后,玩家不知道这一下算没算 | 「操作被收到」这件事本身需要反馈 |
| F7 | 新玩家先动手后看说明,耐心大约 30~60 秒 | 第一分钟之内必须给出第一次成功 |
---
## 2. 核心模型:游戏是一场对话
把真人对话的机制逐条翻译成游戏机制。这张表是整个方案的骨架。
| 对话机制 | 真人对话里 | 游戏机制 | 落地模块 |
|---|---|---|---|
| 知道谁在说 | 听声音、看嘴型 | 发言权信号:游戏说话时和等玩家时,背景声明显不同 | §4.1 发言权管理器 |
| 交接话轮 | 说完停下来,看着你 | 固定的「该你了」提示音,之后接持续的等待声 | §4.1 |
| 附和回应 | 「嗯」「好」 | 每次操作 100ms 内响一个回执声 | §4.2 输入回执 |
| 插话 | 你一开口对方就停 | 玩家一操作,游戏立刻闭嘴 | §4.6 语音优先级 |
| 修复误解 | 「啊?」然后换个说法 | 主动重听;第二次用更短的说法 | §4.4 三档提示 |
| 确认听懂 | 看对方反应 | 让玩家做一次动作,不问「听懂了吗」 | §4.10 首 3 分钟脚本 |
| 不啰嗦 | 说过的不原样重复 | 同一信息每出现一次就更短 | §4.4 |
| 看出对方迷糊 | 对方皱眉,你就放慢、拆开讲 | 从乱点里识别迷茫,然后接管 | §4.5 迷茫检测 |
**检验方法**:把游戏想象成坐在你对面的一个人,问自己:「一个正常人这样跟我说话,我会不会觉得他很怪?」会,就是设计有问题。
---
## 3. 十六条原则
每条都写了「规则」和「参数」。参数是起步值,真人测试后再调。
### A 组:状态可感
**原则 1:两种基本状态要一直听得出来。**
- 规则:
- 「游戏在说」的时候,音乐被压低。
- 「轮到你」的时候,先响一次交接提示音,再接一个持续的等待声,一直到玩家操作为止。
- 语音链的句与句之间不能掉回「轮到你」的听感:句间不恢复音乐音量,也不起等待声。
- 参数:提示音响完 400ms 后起等待声;玩家一操作,等待声在 80ms 内淡出。
- 反例:最后一句说完就彻底安静。
**原则 2:状态要少,每一种都要教。**
- 规则:新手期最多暴露 3 种状态(听讲 / 轮到你 / 危险)。每个状态声第一次出现时,配一句解释。
- 反例:设计了 8 个状态提示音,一个都没教。
**原则 3:随时能问「我在哪、我该干嘛」。**
- 规则:用一个固定手势触发,建议用上滑。回答只有一句,按「位置 → 目标 → 现在能做的一个动作」的顺序说。
- 参数:不超过 20 个字,约 4~5 秒。
- 反例:一上滑就念 26 秒的完整帮助。
### B 组:输入可信
**原则 4:每次操作都有声音回执。**
- 规则:回执统一三种——执行了、此刻无效、请稍等(§4.2)。必须是声音,震动只能当补充。
- 参数:100ms 内出声;回执声长度不超过 0.2 秒。
- 反例:无效操作只震一下。电脑和 iPhone 上就是一片安静。
**原则 5:玩家一动,游戏就停。**
- 规则:
- 游戏说话时玩家操作:立刻停下当前这句,先响应这次操作。
- 排队中已经过时的提示、纠错句全部丢掉。
- 只有「关键句」可以不打断。关键句要短于 3 秒,玩家此时操作给「请稍等」回执。
- 反例:玩家已经走到下一步了,游戏还在排队播上一步的纠错。
**原则 6:手势少,而且含义永远不变。**
- 规则:
- 新手期只开放三个手势:单击、左右滑、上滑。不识别双击。
- 所有游戏共用同一张手势表。
- 容易误触的操作(连点、乱滑)绝不绑定重播、重开等破坏性功能。
- 破坏性操作要二次确认。
- 反例:双击 = 整段剧情从头重播。
**原则 7:无效操作解释一次,之后只给回执。**
- 规则:第一次无效操作,说一句原因和正确做法;以后只给回执声;连续 3 次无效操作,触发迷茫接管。
### C 组:语言经济
**原则 8:一次只说一件事,动作放在最前。**
- 规则:指令句按「动作 + 结果」来写,例如「向左滑,开门。」
- 参数:
- 指令句不超过 12 个字,约 3 秒。
- 叙事单句不超过 25 个字。
- 选项不超过 3 个。
**原则 9:同一条信息,每重复一次就更短。**
- 规则:
- 每条提示写三档:完整句 → 短句 → 只有提示音。
- 游戏主动给的提示,永远不原样重复。
- 只有玩家主动要求重听,才播完整原句,并在前面加「再说一遍:」。
**原则 10:语言、音效、系统音、音乐各管一件事。**
- 规则:
- 语言:你要做什么、发生了什么具体的事。
- 内容音效:发生了哪一类事。
- 系统音:你的操作收到没有、现在轮到谁。
- 音乐和环境声:在哪个场景、有多紧张。
- 系统音和内容音的音色必须明显不同。建议系统音一律用合成音,内容音一律用真实录音。
### D 组:做中学
**原则 11:先做后讲,一次只学一样,学完马上用。**
- 规则:
- 第一句话就让玩家动手。
- 一次只教一个新东西,教完马上用一次。
- 用对了:给一个奖励声,加一句不超过 4 个字的肯定,比如「对。」,然后马上进入下一步。
- 做对过的事不再教第二遍。
- 需要两句以上才讲得清的机制,简化或者砍掉。
- 渐进撤离:同一件事连续做对 3 次,就不再给这件事的语音提示,只给提示音。
**原则 12:迷茫要从「乱点」里识别,不只看「不动」。**
- 规则:用 §4.5 的信号判断迷茫,分三级接管:短提示 → 缩小选择 → 自动演示。
### E 组:动机
**原则 13:乱点也要有收获。**
- 规则:每个方向、每个物体都给一个有特点、带信息的声音反应(比如「这边是墙」)。新手期乱点不扣任何资源。
**原则 14:三层循环,每层都有回报。**
- 参数:
- 微循环 1~3 秒:操作一次,得到一次反馈。
- 小循环 20~30 秒:完成一个小目标,听到进展。
- 大循环 3~5 分钟:一局结束,给结算,并留一个悬念。
- 检查方法:每 30 秒切一刀,看这一段有没有能听出来的进展。没有,就是流失点。
**原则 15:失败要便宜。**
- 规则:
- 失败后 2 秒内用一句话归因,格式是「原因 + 下次怎么做」。
- 原因必须真实。玩家根本没出手,就不能说「慢了半拍」。
- 说完直接回到失败前那一步。
- 新手期前两次失败不扣资源,第三次失败自动降难度。
**原则 16:随时能走,回来一句话接上。**
- 规则:任何时刻退出都自动存档;回来 5 秒内,用一句话复述「刚才在做什么 + 现在做什么」。
---
## 4. 落地方法
本节给的是可以直接照做的设计方法和实现骨架。代码都是**示意**,写法对齐本项目引擎的现有接口(`web/engine/audio.js`、`gesture.js`、`guide.js`);实际写的时候以引擎代码为准。
### 4.1 发言权管理器(Floor):解决「搞不清现在是什么状态」
**是什么**:全局只维护一个「现在谁在说话」的状态。所有语音播放和交出话轮的动作都经过它;它负责背景声怎么变。
**状态定义**:
| 状态 | 含义 | 听感 | 玩家操作时 |
|---|---|---|---|
| `SPEAKING` | 游戏在说一段话,整条语音链都算 | 音乐压低;句间不恢复音乐、不起等待声 | 可打断的句:立刻停,按这次操作处理;关键句:给「请稍等」回执 |
| `YOUR_TURN` | 在等玩家操作 | 交接提示音,然后是持续的等待声 | 等待声淡出,交给游戏处理 |
| `BUSY` | 游戏在演出动画音效,或者在处理 | 只有内容音效,没有等待声 | 给「请稍等」回执,不执行 |
| `ACTION` | 实时玩法中(节奏、闪避) | 玩法本身的声音就是状态 | 直接交给游戏,不打断任何东西 |
**转换规则**:
1. 调 `speak()` 时进入 `SPEAKING`。整条链播完后才离开,句与句之间一直算 `SPEAKING`。
2. 离开 `SPEAKING` 时,只能去两个地方:`YOUR_TURN`(交出话轮)或 `BUSY`(接着演出)。不允许「什么都不是」的安静状态。
3. 进入 `YOUR_TURN` 必须经过 `yieldTurn(waitPoint)`。它会一起做四件事:播交接提示音、起等待声、挂提示计时、开启迷茫检测。
**等待声怎么做**(运行时合成,不需要资产):
- 听感要求:轻、有规律、不烦人、手机外放也能听到。手机扬声器 200Hz 以下基本放不出来,所以频率不能太低。
- 起步参数:三角波 220Hz 和 330Hz 两声「嘟—嘟」,每声 60ms,两声间隔 180ms,每 1.4 秒重复一次;增益 0.03,大约是 `guide.js` 里提示音增益 0.12 的四分之一。
- 等待时间超过 20 秒,等待声的间隔拉长到 2.5 秒,更像在耐心地等,不显得催促。
- 剧情类游戏可以把合成声换成一段很轻的环境循环(风声、钟摆),但必须全游戏统一。
**实现骨架(示意)**:
```js
// web/engine/floor.js(示意)
export class Floor {
constructor(audio, guide) {
this.audio = audio;
this.guide = guide;
this.state = 'BUSY';
this._bed = null; // 等待声定时器
this._chain = null; // 当前语音链 { ids, i, critical, onDone, token }
this._token = 0;
}
/** 播一段话(整条链)。critical=true 表示关键句,不可被打断。 */
speak(ids, { critical = false, onDone } = {}) {
this._stopBed();
this.state = 'SPEAKING';
const token = ++this._token;
this._chain = { ids, i: 0, critical, onDone, token };
this._next(token);
}
_next(token) {
const c = this._chain;
if (!c || c.token !== token) return; // 链已被打断,作废
if (c.i >= c.ids.length) { // 整条链播完
this._chain = null;
this.state = 'BUSY'; // 由 onDone 决定交出话轮还是继续演出
c.onDone && c.onDone();
return;
}
const id = c.ids[c.i++];
this.audio.play(`voice/${id}`, { onComplete: () => this._next(token) });
}
/** 交出话轮:交接提示音 → 400ms 后起等待声 → 挂提示与迷茫检测。 */
yieldTurn(waitPoint) {
this.state = 'YOUR_TURN';
this.guide.turn({});
setTimeout(() => this.state === 'YOUR_TURN' && this._startBed(), 400);
waitPoint && waitPoint.arm(); // 见 §4.4 / §4.5
}
/**
* 输入先经过这里。返回值:
* 'consumed' = 发言权层已处理(例如打断了语音),游戏不必再处理;
* 'busy' = 忽略这次操作,给「请稍等」回执;
* null = 交给游戏处理。
*/
handleInput(g) {
if (this.state === 'SPEAKING') {
if (this._chain && this._chain.critical) return 'busy';
this._token++; // 作废整条链
this._chain = null;
this.audio.stopVoice();
this.state = 'YOUR_TURN';
return null; // 打断后,这次操作照常交给游戏
}
if (this.state === 'BUSY') return 'busy';
this._stopBed();
return null;
}
_startBed() { /* 用 audio.ctx 合成两声「嘟—嘟」,setInterval 1400ms 循环 */ }
_stopBed() { /* 清除定时器,当前这声 80ms 淡出 */ }
}
```
> 打断之后,这次操作是「只打断」还是「打断并执行」?
> - 单击:只打断,等于跳过这句。原因是玩家单击多半只是想跳过。
> - 滑动、上滑:打断并执行。滑动是有明确意图的操作。
>
> 这条规则写进手势表(§4.3),所有游戏一致。
**音乐压低要跨整条链**:`audio.js` 现在每句播完都会检查队列,队列空了就恢复音乐音量。如果链是游戏层用 `onComplete` 一句一句接起来的,句间队列是空的,音乐就会被恢复,玩家就听到一次「好像轮到我了」。有两种修法:让 Floor 在整条链期间自己持有一次压低;或者给 `audio` 加一个 `holdDuck(true/false)`。
### 4.2 输入回执:解决「点了没反应 / 不知道这下算没算」
**三种回执**。音色要避开 `guide.js` 里已有的提示音——`turn` 是 1318Hz 的风铃声,`nudge` 是 880Hz 的木鱼声:
| 回执 | 用在哪里 | 合成参数(起步值) | 听感 |
|---|---|---|---|
| `ok` 执行了 | 操作被游戏接受并生效 | 三角波 1600Hz,时长 35ms,起音 2ms,快速衰减,增益 0.08 | 清脆的「嗒」 |
| `invalid` 此刻无效 | 收到了,但现在用不上 | 正弦波 260Hz 滑到 200Hz,时长 120ms,起音 10ms,增益 0.10 | 低沉柔和的「咚」,不像惩罚 |
| `busy` 请稍等 | 游戏正在说关键句或正在处理 | 正弦波 520Hz,时长 40ms,增益 0.04 | 很轻的「嗯」 |
**规则**:
- 回执由引擎统一播,游戏只负责告诉引擎这次操作的结果是哪一种。
- 游戏已经为这次操作播了很明显的内容音(脚步、开门),可以省掉 `ok` 回执,免得声音堆叠。`invalid` 和 `busy` 永远都要播。
- 回执在任何设备上都要出声,不依赖震动。震动可以照常叠加。
**接线方式**:在手势回调外面包一层管道。
```js
// 示意:每款游戏的 attachGestures 回调都换成这个管道
function makeInputPipeline({ floor, confusion, receipts, invalidHints, game }) {
return (g, info) => {
confusion.record(g); // §4.5
const pre = floor.handleInput(g); // §4.1
if (pre === 'busy') { receipts.play('busy'); return; }
if (pre === 'consumed') { receipts.play('ok'); return; }
// 游戏处理函数返回 'ok' | 'invalid' | 'silent-ok'(已有明显内容音)
const r = game.onGesture(g, info) || 'ok';
if (r === 'ok') receipts.play('ok');
if (r === 'invalid') {
receipts.play('invalid');
invalidHints.explainOnce(game.stateName(), g); // 原则 7:每个「状态 × 手势」只解释一次
confusion.recordInvalid();
}
};
}
```
**防连点**:单击执行了「继续」或「跳过」之后的 400ms 内,再来的单击只给 `busy` 回执,不执行。这样点两下不会连跳两句,也不会误进下一段。
### 4.3 手势宪法与新手模式:解决「操作不符合预期」
**新手手势表**(全部游戏共用):
| 手势 | 唯一含义 | 游戏在说话时 | 等你操作时 |
|---|---|---|---|
| 单击 | 继续 / 确定 / 听一下 | 只跳过当前这句 | 执行「继续」或「确定」 |
| 左滑 / 右滑 | 选择 / 移动 | 打断,然后执行选择或移动 | 执行选择或移动 |
| 上滑 | 我在哪、我该干嘛 | 打断,然后回答一句 | 回答一句 |
| 双击 | 新手期不存在 | 按一次单击处理 | 按一次单击处理 |
| 下滑、长按 | 新手期不开放 | 给 `invalid` 回执 | 给 `invalid` 回执,第一次附一句解释 |
**怎么关掉双击、同时去掉 300ms 延迟**:`attachGestures` 支持第三个参数,把双击窗口设成 0:
```js
attachGestures(el, pipeline, { doubleTapWindow: 0 });
```
这样松手就马上判定为单击,不会再产生双击。键盘上的回车仍然会直接发 `doubletap`,所以游戏层在新手模式下要把 `doubletap` 当成 `tap` 处理。
**解锁阶梯**:满足条件后解锁下一个手势,按原则 11 的方式教——先说一句,马上用一次。
| 阶段 | 解锁条件 | 解锁的手势 | 教学句示例 |
|---|---|---|---|
| 0 | 首次进入 | 单击、左右滑 | 「点一下屏幕。」 |
| 1 | 完成第一个小目标 | 上滑 | 「想知道自己在哪,就向上滑。」 |
| 2 | 完成第一局 | 下滑(菜单) | 「向下滑,可以打开菜单。」 |
| 3 | 累计玩了 3 局 | 双击(重听) | 「没听清,就双击,再听一遍。」 |
| 4 | 玩家主动在大厅开启 | 游戏专属的高级手势 | 由各游戏自己定义 |
解锁进度存到 `_settings` 或各游戏的进度数据里,跨局记住。老玩家可以在大厅一键「全部解锁」。
**如果某款游戏的核心玩法必须用四向滑动**:把帮助换到长按,而且所有「四向滑动是玩法」的游戏都统一这么换。不允许每款游戏各换各的。
### 4.4 三档提示:解决「话音重复」
**数据结构**:每个等待点的提示写成三档。
```js
// 示意:等待点的提示配置
const HINT_FIND_KEY = {
full: 'hint_key_full', // 「钥匙在左边的抽屉里,向左滑去拿。」
short: 'hint_key_short', // 「向左滑。」
earcon: 'turn', // 只播交接提示音
};
```
**选择规则**:
```js
// 示意:同一等待点的第 n 次被动提示
function pickHint(hint, n) {
if (n === 0) return { voice: hint.full };
if (n === 1) return { voice: hint.short };
return { earcon: hint.earcon }; // 第 3 次以后只给提示音
}
// 玩家主动要求重听:永远给完整句,并加前缀
function replayOnRequest(hint) {
return ['sys_say_again', hint.full]; // 「再说一遍:」+ 完整句
}
```
**台词表约定**:
- 每条提示的三档 id 用同一个前缀,后缀是 `_full` 和 `_short`。
- `_short` 不超过 6 个字,只保留动作。
- 全项目只需要一句 `sys_say_again`「再说一遍:」,双性别各录一条。
**计数的作用范围**:计数按「这一个等待点」来算,离开这个点就清零。同一个点,玩家做了一次有效操作但还没完成目标,计数不清零,下一次提示接着变短。
### 4.5 迷茫检测:解决「瞎点、不知道怎么玩」
**信号**(在滑动窗口里统计):
| 信号 | 判定 | 权重 |
|---|---|---|
| 连续无效 | 10 秒内有 3 次 `invalid` | 2 |
| 反复重听 | 10 秒内主动重听 2 次以上 | 1 |
| 乱滑 | 2 秒内滑了 3 个以上不同方向 | 1 |
| 同步骤失败 | 同一个等待点失败 2 次 | 2 |
| 静默 | 动作类 6 秒、剧情类 18 秒没有任何操作 | 1 |
**判定**:权重累计达到 2 就触发接管,然后进入 15 秒冷却期,期间不再触发。
**三级接管**:
| 级别 | 做什么 | 例子 |
|---|---|---|
| 1 提示 | 只说下一步的一个动作,用该等待点的 `_short` 句 | 「向左滑。」 |
| 2 缩小 | 只保留一个正确手势,其他手势只给 `invalid` 回执、不产生任何效果 | 「现在只要向左滑。」 |
| 3 演示 | 游戏自己完成这一步,再把话轮交回 | 「听我做一次。」→ 播放向左走的声音 →「现在你来。」 |
同一个等待点,每触发一次接管就升一级。第 3 级用过后,这个等待点就停止接管,只保留等待声。
**实现骨架(示意)**:
```js
// web/engine/confusion.js(示意)
export class Confusion {
constructor({ onConfused }) {
this.events = []; // { t, kind, dir? }
this.cooldownUntil = 0;
this.onConfused = onConfused; // (level) => void,由当前等待点实现
this.level = 0;
}
record(g) { this._push({ kind: 'input', dir: g }); }
recordInvalid() { this._push({ kind: 'invalid' }); }
recordReplay() { this._push({ kind: 'replay' }); }
recordFail() { this._push({ kind: 'fail' }); }
resetForWaitPoint() { this.events = []; this.level = 0; }
_push(e) {
const now = performance.now();
this.events.push({ t: now, ...e });
this.events = this.events.filter(x => now - x.t < 10000);
if (now < this.cooldownUntil) return;
if (this._score(now) >= 2 && this.level < 3) {
this.level++;
this.cooldownUntil = now + 15000;
this.onConfused(this.level);
}
}
_score(now) {
const n = k => this.events.filter(x => x.kind === k).length;
const recentDirs = new Set(this.events
.filter(x => x.kind === 'input' && now - x.t < 2000 && x.dir !== 'tap')
.map(x => x.dir));
let s = 0;
if (n('invalid') >= 3) s += 2;
if (n('replay') >= 2) s += 1;
if (recentDirs.size >= 3) s += 1;
if (n('fail') >= 2) s += 2;
return s;
}
}
```
静默信号继续用现有的 `guide.watchdog`,把它到点的回调也接进同一套接管逻辑。
### 4.6 语音优先级:解决「我操作了它还在说」「纠错迟到」
每一句语音在台词表里标一个类别:
| 类别 | 例子 | 能否被玩家操作打断 | 排队时是否会过期 |
|---|---|---|---|
| `critical` 关键 | 规则的唯一一次说明、结局、关键获得 | 不能(给 `busy` 回执) | 不过期 |
| `content` 内容 | 剧情、场景描述、对白 | 能 | 不过期 |
| `hint` 提示 | 看门狗提醒、操作提示 | 能 | 1.5 秒内没能播出就丢弃 |
| `feedback` 反馈 | 归因、纠错、「对。」 | 能 | 1.0 秒内没能播出就丢弃 |
**硬规则**:
- `critical` 句必须短于 3 秒。超过 3 秒就拆开,只把真正关键的那一句标成 `critical`。
- `hint` 和 `feedback` 永远不排到长链后面。这类句子只在当前空闲时播,播不出去就丢掉。现有 `guide._say` 的「对白在播时跳过」已经是这个思路,这里再加上过期时间。
- 玩家的一次有效操作,会把排队中所有 `hint` 和 `feedback` 清空。它们针对的是旧局面。
### 4.7 状态 × 手势矩阵:消灭「瞎点不符合预期」
**做法**:
1. 每款游戏列出玩家能感知的状态(最多 3~6 个)。
2. 列出新手期开放的所有手势,再加一列「连点 / 乱滑」。
3. 每一格写:会发生什么、给哪种回执、有没有语音。
**示例(《找猫》)**:
| 状态 | 单击 | 左 / 右滑 | 上滑 | 下滑(未解锁) | 连点 / 乱滑 |
|---|---|---|---|---|---|
| 游戏在说 | 跳过这句 + `ok` | 打断,执行移动 | 打断,回答「我在哪」 | `invalid` + 首次解释 | 只处理第一下,其余 `busy` |
| 轮到你 | 听一下周围(猫叫) | 移动 + 脚步声 | 回答「我在哪」 | `invalid` + 首次解释 | 计入迷茫信号 |
| 结算 | 再来一局 | `invalid` + 首次解释 | 播本局结果 | `invalid` | 只处理第一下 |
**验收**:
- 没有空格。
- 没有任何一格是「没有声音」。
- 「连点 / 乱滑」列里没有重播、重开、跳过多段这类效果。
- 同一列里,同一手势的含义基本一致。
**可以自动检查**:在 `web/harness/` 里遍历每个状态,每个手势都发一次,断言 100ms 内有一次回执或内容音。见 §6.2。
### 4.8 台词写作规范:让每一句都值得听
**句式模板**:
| 用途 | 模板 | 好的写法 | 差的写法 |
|---|---|---|---|
| 指令 | 动作,结果。 | 「向左滑,开门。」 | 「如果你想开门,可以尝试向左滑动。」 |
| 交出选择 | 选项一 + 手势,选项二 + 手势。 | 「左滑,救人。右滑,离开。」 | 「你现在面临一个艰难的选择,你可以……」 |
| 我在哪 | 位置。目标。一个动作。 | 「厨房。找钥匙。左右滑听听周围。」 | 念一遍完整规则 |
| 归因 | 原因。下次怎么做。 | 「慢了。听到猫叫马上滑。」 | 「很遗憾,失败了,没关系。」 |
| 肯定 | 不超过 4 个字 | 「对。」「漂亮。」 | 「你做得非常好,现在我们继续下一步。」 |
| 解释无效操作 | 现在是什么情况。能做什么。 | 「还在讲故事。点一下可以跳过。」 | 「这个操作在这里用不上。」(没有告诉玩家该做什么) |
**检查规则**(可以写一个脚本扫台词表):
- 指令类不超过 12 个字,短句档不超过 6 个字,叙事句不超过 25 个字。
- 句中出现「如果」「可以尝试」「您可以」,标黄复查。
- 同一句里出现 2 个以上手势名,标黄复查:一句只教一个。
### 4.9 失败与重试
**每款游戏都要写一张失败表**:
| 失败类型 | 怎么判定 | 归因句 | 回到哪里 | 新手期代价 |
|---|---|---|---|---|
| 方向错 | 手势方向和目标不一致 | 「在左边。」 | 这一步开头 | 不扣 |
| 太慢 | 窗口内出过手,但晚了 | 「慢了。听到就滑。」 | 这一步开头 | 不扣 |
| 没出手 | 窗口内没有任何操作 | 「这下没接。声音一停就轮到你。」 | 这一步开头 | 不扣 |
| 第三次失败 | 同一步失败 3 次 | 「我放慢一些。」 | 这一步开头,降速重来 | 不扣 |
**规则**:
- 归因必须在 2 秒内播出。玩家一操作,归因句就作废(§4.6 的 `feedback` 类)。
- 「回到哪里」一定是离失败最近的检查点,不重播开场,不重念规则。
### 4.10 首 3 分钟逐秒脚本
每款游戏都要写一份,并且真人测过一次。
**模板**:
| 时间 | 游戏说 / 播 | 期待玩家做 | 如果玩家没做 | 如果玩家做错 | 这一步的目的 |
|---|---|---|---|---|---|
| 0~3 秒 | | | | | 第一句就让玩家动手 |
| 3~10 秒 | | | | | 第一次成功 |
| 10~30 秒 | | | | | 教会「轮到你」这个状态 |
| 30~60 秒 | | | | | 在熟悉的动作上建立信心 |
| 60~120 秒 | | | | | 教第二个手势,马上用 |
| 120~180 秒 | | | | | 第一次小胜利 + 留悬念 |
**示例(《找猫》)**:
| 时间 | 游戏说 / 播 | 期待玩家做 | 如果玩家没做 | 如果玩家做错 |
|---|---|---|---|---|
| 0~3 秒 | 一声清晰的猫叫 →「点一下屏幕。」 | 单击 | 6 秒后:「点一下。」再 6 秒只给提示音 | 滑动:`invalid` +「先点一下就好。」 |
| 3~6 秒 | `ok` 回执 → 右边一声猫叫 →「猫在右边。向右滑。」 | 右滑 | 「向右滑。」 | 左滑:撞墙声 +「在右边。」 |
| 6~10 秒 | 脚步声 → 呼噜声 → 奖励音 →「找到了!」 | 无 | | |
| 10~15 秒 | 「听到这个声音,就是轮到你了。」→ 交接提示音 → 等待声 | 无 | | |
| 15~60 秒 | 3 只猫,方向随机,每只都先交接再等 | 左 / 右滑 | 走三档提示 | 归因 + 本步重来 |
| 约 60 秒 | 「想知道自己在哪,就向上滑。」 | 上滑 | 「向上滑。」 | `invalid` + 首次解释 |
| 60~180 秒 | 猫叫变远变轻,加一个干扰声 | 左 / 右滑 | 迷茫接管 | 第 3 次失败降速 |
| 约 180 秒 | 奖励音 + 结算 →「下一关,会有两只猫。」 | 单击 = 下一局 | | |
**节奏检查**:
- 3 秒内有第一次操作。
- 10 秒内有第一次成功。
- 每 30 秒至少一次能听出来的进展。
- 整段没有一句纯规则说明。
### 4.11 声音分层与混音
| 优先级 | 层 | 相对电平(起步值) | 规则 |
|---|---|---|---|
| 1 | 输入回执 | 增益 0.04~0.10 | 永远最先响,不被任何东西压住 |
| 2 | 语音 | 基准 | 同一时间只有一条 |
| 3 | 关键事件音(危险预警) | 比背景响约 10dB | 起音快;预警到判定之间至少 1.5 秒 |
| 4 | 普通内容音效 | 正常 | |
| 5 | 等待声 / 状态声 | 增益约 0.03 | 很轻,但不停 |
| 6 | 音乐、环境声 | 语音播放时压低约 12dB | 整条语音链期间都保持压低(§4.1) |
**硬规则**:
- 同一时间能听清的声音流最多 2 条。
- 区分东西主要靠音色,其次靠节奏,不能单靠音高。
- 系统音(回执、交接提示音、等待声)全部用合成音;内容音用录音。两类永远不混用。
### 4.12 断点复述
- 退出时,把当前等待点的 id 和一句「复述句 id」存进进度数据。
- 回来后,先播状态声,再播复述句,然后 `yieldTurn()`。
- 复述句模板:「你在 X,正在 Y。Z 可以继续。」例如「你在厨房,正在找钥匙。向右滑去卧室。」
---
## 5. 改造现有游戏的顺序
按性价比排序。每一档做完都拿 §6.1 的冷启动测试试一次,再决定做不做下一档。
### P0:半天级小改动,直接解决「点两下就重复」「点了没反应」
1. **新手模式关掉双击**:`attachGestures(..., { doubleTapWindow: 0 })`,`doubletap` 当 `tap` 处理。点两下就不会再从头重播,单击也没有 300ms 延迟了。
2. **回执三件套**:用 `audio.ctx` 合成 `ok`、`invalid`、`busy` 三个声音,接进 §4.2 的输入管道。先保证「无效操作有声音」。
3. **防连点**:单击执行「继续」或「跳过」之后 400ms 内的单击只给 `busy`。
4. **滑动打断**:游戏说话时玩家滑动,立刻 `stopVoice()`,然后执行这次滑动。
### P1:解决「搞不清当前状态」
5. **发言权管理器和等待声**(§4.1),包括音乐压低跨整条链。
6. **三档提示**(§4.4):先改每款游戏最常出现的 3 个等待点,加 `sys_say_again`。
7. **语音分类和过期**(§4.6):给台词表加类别列,引擎按类别处理。
### P2:解决「不知道怎么玩」
8. **重写首 3 分钟**(§4.10):每款游戏一份逐秒脚本,砍掉开场的规则说明。
9. **迷茫检测**(§4.5):接入三级接管。
10. **状态 × 手势矩阵**(§4.7):每款游戏补一张,逐格修掉没有声音的格子。
### P3:解决「没耐心继续」
11. **手势解锁阶梯**(§4.3)。
12. **失败表和就近重试**(§4.9)。
13. **30 秒进展检查**:每款游戏按原则 14 切片检查,补奖励点。
14. **断点复述**(§4.12)。
**建议的起步方式**:先只挑一款游戏,建议 `storyline`(交互最简单,问题最集中)做完 P0 和 P1,自己闭眼玩一次,确认体感改善了,再推广到其他游戏,并把通用部分抽进引擎。
---
## 6. 验证方法
### 6.1 自己闭眼冷启动(每次改动后,10 分钟)
- 用手机、戴耳机、闭眼,假装第一次玩。
- 必须故意做这些事:
1. 不等语音说完就单击;
2. 连点两下、连点三下;
3. 往错误方向滑;
4. 乱滑四个方向;
5. 放着不动 30 秒;
6. 中途退出再进来。
- 每一次「这下发生了什么?」都记一行:时间、做了什么、听到了什么、期待的是什么。
### 6.2 乱点机器人(自动化,每次提交跑一次)
在 `web/harness/` 加一个急躁新手模拟器,模拟真实新手的坏习惯:
| 行为 | 概率 / 参数 |
|---|---|
| 语音开始后 0.5~2 秒就单击 | 40% |
| 连点两到三下(间隔 80~250ms) | 20% |
| 随机方向滑动 | 20% |
| 1 秒内没听到回应就再点一次 | 100% |
| 发呆 10~30 秒 | 10% |
**自动统计的指标**:
- 无声操作数:操作后 100ms 内没有任何回执或内容音。
- 被动原句重复数:同一个语音 id 在没有主动重听请求的情况下,30 秒内播了两次。
- 打断失败数:操作之后,同一句语音还在播超过 150ms(`critical` 句除外)。
- 过期提示数:`hint`/`feedback` 句在触发它的操作之后超过 1.5 秒才播。
- 迷茫触发次数,以及触发后多久完成当前等待点。
- 第一次成功的时间。
### 6.3 真人冷启动测试(每档改完做一次,3~5 人)
**规则**:找没玩过的人,每人 5 分钟,全程不解释任何东西。请他边玩边把想法说出来。
**记录表**:
| 项目 | 第 1 人 | 第 2 人 | 第 3 人 |
|---|---|---|---|
| 第一次操作(秒) | | | |
| 第一次成功(秒) | | | |
| 第一次说出「哦,懂了」之类的话(秒) | | | |
| 说「现在呢?」「怎么又来了」的次数 | | | |
| 乱点连续超过 3 下的次数 | | | |
| 主动放弃的时间点,以及当时在做什么 | | | |
| 结束后一句话评价 | | | |
### 6.4 合格指标
| 指标 | 目标 |
|---|---|
| 第一次操作 | 10 秒以内 |
| 第一次成功 | 20 秒以内 |
| 无声操作 | 0 次 |
| 被动原句重复 | 0 次 |
| 打断失败 | 0 次 |
| 过期提示 | 0 次 |
| 真人 5 分钟内主动放弃 | 5 个人里不超过 1 个 |
| 真人说「现在呢?」 | 平均每人不超过 2 次 |
---
## 7. 一票否决的反模式
1. 玩家操作后没有任何声音。
2. 玩家已经操作了,游戏还在说之前的话(`critical` 句除外)。
3. 游戏主动把同一句话原样重复。
4. 同一手势在不同时刻、不同游戏里意思不同。
5. 连点、双击绑定了重播、重开、连跳多段。
6. 开场先念规则,3 秒内玩家还不能动手。
7. 一个机制要两句以上才讲得清。
8. 等玩家的时候一片安静。
9. 语音链句间掉回「轮到你」的听感。
10. 状态提示音从来没教过。
11. 归因和玩家的实际行为对不上。
12. 失败后要从很前面重新开始。
13. 重要反馈只靠震动。
14. 纠错、提示句排在长链后面迟到播出。
## 8. 随手自检五问
任意时刻停下来问自己:
1. 现在是游戏在说,还是轮到我?2 秒内能听出来吗?(原则 1、2 → §4.1)
2. 我刚才那一下,游戏收到了吗?(原则 4 → §4.2)
3. 如果我现在随便点一下,会发生什么?我能猜到吗?(原则 6 → §4.3、§4.7)
4. 我知道下一步该做的一个动作吗?(原则 3、11 → §4.4、§4.10)
5. 最近 30 秒里我有没有得到一次进展?(原则 14 → §4.10)
---
## 附录 A:等待点配置表模板
每个需要玩家操作的时刻填一行。
| 等待点 id | 期待的手势 | 交接提示音 | 等待声 | 提示 `_full` | 提示 `_short` | 静默阈值 | 迷茫第 2 级只保留的手势 | 迷茫第 3 级演示内容 | 成功反馈 | 失败归因 |
|---|---|---|---|---|---|---|---|---|---|---|
| `find_cat_1` | 右滑 | `turn` | 默认 | 「猫在右边。向右滑。」 | 「向右滑。」 | 6 秒 | 右滑 | 游戏自己向右走一步 | 呼噜声 +「找到了!」 | 左滑:「在右边。」 |
## 附录 B:状态 × 手势矩阵模板
| 状态 \ 手势 | 单击 | 左滑 | 右滑 | 上滑 | 下滑 | 长按 | 双击 | 连点 / 乱滑 |
|---|---|---|---|---|---|---|---|---|
| 状态 1 | 效果 / 回执 / 语音 | | | | | | | |
| 状态 2 | | | | | | | | |
| 状态 3 | | | | | | | | |
## 附录 C:单款游戏设计单(开工前填完)
1. 一句话核心动作:________(写不成一句,就先简化玩法)
2. 玩家能感知的状态(不超过 3 个)以及每个状态的声音身份:________
3. 新手手势表,以及解锁阶梯:________
4. 状态 × 手势矩阵(附录 B)
5. 所有等待点配置(附录 A)
6. 首 3 分钟逐秒脚本(§4.10)
7. 失败表(§4.9)
8. 台词表:每句标类别(critical / content / hint / feedback),提示类都要有 `_full` 和 `_short`
9. 断点复述句
10. 测试记录:闭眼冷启动 + 乱点机器人 + 真人冷启动的结果