chore: initial commit
This commit is contained in:
622
Docs/Design/12_AudioSystem.md
Normal file
622
Docs/Design/12_AudioSystem.md
Normal file
@@ -0,0 +1,622 @@
|
||||
# 12 · 音频系统
|
||||
|
||||
> **命名空间** `BaseGames.Audio`
|
||||
> **所属文档集** [← 返回索引](./README.md) · [总览](./00_Overview.md)
|
||||
> **依赖** `BaseGames.Core.Events` · Unity Audio · AudioMixer
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [设计原则](#1-设计原则)
|
||||
2. [AudioMixer 架构](#2-audiomixer-架构)
|
||||
3. [AudioManager](#3-audiomanager)
|
||||
4. [BGMController — 自适应音乐](#4-bgmcontroller--自适应音乐)
|
||||
5. [音乐状态机](#5-音乐状态机)
|
||||
6. [AudioZone — 区域音乐触发](#6-audiozone--区域音乐触发)
|
||||
7. [AudioEventSO — SFX 集成](#7-audioeventsso--sfx-集成)
|
||||
8. [GlobalSFXPlayer](#8-globalsfxplayer)
|
||||
9. [音频资产规范](#9-音频资产规范)
|
||||
10. [AudioConfigSO](#10-audioconfigso)
|
||||
11. [SettingsManager — 音量持久化](#11-settingsmanager--音量持久化)
|
||||
12. [事件频道](#12-事件频道)
|
||||
13. [编辑器友好设计](#13-编辑器友好设计)
|
||||
|
||||
---
|
||||
|
||||
## 1. 设计原则
|
||||
|
||||
- **分层混音**:所有音频通过 AudioMixer 路由,统一控制音量分组,不直接设置 `AudioSource.volume`
|
||||
- **自适应音乐**:BGM 根据游戏状态(探索/战斗/Boss)动态切换,保持氛围沉浸感
|
||||
- **零耦合**:`BGMController` 只订阅事件频道,不持有 `GameManager`、`EnemyBase` 等引用
|
||||
- **像素风音效**:优先使用复古 8-bit / 16-bit 风格音效,随机 Pitch 变化(±5%~10%)防止重复感
|
||||
- **2D 空间音频**:本游戏为 2D 横版,不使用 3D 音效空间化(`AudioSource.spatialBlend = 0`),仅靠左右声道区分近远
|
||||
|
||||
---
|
||||
|
||||
## 2. AudioMixer 架构
|
||||
|
||||
`MainMixer.mixer` 资产路径:`Assets/Audio/MainMixer.mixer`
|
||||
|
||||
### 混音组层级
|
||||
|
||||
```
|
||||
Master
|
||||
├── BGM (背景音乐:探索BGM、Boss BGM、主菜单BGM)
|
||||
├── SFX (音效:战斗、UI、环境互动)
|
||||
│ ├── SFX_Player (玩家动作音效,子混音组)
|
||||
│ ├── SFX_Enemy (敌人音效,子混音组)
|
||||
│ └── SFX_World (世界互动音效,子混音组)
|
||||
└── Ambient (环境音:风声、水声、洞穴回声等)
|
||||
```
|
||||
|
||||
### Exposed Parameters(暴露给代码控制的参数名)
|
||||
|
||||
| 参数名 | 对应混音组 | 范围 | 说明 |
|
||||
|--------|-----------|------|------|
|
||||
| `MasterVolume` | Master | -80 ~ 0 dB | 总音量 |
|
||||
| `BGMVolume` | BGM | -80 ~ 0 dB | 背景音乐音量 |
|
||||
| `SFXVolume` | SFX | -80 ~ 0 dB | 音效总音量 |
|
||||
| `AmbientVolume` | Ambient | -80 ~ 0 dB | 环境音音量 |
|
||||
|
||||
**线性值转分贝**(Settings 滑条映射):
|
||||
|
||||
```csharp
|
||||
public static float LinearToDecibel(float linear)
|
||||
=> linear > 0.0001f ? 20f * Mathf.Log10(linear) : -80f;
|
||||
```
|
||||
|
||||
### AudioMixer 快照(Snapshots)
|
||||
|
||||
| 快照名 | 用途 | 关键差异 |
|
||||
|--------|------|---------|
|
||||
| `Default` | 正常游玩 | 所有组正常音量 |
|
||||
| `Paused` | 游戏暂停 | BGM / SFX 均降低 -12 dB,添加低通滤波(Cutoff 500Hz)|
|
||||
| `Dead` | 玩家死亡 | BGM 渐出 -80 dB(1.5s 过渡) |
|
||||
| `BossFight` | Boss 战 | Ambient 降低 -20 dB(突出 Boss BGM)|
|
||||
|
||||
快照切换:`_mixer.TransitionToSnapshots(new[] { snapshot }, new[] { 1f }, transitionTime)`
|
||||
|
||||
---
|
||||
|
||||
## 3. AudioManager
|
||||
|
||||
`AudioManager` 常驻 Persistent 场景,管理 BGM AudioSource 和 SFX 全局播放:
|
||||
|
||||
```csharp
|
||||
namespace BaseGames.Audio
|
||||
{
|
||||
[DefaultExecutionOrder(-500)]
|
||||
public class AudioManager : MonoBehaviour
|
||||
{
|
||||
[Header("AudioMixer")]
|
||||
[SerializeField] AudioMixer _mixer;
|
||||
|
||||
[Header("BGM Sources")]
|
||||
[SerializeField] AudioSource _bgmSourceA; // 双 Source 交叉淡入淡出
|
||||
[SerializeField] AudioSource _bgmSourceB;
|
||||
|
||||
[Header("SFX Source")]
|
||||
[SerializeField] AudioSource _globalSFXSource; // 一次性 SFX 播放
|
||||
|
||||
[Header("Config")]
|
||||
[SerializeField] AudioConfigSO _config;
|
||||
|
||||
// 对外接口
|
||||
public void Initialize(); // SettingsManager 读取 → 应用音量
|
||||
public void SetVolume(string exposedParam, float linear); // 滑条回调
|
||||
public void PlayBGM(AudioClip clip, float fadeOutDur = 1f, float fadeInDur = 1f);
|
||||
public void StopBGM(float fadeOutDur = 1f);
|
||||
public void PlaySFX(AudioClip clip, float volumeScale = 1f);
|
||||
public void TransitionToSnapshot(string snapshotName, float transitionTime);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### BGM 交叉淡入淡出
|
||||
|
||||
使用双 `AudioSource`(A/B)实现无缝交叉淡变:
|
||||
|
||||
```
|
||||
当前播放 → Source A(淡出 fadeOutDur)
|
||||
新曲目 → Source B(淡入 fadeInDur)
|
||||
淡出结束 → 交换 A/B 角色,下次切换时复用
|
||||
```
|
||||
|
||||
```csharp
|
||||
IEnumerator CrossFade(AudioClip newClip, float fadeOutDur, float fadeInDur)
|
||||
{
|
||||
var outSource = _activeSource;
|
||||
var inSource = _inactiveSource;
|
||||
|
||||
inSource.clip = newClip;
|
||||
inSource.volume = 0f;
|
||||
inSource.Play();
|
||||
|
||||
float t = 0f;
|
||||
while (t < Mathf.Max(fadeOutDur, fadeInDur))
|
||||
{
|
||||
t += Time.unscaledDeltaTime;
|
||||
outSource.volume = Mathf.Lerp(1f, 0f, t / fadeOutDur);
|
||||
inSource.volume = Mathf.Lerp(0f, 1f, t / fadeInDur);
|
||||
yield return null;
|
||||
}
|
||||
outSource.Stop();
|
||||
(_activeSource, _inactiveSource) = (inSource, outSource);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. BGMController — 自适应音乐
|
||||
|
||||
`BGMController` 订阅各种游戏事件,自动切换 BGM 曲目:
|
||||
|
||||
```csharp
|
||||
public class BGMController : MonoBehaviour
|
||||
{
|
||||
[SerializeField] AudioManager _audioManager;
|
||||
[SerializeField] AudioConfigSO _config;
|
||||
|
||||
// 监听的事件频道
|
||||
[SerializeField] GameStateEventChannelSO _onGameStateChanged;
|
||||
[SerializeField] StringEventChannelSO _onRegionEntered;
|
||||
[SerializeField] BoolEventChannelSO _onBossFightToggled;
|
||||
|
||||
string _currentRegion = "Forest";
|
||||
|
||||
void OnGameStateChanged(GameState state)
|
||||
{
|
||||
switch (state)
|
||||
{
|
||||
case GameState.MainMenu:
|
||||
_audioManager.PlayBGM(_config.mainMenuBGM, fadeOut: 0.5f, fadeIn: 1.0f);
|
||||
break;
|
||||
case GameState.BossFight:
|
||||
// BGMController 不直接切换,等待 OnBossFightToggled
|
||||
break;
|
||||
case GameState.Dead:
|
||||
_audioManager.TransitionToSnapshot("Dead", 1.5f);
|
||||
break;
|
||||
case GameState.Paused:
|
||||
_audioManager.TransitionToSnapshot("Paused", 0.2f);
|
||||
break;
|
||||
case GameState.Gameplay:
|
||||
_audioManager.TransitionToSnapshot("Default", 0.3f);
|
||||
PlayRegionBGM(_currentRegion);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void OnRegionEntered(string regionId)
|
||||
{
|
||||
_currentRegion = regionId;
|
||||
if (_musicState == MusicState.Exploration)
|
||||
PlayRegionBGM(regionId);
|
||||
}
|
||||
|
||||
void OnBossFightToggled(bool started)
|
||||
{
|
||||
if (started)
|
||||
{
|
||||
_musicState = MusicState.Boss;
|
||||
_audioManager.PlayBGM(_config.GetBossBGM(_currentRegion), fadeOut: 1f, fadeIn: 0.5f);
|
||||
_audioManager.TransitionToSnapshot("BossFight", 0.5f);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Boss 击败:播放胜利 Sting,然后恢复探索 BGM
|
||||
StartCoroutine(PlayVictoryThenRestore());
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator PlayVictoryThenRestore()
|
||||
{
|
||||
_musicState = MusicState.Victory;
|
||||
_audioManager.PlayBGM(_config.victoryStingBGM, fadeOut: 0.3f, fadeIn: 0.1f);
|
||||
yield return new WaitForSecondsRealtime(_config.victoryStingDuration);
|
||||
_musicState = MusicState.Exploration;
|
||||
PlayRegionBGM(_currentRegion);
|
||||
_audioManager.TransitionToSnapshot("Default", 1.0f);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 音乐状态机
|
||||
|
||||
```
|
||||
MusicState 枚举:
|
||||
Exploration ← 默认:区域探索 BGM
|
||||
Boss ← Boss 战:Boss 主题 BGM
|
||||
Victory ← Boss 击败后短暂胜利音乐
|
||||
None ← 过场/死亡/主菜单时由 BGMController 直接切换
|
||||
```
|
||||
|
||||
**状态转换**:
|
||||
|
||||
```
|
||||
Exploration ──[OnBossFightToggled(true)]──► Boss
|
||||
◄─[OnBossFightToggled(false)]──
|
||||
Boss ──[Boss 击败动画完成]──────────► Victory
|
||||
Victory ──[VictorySting 播放完毕]───────► Exploration
|
||||
Exploration ──[OnRegionEntered]────────────► Exploration(切换同状态内不同曲目)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. AudioZone — 区域音乐触发
|
||||
|
||||
`AudioZone` 挂载在每个区域的入口触发器上,通知 BGMController 切换对应 BGM:
|
||||
|
||||
```csharp
|
||||
public class AudioZone : MonoBehaviour
|
||||
{
|
||||
[SerializeField] string _regionId; // 如 "Forest", "Cave", "Ruins"
|
||||
[SerializeField] StringEventChannelSO _onRegionEntered;
|
||||
|
||||
void OnTriggerEnter2D(Collider2D other)
|
||||
{
|
||||
if (other.CompareTag("Player"))
|
||||
_onRegionEntered.Raise(_regionId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**区域 BGM 配置**在 `AudioConfigSO` 中(见 §10),`BGMController` 通过 regionId 查表获取对应 AudioClip。
|
||||
|
||||
---
|
||||
|
||||
## 7. AudioEventSO — SFX 集成
|
||||
|
||||
`AudioEventSO` 已在 [07_FeedbackSystem.md](./07_FeedbackSystem.md) 中定义,本系统扩展其与 AudioMixer 的集成:
|
||||
|
||||
```csharp
|
||||
[CreateAssetMenu(menuName = "Audio/AudioEvent")]
|
||||
public class AudioEventSO : ScriptableObject
|
||||
{
|
||||
[SerializeField] AudioClip[] _clips; // 随机选取
|
||||
[SerializeField] AudioMixerGroup _mixerGroup; // 指定路由到哪个子混音组
|
||||
[Range(0f, 1f)]
|
||||
[SerializeField] float _baseVolume = 1f;
|
||||
[SerializeField] Vector2 _pitchRange = new(0.9f, 1.1f); // 随机 Pitch 范围
|
||||
|
||||
public void Play(AudioSource source)
|
||||
{
|
||||
if (_clips.Length == 0) return;
|
||||
source.outputAudioMixerGroup = _mixerGroup;
|
||||
source.clip = _clips[Random.Range(0, _clips.Length)];
|
||||
source.volume = _baseVolume;
|
||||
source.pitch = Random.Range(_pitchRange.x, _pitchRange.y);
|
||||
source.Play();
|
||||
}
|
||||
|
||||
public void PlayOneShot(AudioSource source)
|
||||
{
|
||||
if (_clips.Length == 0) return;
|
||||
source.outputAudioMixerGroup = _mixerGroup;
|
||||
float pitch = Random.Range(_pitchRange.x, _pitchRange.y);
|
||||
source.pitch = pitch;
|
||||
source.PlayOneShot(_clips[Random.Range(0, _clips.Length)], _baseVolume);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Feel 的 `MMF_AudioSource` 持有 `AudioEventSO` 引用,调用 `Play()` 即可。
|
||||
|
||||
---
|
||||
|
||||
## 8. GlobalSFXPlayer
|
||||
|
||||
对于不依附于具体 GameObject 的一次性 SFX(如 UI 按钮音效),使用 `GlobalSFXPlayer`:
|
||||
|
||||
```csharp
|
||||
// AudioManager 的便捷方法(内部持有专用 AudioSource)
|
||||
public void PlaySFXGlobal(AudioEventSO audioEvent)
|
||||
=> audioEvent.PlayOneShot(_globalSFXSource);
|
||||
```
|
||||
|
||||
UI 按钮的 `onClick.AddListener(() => AudioManager.Instance.PlaySFXGlobal(btnClickSFX))`。
|
||||
|
||||
---
|
||||
|
||||
## 9. 音频资产规范
|
||||
|
||||
### 文件路径
|
||||
|
||||
```
|
||||
Assets/Audio/
|
||||
├── BGM/
|
||||
│ ├── BGM_MainMenu.ogg
|
||||
│ ├── BGM_Forest_Exploration.ogg
|
||||
│ ├── BGM_Cave_Exploration.ogg
|
||||
│ ├── BGM_Ruins_Exploration.ogg
|
||||
│ ├── BGM_Boss_Forest.ogg
|
||||
│ ├── BGM_Boss_Cave.ogg
|
||||
│ └── BGM_Victory_Sting.ogg
|
||||
├── SFX/
|
||||
│ ├── Player/
|
||||
│ │ ├── SFX_Player_Jump.wav
|
||||
│ │ ├── SFX_Player_Dash.wav
|
||||
│ │ ├── SFX_Player_Attack_01/02/03.wav (连击三段各一个)
|
||||
│ │ ├── SFX_Player_Hurt.wav
|
||||
│ │ ├── SFX_Player_Death.wav
|
||||
│ │ └── SFX_Parry_Success.wav
|
||||
│ ├── Enemy/
|
||||
│ │ ├── SFX_Enemy_Hit.wav
|
||||
│ │ ├── SFX_Enemy_Death.wav
|
||||
│ │ └── SFX_Enemy_Alert.wav
|
||||
│ ├── World/
|
||||
│ │ ├── SFX_Collectible_Geo.wav
|
||||
│ │ ├── SFX_SavePoint_Activate.wav
|
||||
│ │ ├── SFX_Door_Open.wav
|
||||
│ │ └── SFX_Platform_Crumble.wav
|
||||
│ └── UI/
|
||||
│ ├── SFX_UI_Confirm.wav
|
||||
│ ├── SFX_UI_Cancel.wav
|
||||
│ └── SFX_UI_Navigate.wav
|
||||
└── Ambient/
|
||||
├── AMB_Forest_Wind.ogg
|
||||
├── AMB_Cave_Drip.ogg
|
||||
└── AMB_Ruins_Echo.ogg
|
||||
```
|
||||
|
||||
### 音频导入设置规范
|
||||
|
||||
| 类型 | 格式 | Load Type | 说明 |
|
||||
|------|------|-----------|------|
|
||||
| BGM | `.ogg` | Streaming | 大文件流式读取,节省内存 |
|
||||
| SFX(短促)| `.wav` | Decompress on Load | 最低延迟,内存占用接受 |
|
||||
| SFX(循环环境)| `.ogg` | Compressed in Memory | 中等大小,压缩存储 |
|
||||
| Ambient | `.ogg` | Streaming | 长音频流式读取 |
|
||||
|
||||
---
|
||||
|
||||
## 10. AudioConfigSO
|
||||
|
||||
所有 BGM 和关键 SFX 引用集中配置,避免硬编码引用:
|
||||
|
||||
```csharp
|
||||
[CreateAssetMenu(menuName = "Config/AudioConfig")]
|
||||
public class AudioConfigSO : ScriptableObject
|
||||
{
|
||||
[Header("BGM")]
|
||||
public AudioClip mainMenuBGM;
|
||||
public AudioClip victoryStingBGM;
|
||||
public float victoryStingDuration = 3f;
|
||||
|
||||
[Header("Region BGMs")]
|
||||
public RegionBGM[] regionBGMs; // RegionBGM { string regionId; AudioClip clip; AudioClip bossBGM; }
|
||||
|
||||
[Header("Fade Settings")]
|
||||
public float defaultFadeOut = 1.0f;
|
||||
public float defaultFadeIn = 1.0f;
|
||||
|
||||
public AudioClip GetRegionBGM(string regionId)
|
||||
=> Array.Find(regionBGMs, r => r.regionId == regionId)?.clip;
|
||||
|
||||
public AudioClip GetBossBGM(string regionId)
|
||||
=> Array.Find(regionBGMs, r => r.regionId == regionId)?.bossBGM;
|
||||
}
|
||||
```
|
||||
|
||||
资产路径:`Assets/ScriptableObjects/Config/AudioConfigSO.asset`
|
||||
|
||||
---
|
||||
|
||||
## 11. SettingsManager — 音量持久化
|
||||
|
||||
设置独立于存档系统,写入 `Application.persistentDataPath/settings.json`:
|
||||
|
||||
```csharp
|
||||
[Serializable]
|
||||
public class SettingsData
|
||||
{
|
||||
public float masterVolume = 1.0f;
|
||||
public float bgmVolume = 0.8f;
|
||||
public float sfxVolume = 1.0f;
|
||||
public float ambientVolume = 0.6f;
|
||||
public bool haptics = true;
|
||||
// P1: keybindings[]
|
||||
}
|
||||
|
||||
public class SettingsManager : MonoBehaviour
|
||||
{
|
||||
public void Initialize()
|
||||
{
|
||||
_data = Load() ?? new SettingsData();
|
||||
Apply(_data);
|
||||
}
|
||||
|
||||
public void Apply(SettingsData data)
|
||||
{
|
||||
_audioManager.SetVolume("MasterVolume", data.masterVolume);
|
||||
_audioManager.SetVolume("BGMVolume", data.bgmVolume);
|
||||
_audioManager.SetVolume("SFXVolume", data.sfxVolume);
|
||||
_audioManager.SetVolume("AmbientVolume",data.ambientVolume);
|
||||
}
|
||||
|
||||
public void Save() => File.WriteAllText(_savePath, JsonUtility.ToJson(_data));
|
||||
SettingsData Load() => File.Exists(_savePath) ? JsonUtility.FromJson<SettingsData>(File.ReadAllText(_savePath)) : null;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. 事件频道
|
||||
|
||||
新增频道(`Assets/ScriptableObjects/Events/Audio/`):
|
||||
|
||||
| 资产名 | 类型 | 用途 |
|
||||
|--------|------|------|
|
||||
| `OnRegionEntered.asset` | `StringEventChannelSO` | AudioZone 触发,传递 regionId |
|
||||
| `OnBGMChangeRequested.asset` | `StringEventChannelSO` | P1:显式请求切换 BGM(过场等)|
|
||||
|
||||
> `OnBossFightToggled.asset`、`OnGameStateChanged.asset` 在其他频道组已定义,BGMController 直接订阅。
|
||||
|
||||
---
|
||||
|
||||
## 13. 编辑器友好设计
|
||||
|
||||
- `AudioManager` Custom Inspector:实时显示当前 BGM 名称、Source A/B 音量、当前 Snapshot
|
||||
- AudioConfigSO 提供 `[播放预览]` 按钮(Editor Only),在 Inspector 中点击可直接预听对应 BGM/SFX(UI Toolkit `Button`,`CreateInspectorGUI()` 中添加)
|
||||
- `AudioZone` Gizmo:在 Scene View 显示音频区域范围(半透明绿色圆圈 + regionId 文字标签)
|
||||
- `BGMController` Inspector:显示当前 `MusicState` 枚举状态 + 当前区域 ID
|
||||
|
||||
---
|
||||
|
||||
## 14. 脚步声材质分层(Footstep Material System)
|
||||
|
||||
脚步声不使用单一 SFX,而是根据脚下地面材质动态切换,增加环境真实感。
|
||||
|
||||
### 14.1 地面材质枚举
|
||||
|
||||
```csharp
|
||||
public enum FootstepMaterial
|
||||
{
|
||||
Stone, // 石板地(默认)
|
||||
Dirt, // 泥土/草地
|
||||
Wood, // 木板
|
||||
Metal, // 金属格栅
|
||||
Water, // 浅水区(溅水声)
|
||||
Sand, // 沙地(细碎声)
|
||||
Grass, // 草丛(沙沙声)
|
||||
Cave, // 洞穴(回响加强)
|
||||
}
|
||||
```
|
||||
|
||||
### 14.2 FootstepAudioConfigSO
|
||||
|
||||
```csharp
|
||||
[CreateAssetMenu(menuName = "BaseGames/Audio/FootstepAudioConfig")]
|
||||
public class FootstepAudioConfigSO : ScriptableObject
|
||||
{
|
||||
[System.Serializable]
|
||||
public struct MaterialEntry
|
||||
{
|
||||
public FootstepMaterial material;
|
||||
public AudioClip[] clips; // 随机选一个,防止重复感
|
||||
[Range(0f, 1f)] public float volume;
|
||||
[Range(0.8f, 1.2f)] public float pitchVariance; // 每次随机 pitch 偏移范围
|
||||
}
|
||||
|
||||
public MaterialEntry[] entries;
|
||||
}
|
||||
```
|
||||
|
||||
### 14.3 地面标记
|
||||
|
||||
在 Tilemap 的每个 Tile 上(或碰撞体所在 GameObject 上)添加 `FootstepMaterialMarker` 组件:
|
||||
|
||||
```csharp
|
||||
public class FootstepMaterialMarker : MonoBehaviour
|
||||
{
|
||||
public FootstepMaterial material;
|
||||
}
|
||||
```
|
||||
|
||||
玩家落地/行走时,通过当前碰撞的地面 GameObject 获取 `FootstepMaterialMarker.material`,驱动脚步声选择。若地面无标记,默认使用 `Stone`。
|
||||
|
||||
### 14.4 播放时机
|
||||
|
||||
- **落地**:`PlayerController.OnLanded()` 播放较响的落地音效(同一 MaterialEntry,但音量×1.5)
|
||||
- **行走**:Animancer 动画事件(`FootstepL` / `FootstepR` event tag)触发脚步声播放
|
||||
- **冲刺起步**:`Dash` 动画第2帧触发 `DashSFX`(不用 Footstep,用单独 SFX)
|
||||
|
||||
---
|
||||
|
||||
## 15. 水下音效处理(Underwater Audio)
|
||||
|
||||
进入 `LiquidZone`(见 40_LiquidSwimSystem)时,全局音效自动应用水下 DSP 处理。
|
||||
|
||||
### 15.1 水下 AudioMixer Snapshot
|
||||
|
||||
```
|
||||
Snapshot: "Underwater"
|
||||
BGM Bus: Low-Pass Filter 切割频率 800 Hz(水下声音沉闷)
|
||||
SFX Bus: Low-Pass Filter 1200 Hz + Volume ×0.7
|
||||
Ambient Bus: Volume ×0 + 替换为水下环境音(气泡声)
|
||||
Reverb Bus: Room Size 增大,Decay Time 1.8s(水下混响长)
|
||||
PlayerSFX Bus: Low-Pass Filter 1000 Hz(攻击声变闷)
|
||||
```
|
||||
|
||||
```csharp
|
||||
// 进入/退出液体区域时过渡
|
||||
public class UnderwaterAudioController : MonoBehaviour
|
||||
{
|
||||
[SerializeField] AudioMixer _mixer;
|
||||
[SerializeField] float _transitionDuration = 0.3f;
|
||||
|
||||
public void EnterWater()
|
||||
{
|
||||
_mixer.FindSnapshot("Underwater")
|
||||
.TransitionTo(_transitionDuration);
|
||||
// 同时降低 BGM 音量,增强代入感
|
||||
}
|
||||
|
||||
public void ExitWater()
|
||||
{
|
||||
_mixer.FindSnapshot("Default")
|
||||
.TransitionTo(_transitionDuration);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 15.2 水下专属 SFX
|
||||
|
||||
| 动作 | 水上 SFX | 水下 SFX |
|
||||
|------|---------|---------|
|
||||
| 攻击 | `sfx_player_slash` | `sfx_player_slash_underwater`(低频混响版)|
|
||||
| 跳跃(浮出水面)| — | `sfx_splash_exit` |
|
||||
| 入水 | — | `sfx_splash_enter` |
|
||||
| 游泳移动 | — | `sfx_swim_loop`(循环,随速度调整 pitch)|
|
||||
|
||||
---
|
||||
|
||||
## 16. 距离衰减曲线(Distance Attenuation)
|
||||
|
||||
所有世界空间中的 3D 音源(非 2D UI 音效)使用统一的衰减配置。
|
||||
|
||||
### 16.1 衰减模式
|
||||
|
||||
**游戏是 2D 横版卷轴,使用 `AudioSource.spatialBlend = 0`(纯2D)为主**,仅以下场景例外:
|
||||
|
||||
| 场景 | 实现方式 |
|
||||
|------|---------|
|
||||
| 大型场景中的远景环境音(瀑布/机械声)| `AudioSource.spatialBlend = 1.0`,自定义 Volume Rolloff 曲线 |
|
||||
| NPC 的近身对话语音 | `spatialBlend = 0.5`,增加位置感但不做完整 3D |
|
||||
| 远处 Boss 的警示音效(在门口听到 Boss 移动声)| `spatialBlend = 0.7`,最大距离 30 个世界单位 |
|
||||
|
||||
### 16.2 推荐衰减曲线参数(用于 3D 音源)
|
||||
|
||||
| 参数 | 值 |
|
||||
|------|---|
|
||||
| Min Distance | 2(2单位内全音量)|
|
||||
| Max Distance | 25(超过25单位静音)|
|
||||
| Rolloff Mode | Custom Curve(非线性,靠近时迅速增大,见下方)|
|
||||
| Volume Rolloff | 2~5: ×1.0 → 5~15: ×0.6 → 15~25: ×0.2 → >25: ×0 |
|
||||
|
||||
```
|
||||
Volume
|
||||
1.0 |───╮
|
||||
| ╲
|
||||
0.6 | ╲──╮
|
||||
| ╲
|
||||
0.2 | ╲──╮
|
||||
0.0 | ╲────
|
||||
+─────────────────────> Distance
|
||||
0 5 10 15 20 25
|
||||
```
|
||||
|
||||
### 16.3 Reverb Zone(区域混响)
|
||||
|
||||
每个主区域在 Unity 的 `AudioReverbZone` 组件中配置环境混响:
|
||||
|
||||
| 区域 | ReverbPreset | 额外调整 |
|
||||
|------|-------------|---------|
|
||||
| 森林 | `Forest` | DecayTime 0.8s |
|
||||
| 地穴 | `Cave` | DecayTime 2.0s,高频较少 |
|
||||
| 废墟 | `Room` | DecayTime 1.2s |
|
||||
| 深渊 | `Cave`(更大)| DecayTime 3.5s,Bass 增强 |
|
||||
| 核心 | `Hallway` | DecayTime 1.8s,金属感 |
|
||||
Reference in New Issue
Block a user