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

7.2 KiB
Raw Blame History

《声命线》(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 淡入淡出时长