- 共享引擎(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 模板
115 lines
7.2 KiB
Markdown
115 lines
7.2 KiB
Markdown
# 《声命线》(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 淡入淡出时长 |
|