初始提交: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 模板
This commit is contained in:
SoundGame Dev
2026-08-29 13:00:34 +08:00
commit f0f0a9b50e
446 changed files with 2627836 additions and 0 deletions
+191
View File
@@ -0,0 +1,191 @@
# SoundGame 引擎 API 文档
面向游戏开发 agent:本文档说明如何在共享引擎骨架上开发一款纯音频游戏。
骨架已提供:手势识别(GestureEngine)、分层音频(AudioEngine)、状态机基类
(GameEngine)、全屏封面 Activity(MainActivity)。**你只需要实现 `Game`
接口并提供资产,不需要碰 Activity、Compose 或音频底层。**
## 构建与环境
```bash
source /home/ubuntu/SoundGame/env.d/android.sh # 必须先执行(JDK17 / SDK / ffmpeg)
cd /home/ubuntu/SoundGame
# 构建某一个 flavor(各自独立安装,applicationId 不同)
./gradlew assembleStorylineDebug # com.soundgame.storyline
./gradlew assembleReflexDebug # com.soundgame.reflex
./gradlew assembleEscapeDebug # com.soundgame.escape
# 一次构建全部
./gradlew assembleDebug
```
APK 输出在 `app/build/outputs/apk/<flavor>/debug/`。
首次构建会下载依赖,较慢属正常(daemon 有缓存,重跑会快很多)。
## 工程结构
```
app/
build.gradle.kts # flavor 定义、依赖
src/
main/ # 共享引擎,不要放游戏逻辑
java/com/soundgame/
MainActivity.kt # 全屏封面 + 手势路由(一般不用改)
engine/
Gesture.kt # Gesture 枚举
GestureEngine.kt # Modifier.gestureDetector
AudioEngine.kt # AudioEngine 接口 + DefaultAudioEngine
Game.kt # Game 接口 + GameEngine<S> 状态机基类
AndroidManifest.xml
storyline/ # 你的游戏放这里(以 storyline 为例)
java/com/soundgame/game/
GameFactory.kt # 注册点:创建你的 Game 实现
... # 你的其他游戏代码,随意组织
res/drawable/cover.png # 封面图(1080x1920,必须叫 cover)
assets/audio/ # 全部音频资产
bgm/ voice/ sfx/
```
## 快速上手:实现一款游戏
**第 1 步**:在 `app/src/<你的flavor>/java/com/soundgame/game/` 下写游戏类。
推荐继承 `GameEngine<S>`(状态机模式):
```kotlin
package com.soundgame.game
import com.soundgame.engine.AudioEngine
import com.soundgame.engine.GameEngine
import com.soundgame.engine.Gesture
sealed interface Scene {
data object Intro : Scene
data object Hallway : Scene
data object Ending : Scene
}
class MyGame(audio: AudioEngine) : GameEngine<Scene>(audio) {
override val initialState = Scene.Intro
// 状态转移:当前状态 + 手势 -> 新状态(返回原状态表示无转移)
override fun transition(state: Scene, gesture: Gesture): Scene = when (state) {
Scene.Intro -> when (gesture) {
Gesture.TAP -> Scene.Hallway
else -> state
}
Scene.Hallway -> when (gesture) {
Gesture.SWIPE_LEFT, Gesture.SWIPE_RIGHT -> Scene.Ending
else -> state
}
Scene.Ending -> state
}
// 进入新状态时播放音频(onStart 时也会以 previous=null 调一次 initialState)
override fun onEnterState(state: Scene, previous: Scene?) {
when (state) {
Scene.Intro -> {
audio.playBgm("bgm/main_theme")
audio.playVoice("voice/intro")
}
Scene.Hallway -> audio.playVoice("voice/hallway")
Scene.Ending -> {
audio.stopBgm()
audio.playVoice("voice/ending")
}
}
}
}
```
如果你的玩法不是状态机(如 reflex 的计时反应),直接实现 `Game` 接口:
```kotlin
class MyGame(private val audio: AudioEngine) : Game {
override fun onGesture(gesture: Gesture) { /* 任意逻辑 */ }
override fun onStart() { /* 开场 */ }
override fun onStop() { /* 收尾 */ }
}
```
**第 2 步**:在 `GameFactory.kt`(已在骨架中,包名必须是 `com.soundgame.game`)
里把 stub 换成你的实现:
```kotlin
object GameFactory {
fun create(audio: AudioEngine): Game = MyGame(audio)
}
```
MainActivity 只认 `com.soundgame.game.GameFactory.create(audio)`,不要改它的签名。
**第 3 步**:放资产、构建、验证。
## Gesture 枚举
`com.soundgame.engine.Gesture`,共 6 种:
| 值 | 含义 | 判定 |
|---|---|---|
| `SWIPE_UP` / `SWIPE_DOWN` / `SWIPE_LEFT` / `SWIPE_RIGHT` | 四向滑动 | 位移 ≥ 64dp 且平均速度 ≥ 400px/s,方向取位移较大的轴(防误触) |
| `TAP` | 单击 | 标准 tap |
| `DOUBLE_TAP` | 双击 | 标准 double tap |
注意:因为注册了双击,**单击回调会延迟约 300ms**(等双击判定窗口)才触发。
设计玩法时把关键即时反馈尽量放在滑动或双击上,或接受该延迟。
阈值可在 `GestureConfig` 调整(骨架用默认值,如需改动在 `MainActivity` 的
`gestureDetector(config = ...)` 处传参——这影响所有 flavor,改动前先沟通)。
## AudioEngine 接口
```kotlin
interface AudioEngine {
fun play(id: String) // 按 id 前缀分发
fun playBgm(id: String, loop: Boolean = true, fadeMs: Long = 800)
fun stopBgm(fadeOutMs: Long = 800)
fun playVoice(id: String, duckBgm: Boolean = true, onComplete: (() -> Unit)? = null)
fun stopVoice()
fun playSfx(id: String, volume: Float = 1f)
fun setBgmVolume(volume: Float)
fun release() // Activity 已调用,你不用管
}
```
- **三层分离**:BGM 用 ExoPlayer(切换时自动交叉淡入淡出);对白用另一个
ExoPlayer,播放期间自动把 BGM 压到 20%(ducking),播完恢复;短音效用
SoundPool,低延迟可叠加。
- **`id` 约定**:相对 `assets/audio/` 的路径,如 `"sfx/door_open"`。
前缀 `bgm/`、`voice/`、`sfx/` 决定走哪一层;`play(id)` 就靠前缀分发。
**扩展名可省略**,引擎依次尝试 `.ogg` → `.mp3` → `.wav`。
- `playVoice` 的 `onComplete` 在对白自然播完时回调(被 `stopVoice` 打断不回调),
适合做"台词说完再开选项"的剧情驱动。
- 找不到资产时引擎打 `Log.e("AudioEngine", ...)` 并静默跳过,不会崩溃——
但请用 logcat 检查拼写:`adb logcat -s AudioEngine`。
## 资产放哪
```
app/src/<flavor>/assets/audio/
bgm/ 背景音乐(循环长音频,建议 .ogg)
voice/ 对白/旁白(建议 .ogg 或 .mp3)
sfx/ 短音效(建议 .wav 或短 .ogg,SoundPool 低延迟)
```
封面图:`app/src/<flavor>/res/drawable/cover.png`(1080x1920 竖图,骨架已生成
占位渐变图,换成正式图时保持文件名 `cover.png` 不变,MainActivity 按名引用)。
生成/处理音频用 ffmpeg(已配好):如
`ffmpeg -i in.wav -c:a libvorbis -q:a 5 app/src/storyline/assets/audio/bgm/theme.ogg`。
## 规则与注意事项
- **不要修改** `app/src/main/` 下的引擎代码和 `MainActivity`,除非你 flavor 的
需求所有游戏都需要——那是共享代码。确需扩展引擎能力时,加新方法而不要改
已有签名。
- 每个 flavor 的包结构固定为 `com.soundgame.game.GameFactory` + 任意其他类,
三个 flavor 的同名类互不冲突(源码集隔离)。
- 全屏沉浸式、屏幕常亮、竖屏锁定已在 MainActivity/Manifest 处理,不用自己弄。
- 封面图已带 TalkBack 无障碍描述(contentDescription),换图不影响。
- 音频游戏注意节奏:手势回调在主线程,音频 API 均非阻塞,可以直接在
`onGesture` 里调用。