初始提交: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:
@@ -0,0 +1,161 @@
|
||||
# SoundGame — 架构文档
|
||||
|
||||
本文档描述共享引擎(`app/src/main/`)的内部实现与设计决策。
|
||||
如何基于引擎开发新游戏见 `docs/engine-api.md`(面向使用的 API 手册);
|
||||
本文面向需要读懂/改动引擎本身的人。
|
||||
|
||||
## 模块划分
|
||||
|
||||
```
|
||||
app/src/main/java/com/soundgame/
|
||||
├── MainActivity.kt # 唯一 Activity:全屏封面 + 手势路由 + 生命周期
|
||||
└── engine/
|
||||
├── Gesture.kt # Gesture 枚举(6 种手势)
|
||||
├── GestureEngine.kt # Modifier.gestureDetector:手势识别
|
||||
├── AudioEngine.kt # AudioEngine 接口 + DefaultAudioEngine 实现
|
||||
└── Game.kt # Game 接口 + GameEngine<S> 状态机基类
|
||||
```
|
||||
|
||||
调用关系:
|
||||
|
||||
```
|
||||
MainActivity ──创建──> DefaultAudioEngine ──注入──> GameFactory.create(audio)
|
||||
│ │
|
||||
└─ gestureDetector { game.onGesture(it) } <──────────┘
|
||||
(每个 flavor 提供自己的 GameFactory/Game 实现)
|
||||
```
|
||||
|
||||
- **MainActivity**:只做四件事——沉浸式全屏 + 屏幕常亮、显示
|
||||
`res/drawable/cover.png`(带 TalkBack 描述)、把手势回调路由给
|
||||
`game.onGesture`、在 `onDestroy` 里 `game.onStop()` + `audioEngine.release()`。
|
||||
它通过硬编码约定 `com.soundgame.game.GameFactory.create(audio)` 拿到游戏实例,
|
||||
三个 flavor 各自在隔离的源码集里提供同名类,互不冲突。
|
||||
- **GestureEngine**:一个 Compose `Modifier` 扩展,把触摸事件翻译成
|
||||
`Gesture` 枚举回调。
|
||||
- **AudioEngine**:接口 + 唯一实现 `DefaultAudioEngine`,三层音频分发。
|
||||
- **Game / GameEngine**:游戏侧契约。`Game` 是三个方法的极简接口;
|
||||
`GameEngine<S>` 是其上的状态机抽象(见下文)。
|
||||
|
||||
## 音频三层架构(DefaultAudioEngine)
|
||||
|
||||
| 层 | id 前缀 | 播放器 | 特性 |
|
||||
|---|---|---|---|
|
||||
| BGM | `bgm/` | ExoPlayer(单实例轮换) | 循环播放,切换时交叉淡入淡出 |
|
||||
| 对白 | `voice/` | ExoPlayer(单实例轮换) | 播放期间 duck BGM 到 20%,播完恢复 |
|
||||
| 音效 | `sfx/` | SoundPool(maxStreams=8) | 低延迟,可叠加 |
|
||||
|
||||
`play(id)` 按前缀分发;id 可省略扩展名,依次尝试 `.ogg` → `.mp3` → `.wav`
|
||||
(`resolveAssetPath`);找不到资产打 `Log.e("AudioEngine", ...)` 并静默跳过,
|
||||
**不会崩溃**——拼写错误只能靠 `adb logcat -s AudioEngine` 发现。
|
||||
|
||||
### BGM 交叉淡入时序
|
||||
|
||||
`playBgm(id)` 被调用时:
|
||||
|
||||
```
|
||||
t=0 新 player 以 volume=0 启动(prepare+play),旧 player 仍在响
|
||||
t=0~fadeMs 协程逐帧推音量:新 player 0 → 目标音量;旧 player 当前音量 → 0
|
||||
t=fadeMs 旧 player release()
|
||||
```
|
||||
|
||||
默认 `fadeMs=800`,步进 30ms 一格(`fadeVolume`)。`stopBgm(fadeOutMs)` 同理淡出后
|
||||
release。切 BGM 不会停止对白层。
|
||||
|
||||
### 对白 ducking 时序
|
||||
|
||||
```
|
||||
playVoice(id) ──> 新 voice player 开播,同时 duckBgm():
|
||||
BGM 音量 300ms 内渐降到 baseVolume × 0.2
|
||||
播放自然结束 ────> onPlaybackStateChanged(ENDED):
|
||||
unduckBgm()(300ms 恢复)→ release player → 回调 onComplete
|
||||
```
|
||||
|
||||
- duck 比例为 `duckVolume = 0.2`(构造参数)。
|
||||
- `stopVoice()` 打断的对白**不触发** `onComplete`——游戏层(storyline 的
|
||||
`playSeq`、escape 的 `speak`、reflex 的 `say` 队列)都依赖这一点实现
|
||||
"打断即作废整条语音链"。
|
||||
- `onComplete` 是"台词说完再推进剧情"的核心驱动机制。
|
||||
|
||||
### SFX 层注意事项
|
||||
|
||||
- SoundPool `load()` 是异步的:首次播放某音效时若尚未加载完成,`play()` 返回 0,
|
||||
静默失败(见 `playSfx` 注释)。**因此每个游戏 onStart 都必须以音量 0 预播一遍
|
||||
全部短音效**(storyline/reflex/escape 均已实现 `prewarm`)。
|
||||
- 音效按 `assetPath.hashCode` 缓存 soundId,重复播放不重复加载。
|
||||
|
||||
## 手势识别(GestureEngine)
|
||||
|
||||
实现:两个叠加的 `pointerInput`——`detectTapGestures`(单击/双击)+
|
||||
`detectDragGestures`(滑动)。
|
||||
|
||||
滑动判定(`GestureConfig` 默认值):
|
||||
|
||||
| 参数 | 默认值 | 含义 |
|
||||
|---|---|---|
|
||||
| `minSwipeDistance` | 64dp | 累积位移低于此值不识别为滑动(防误触) |
|
||||
| `minSwipeVelocityPxPerSec` | 400 px/s | 平均速度(位移/拖动时长)低于此值不触发 |
|
||||
|
||||
方向取位移较大的轴(`|dx| > |dy|` 判左右,否则判上下),斜滑归一到最近主轴。
|
||||
|
||||
**单击 300ms 延迟**:因为同时注册了 `onDoubleTap`,Compose 的
|
||||
`detectTapGestures` 必须等双击判定窗口(约 300ms)关闭后才能回调 `onTap`。
|
||||
这是全引擎级的行为,游戏设计必须绕开它:
|
||||
|
||||
- reflex:正面攻击要求单击格挡,因此其反应窗口强制 ≥ 1100ms
|
||||
(`FRONT_MIN_WINDOW_MS`),且"无正面攻击时单击不惩罚";
|
||||
- storyline:单击只用于"继续剧情"这种非即时操作;
|
||||
- escape:单击检查房间,同样非即时。
|
||||
|
||||
关键即时反馈尽量放在滑动或双击上。
|
||||
|
||||
## 状态机模式(GameEngine<S>)
|
||||
|
||||
```kotlin
|
||||
abstract class GameEngine<S>(protected val audio: AudioEngine) : Game
|
||||
```
|
||||
|
||||
- 游戏定义状态类型 `S`,实现 `transition(state, gesture): S`(纯函数,
|
||||
返回原状态表示无转移)和 `onEnterState(state, previous)`(副作用:播音频)。
|
||||
- `onGesture` 是 final 的:算 `transition`,状态变了才更新并回调 `onEnterState`。
|
||||
- `onStart` 以 `previous=null` 进一次 `initialState`。
|
||||
|
||||
实际采用情况:**三个游戏都没有用 `GameEngine`**——
|
||||
|
||||
- storyline:剧情树 + STORY/MENU 双模式 + 选择缓存(`pendingChoice`),
|
||||
状态不是简单枚举,直接实现 `Game`;
|
||||
- reflex:计时器驱动的反应玩法(Handler 调度出怪),非状态机;
|
||||
- escape:INTRO/PLAY/END 三 phase + 位置/物品等多维状态,直接实现 `Game`。
|
||||
|
||||
`GameEngine` 仍是新游戏(线性剧情类)的推荐起点。
|
||||
|
||||
## flavor 机制
|
||||
|
||||
`app/build.gradle.kts` 中 `flavorDimensions += "game"` + 三个 productFlavor
|
||||
(storyline / reflex / escape),各自独立 `applicationId`,可同机共存安装。
|
||||
|
||||
每个 flavor 源码集(`app/src/<flavor>/`)包含:
|
||||
|
||||
```
|
||||
java/com/soundgame/game/GameFactory.kt # 注册点(包名/签名固定)
|
||||
java/com/soundgame/game/<Xxx>Game.kt # 游戏实现
|
||||
res/drawable/cover.png # 封面(1080x1920,文件名固定)
|
||||
assets/audio/{bgm,voice,sfx}/ # 全部音频资产(ogg)
|
||||
gen/ # 资产生成脚本(可复现,不入 APK)
|
||||
```
|
||||
|
||||
`minSdk = 26`,`compileSdk = 36`,依赖 Compose BOM 2025.06.01 +
|
||||
media3-exoplayer 1.6.1。
|
||||
|
||||
## 已知限制
|
||||
|
||||
- **单击延迟 ~300ms**(双击判定窗口),引擎级,见上。
|
||||
- **SoundPool 首次播放丢失**:靠各游戏预热缓解,引擎层未做加载完成队列。
|
||||
- **引擎静默容错**:资产缺失只打日志不崩溃,拼写错误不易发现。
|
||||
- **手势参数全局共享**:`GestureConfig` 在 MainActivity 处对所有 flavor 生效,
|
||||
不能单游戏定制。
|
||||
- **无持久化**:reflex 最高分为进程内变量(`companion object var highScore`),
|
||||
杀进程即丢;三款游戏均无存档。
|
||||
- **单 voice 通道**:同时只有一条对白在播,新 `playVoice` 会顶掉旧的;
|
||||
需要"背景环境音 + 对白"时环境音只能走 sfx 层(escape 的做法)或并进 BGM。
|
||||
- **未真机听测**:反应窗口、响度平衡、立体声分离度等手感参数只验证到
|
||||
构建层面,需实机试听调整(见 README 待办)。
|
||||
Reference in New Issue
Block a user