730 lines
36 KiB
Markdown
730 lines
36 KiB
Markdown
# 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. 测试记录:闭眼冷启动 + 乱点机器人 + 真人冷启动的结果
|