# 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` 的关系:那一份是基于文献、经模拟玩家验证的方法论。本文补的是它漏掉的部分——真实新玩家的行为:会乱点、没耐心、不会先听完再操作。两份冲突时,以本文为准。 > > 本目录其余设计文档已按本文重写。项目施工口径见 [00-global-ux.md](00-global-ux.md);索引见 [README.md](README.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. 测试记录:闭眼冷启动 + 乱点机器人 + 真人冷启动的结果