- 共享引擎(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 模板
7.5 KiB
SoundGame 引擎 API 文档
面向游戏开发 agent:本文档说明如何在共享引擎骨架上开发一款纯音频游戏。
骨架已提供:手势识别(GestureEngine)、分层音频(AudioEngine)、状态机基类
(GameEngine)、全屏封面 Activity(MainActivity)。你只需要实现 Game
接口并提供资产,不需要碰 Activity、Compose 或音频底层。
构建与环境
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>(状态机模式):
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 接口:
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 换成你的实现:
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 接口
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里调用。