Files
SoundGame/docs/games/storyline.md
T
SoundGame Dev f0f0a9b50e 初始提交:3 款纯音频游戏 + 共享引擎 + 完整文档与资产管线
- 共享引擎(app/src/main):GestureEngine 6 手势识别、AudioEngine 三层音频
  (ExoPlayer BGM 交叉淡入 / 对白 ducking / SoundPool 低延迟)、Game/GameEngine
  状态机接口、MainActivity 全屏封面 + TalkBack
- 《声命线》storyline:末世互动广播剧,3 幕 8 抉择点 4 结局
- 《声东击西》reflex:听声辨位反应,12 波战斗 + BGM 三档 BPM 变速
- 《暗室》escape:3x3 网格密室逃脱,钥匙→铁柜→保险丝→电箱→大门谜题链
- 文档:setup(从零搭建)/ architecture(架构)/ engine-api(引擎 API)/
  asset-pipeline(资产流水线)/ games/*(三款游戏设计文档)
- 资产管线:Qwen3-TTS 对白 + 程序合成音效/BGM + Mureka BGM 的 gen 脚本与台账,
  全部 ogg 资产入库可复现
- env.d 环境脚本:android/tts 入库;sfx/mureka 含密钥不入库,仅提交 .example.sh 模板
2026-08-29 13:00:34 +08:00

115 lines
7.2 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.
# 《声命线》(storyline) — 末世互动广播剧 设计文档
- 包名:`com.soundgame.storyline`
- 代码:`app/src/storyline/java/com/soundgame/game/StorylineGame.kt`
- 生成脚本:`app/src/storyline/gen/`(`gen_voice.py` / `gen_sfx.py` / `convert.sh` / `make_cover.py`)
大停电后第 30 天,前急救员林晚靠一台旧收音机寻找妹妹林晨,对抗抢掠者"猎狗"。
3 幕、8 个抉择点、4 种结局,单局 5-7 分钟。
## 手势映射
| 手势 | 剧情模式 (STORY) | 菜单模式 (MENU) |
|---|---|---|
| 单击 | 继续剧情(对白播完才生效)/ 结局后重开 | 返回故事 |
| 双击 | 重听当前节点全部台词 | 重播菜单语音 |
| 左滑/右滑 | 选择节点做抉择(提示音播完才生效,播放中会缓存 `pendingChoice`) | 左滑=重新开始;右滑=返回 |
| 上滑 | 帮助 + 当前幕进度,随后自动重播当前节点 | 听帮助,随后回菜单 |
| 下滑 | 呼出菜单 | — |
实现要点:
- 游戏是 `Mode.STORY / Mode.MENU` 双模式 + 剧情节点表驱动,未用 `GameEngine` 基类。
- `playSeq` 用 `seqToken` 做打断作废:任何重播/菜单/重开都会 `seqToken++`,
旧语音链的 `onComplete` 回调因此失效,不会错序推进。
- 对白播放中单击**不**跳节点(防误触);选择提示音播放中的滑动会被缓存,
播完立即生效(`choiceArmed` / `pendingChoice`)。
- `onStart` 里以音量 0 预播 `sfx/ui_confirm` 预热 SoundPool。
## 剧情树
节点 id 即资产文件名前缀(`voice/n00_intro.ogg` 等)。`◆` 为选择节点,
`【标记】`为选择写入的剧情标记,`※` 为结局。
```
n00 开场+教学 ──> n01 地铁站醒来 ──> n02 ◆p1 检查收音机 / 摸黑找出口
├─ 左: n03 收音机(林晨录音) ─┐
└─ 右: n04 出口(红漆警告) ──┤
v
n06 口哨包围 <── n05 避难所广播(坐标37.5)
│
n07 ◆p2 钻通风管 / 尸体堆装死
├─ 左: n08 通风管 ──────────┐
└─ 右: n09 装死(听到陷阱情报) ┤ 【knowTrap 死标记,见缺陷】
v
n10 ◆p3 酸雨夜,哭声:查看 / 不理会
├─ 左: n11 ◆p4 小满:带上他 / 留下食物
│ ├─ 左: n12 带上小满 【companion=true】 ─┐
│ └─ 右: n13 独自上路 ──────────────────┤
└─ 右: n14 无视哭声 ──────────────────────────┤
v
n15 ◆p5 断桥:涵洞 / 钢索
├─ 左: v_culvert → n16 / n16b(带小满版) ──┐
└─ 右: v_cable → n17 / n17b(带小满版) ──┤
(v_* 虚拟节点由 resolve() 按 companion 解析)
v
n18 ◆p6 猎狗索坐标:交出 / 周旋
├─ 左: n19 交出坐标 【betrayed=true】 ──> n20 ◆p7
│ ├─ 左: n21 追上去警告 ──> ※e2 代价
│ └─ 右: 自己逃命 ────────> ※e3 孤声(坏结局)
└─ 右: n22 扬沙脱身(中弹) ──> n23 ◆p8 广播频道
├─ 左: n24 公开频道 ──> ※e4 静默
└─ 右: n25 加密频段 ──> ※e1 黎明(好结局)
(companion 时追加
e1_child 彩蛋台词)
```
三幕划分(`actOf()`,用于上滑报进度):n00–n09 第一幕(地铁站与街面),
n10–n17b 第二幕(雨夜与断桥),n18 起及所有结局 第三幕(广播塔)。
## 角色音色分配(gen_voice.py)
| 角色 | speaker | instruct 变体 |
|---|---|---|
| 旁白 | Uncle_Fu(低沉醇厚中年男声) | NARR 冷静苍凉 / NARR_WARM 转温暖 / NARR_COLD 冷漠空洞 / NARR_EPIC 悲壮 |
| 林晚(主角) | Serena(温暖柔和年轻女声) | LIN_CALM 虚弱强撑 / LIN_SOFT 温柔坚定 / LIN_CRY 哭腔 / LIN_RUN 奔跑喘息 / LIN_SHOUT 嘶哑呼喊 / LIN_LOW 压低克制 |
| 猎狗(反派) | Dylan(北京话年轻男声) | DOG_PLAY 阴冷戏谑 / DOG_COLD 残忍带杀气 |
| 小满 / 林晨 / 广播 | Vivian(明亮年轻女声) | KID 稚嫩童声 / CHEN 急切压低(电台感) / BCAST 机械无感情 |
无线电失真不在 TTS 侧做:`convert.sh` 转码时对 `n03_msg` / `n05_bcast`
两条加 ffmpeg 带通(highpass 400Hz + lowpass 3200Hz + 压缩器)模拟电台。
## 资产清单
| 层 | 数量 | 内容 |
|---|---|---|
| voice | 48 | 系统语音 5(help/status1-3/menu)+ 选择提示 8(p1-p8)+ 剧情 30 + 结局 5 |
| sfx | 12 | ui_confirm, ui_menu, heartbeat, radio_static, whistle, footsteps, rubble, water, gunshot, struggle, wind, signal_beep(全部程序合成) |
| bgm | 1 | dark_drone.ogg(Mureka 生成,117s 无缝循环;原程序合成版备份在 tools/bgm_backup/) |
## 已知缺陷 / 遗留线索
- **`knowTrap` 是死标记**:`choose()` 里 `when (nodeId)` 的 `"n09"` 分支永远不会
命中——`choose` 只在选择节点被调用,而 n09 不是选择节点(它是 n07 抉择的
目标节点)。也就是说"装死听到北区陷阱情报"这个情报没有任何后续剧情消费它。
修复方向:把判定移到 `playNode("n09")` 处,或在 n10/n18 处消费该标记。
- **`betrayed` 只写不读**:n18 交出坐标后标记置位,但后续节点没有任何分支
读取它(e2/e3 的分化完全由 n20 的抉择决定)。可用于让旁白在结局前追加
一句"赎罪"差分。
- 菜单模式下双击重播菜单语音,但如果菜单语音正在播则无响应(`voicePlaying`
守卫),手感上像"双击失灵",属有意为之。
- 结局后任意单击直接重开,没有确认;误触会丢失当前结局画面(音频游戏
影响较小)。
## 调参入口
| 位置 | 参数 | 控制什么 |
|---|---|---|
| `StorylineGame.nodes` | 整张剧情表 | 节点增减、分支结构、台词文件映射 |
| `gen_voice.py` `LINES` | 台词/instruct | 文案与语气 |
| `gen_voice.py` `CHUNK = 6` | TTS 批量大小 | 显存/吞吐 |
| `gen_sfx.py` 各函数 | 合成参数 | 音效质感(采样率 24kHz,峰值归一 0.85) |
| `gen_sfx.py` `bgm_drone()` | 程序合成 BGM | 已被 Mureka 版替换,仅留作备份复现 |
| `convert.sh` | `-q:a 4`(voice/sfx)/ `-q:a 3`(bgm) | ogg 码率/体积 |
| 引擎层 | `playBgm` fadeMs=800 | BGM 淡入淡出时长 |