从”普通程序员能听懂”到”能改 UE 音频源码”——基于源码逐行考证:空间音频的原理、使用、
玩法结合、优化、封装
五条主线全覆盖

  • 文档版本:1.0(2026-08-26)
  • 源码快照:本地仓库 d:\Project\GameDevelop\UnrealEngine,UE 5.9 移动优化 fork,HEAD 6cea9bd20f8f
  • 写作原则:文中所有”源码考古”块引用的文件路径、行号、函数名均在本仓库中实际验证过;行号只用于定位,请以 路径:函数名 为准
  • 姊妹文档:本文是《Nanite》《VSM》《UE 网络架构》之后的第四篇——前两篇是 GPU 侧单进程,网络篇是 CPU 侧跨进程,本篇是 CPU 侧单进程 + 每音频帧 10ms 级实时循环。无需先读前三篇,但第 10 章(玩法结合)会引用网络文档的 RPC 章节
  • 符号说明Attenuation=衰减,Occlusion=遮挡,Reverb=混响,Concurrency=并发,Spatialization=空间化,Panning=声像(扬声器声像),Listener=听者,Ducking=避让;Submix/Bus/HRTF/ITD/ILD/Ambisonics/FOA/Focus 不翻译

第 0 章 导读:这份文档怎么读

TL;DR|这份文档把 UE 空间音频拆成五条主线讲透:原理(人耳如何定位声音)、机制
(一个声音从游戏代码到耳朵的完整旅程)、使用(从零配置一个 3D 声源)、玩法结合
(事件驱动/多人同步/环境区域)、优化与封装(并发预算/流送/音频管家模式)。
心智模型:空间音频 = 衰减(距离感)+ 空间化(方向感)+ 遮挡(隔墙感)+ 混响(空间感)
四件套的合奏
。三个深度档位:30 秒看懂、半天学会用、1 天读源码。

0.1 目标读者与前置知识

目标读者是普通程序员:会写 C++ 或蓝图、用过 UE 的场景与组件、想给自己的游戏加上”听声辨位”级别的音频体验。每个音频术语第一次出现都配一句直觉解释。

前置知识三个:

  1. C++/蓝图与 Actor 组件:声音挂在场景里(AudioComponent),由代码触发;
  2. 向量与距离:衰减/遮挡全是”声源与听者的距离/方向”计算;
  3. 实时循环:音频线程以 10ms 级周期处理声音——理解”每帧几十个声音 × 每个几毫秒 DSP”的预算思维。

本文的特别之处:前四篇只讲”引擎如何工作”,本篇额外回答”项目里怎么用、怎么管“——第 9~12 章的”动手实验室”块是新增的配方型内容。

0.2 三条阅读路径

通道 读什么 耗时 读完能做什么
🚀 30 秒通道 每章 TL;DR + 第 2 章九类比 + 图 F1 10 分钟 跟人讲清空间音频四件套与游戏音频预算
📚 标准通道 TL;DR + 正文 + 第 9~12 章(四大重点) 6~8 小时 能配衰减/遮挡/混响、能写事件驱动音频、能做并发预算、能搭 AudioManager
🔬 源码考古通道 全部 + 源码考古块 + 附录 A 对照 1~2 天 敢打开 Engine/Private/ 对照着读,能定位任意音频子系统

0.3 块标记约定

与前三篇一致,新增一种:

  • **TL;DR**(每章开头):30 秒读完本章,3~5 句、必须含类比名、不含源码路径;
  • **类比实验室**(正文小节):用普通程序员已懂的东西讲陌生概念;
  • **源码考古**(每章末尾,折叠块):贴真实源码(≤25 行/段),逐行翻译;
  • **避坑**(正文穿插):版本差异、已删除类、公开资料不一致;
  • **动手实验室**(第 9~12 章新增):配方型内容——资产配置步骤/代码骨架/验收标准,给”想马上用起来”的读者。

0.4 一张图看懂全局

图 F1 声音的完整旅程(从游戏代码到耳朵;贯穿全文的主干图)
游戏线程(GameThread) PlaySoundAtLocation / Play SoundIsAudible 距离门控 并发检查(Concurrency) 构造 FActiveSound AddNewActiveSound(入队) 并发管理器(第 8 章) 偷声/避让/预算截断 音频线程(AudioThread) FAudioDevice::Update(每帧) GetSortedActiveWaveInstances(排序) UpdateWaveInstances UpdateAttenuation(衰减/遮挡/混响求值) Parse(生成 FWaveInstance) StartSources → new FMixerSource SourceVoice(绑定音频流) 音频渲染线程(每 callback 10ms 级) FMixerSourceManager::ComputeNextBlock 逐源链:解码 → 衰减 → 滤波 → 空间化 Panning 声像 / HRTF 插件(第 5 章) 遮挡 LPF / 混响发送(第 6/7 章) 子混音图(MasterSubmix→Reverb…) 总线/效果链/混响 DSP 最终混合 → 平台输出(扬声器/耳机) 听者(Listener)与空间输入 监听器位置/朝向(通常绑玩家 Pawn) 音量区域(AAudioVolume 混响/遮挡优先级) Focus 聚焦参数("音频变焦") 音量分层(SoundClass 树 + SoundMix) SFX / 音乐 / 语音 独立推子 环境声(AmbientSound / 循环声) 优化(第 11 章) 并发预算 / 流送 / 虚拟循环 / 移动端

听者移动/音量区域变化 → 下一音频帧重新求值(UpdateAttenuation)

第 3 章
第 3/4 章
第 57 章
第 912 章

读图方法:一个声音从游戏代码出发,过三道门(距离门控 → 并发检查 → 入队),在音频线程被求值(衰减/遮挡/混响算好”该多响、多闷、多少回声”),在渲染线程被逐源处理(解码 → 滤波 → 空间化 → 混音),最终被听者”接收”。四件套(衰减/空间化/遮挡/混响)在音频线程求值、渲染线程执行——这是全书的骨架。

0.5 章节地图

章节 一句话
开篇 第 0~1 章 怎么读 + 人耳如何定位声音(四要素)
概念篇 第 2 章 九大概念速览(每个一个类比)
机制篇 第 3~8 章 播放链路 → 衰减 → 空间化 → 遮挡 → 混响 → 并发
使用篇 第 9 章 从零配置一个 3D 声源(重点)
玩法篇 第 10 章 事件驱动 / 多人同步 / 环境区域(重点)
优化篇 第 11 章 并发预算 / 流送 / 虚拟循环 / 移动端(重点)
封装篇 第 12 章 AudioManager 设计模式 / 数据驱动(重点)
调优篇 第 13~14 章 CVar 速查与常见坑 / 局限与展望
附录 A~D 源码地图、术语表、参考资料、自测题

第 1 章 空间音频是什么:人耳如何定位声音

TL;DR|空间音频的目标是”让玩家凭耳朵知道声音在哪“。人耳用四要素定位:ITD
(双耳时间差,低频)/ ILD(双耳响度差,高频)/ 耳廓滤波(前后上下)/ 转头动态线索
游戏把这四要素拆成四件套衰减管距离感、空间化管方向感、遮挡管隔墙感、
混响管空间感。类比:人耳是立体声雷达,游戏是雷达的模拟器

1.1 人耳定位四要素

线索 全称 原理 频段 游戏实现
ITD Interaural Time Difference 声音先到近耳(时间差 ~0.6ms 量级) 低频(波长长,相位差可辨) 空间化(Panning 近似 + HRTF 插件精确模拟)
ILD Interaural Level Difference 近耳更响(头部遮挡高频) 高频(头影效应) 声像增益差(EqualPower/Linear)
耳廓滤波 Pinna Filtering 外耳形状对特定角度滤波 全频段 仅 HRTF 插件能模拟(本 fork 无内置)
动态线索 Head Movement 转头时线索变化 听者朝向(Listener 旋转)

为什么”前后与上下最难分”:同一距离球面上的点,ITD/ILD 相同——所以”正前方 vs 正后方”在双耳线索上几乎等价。游戏里常见解决:轻微增益差异 + 混响差异 + 玩家转头

1.2 两条出路:耳机与扬声器

  • 耳机:双声道直达双耳,天然适合 ITD/ILD/HRTF——双耳渲染(Binaural)的舞台;
  • 扬声器:左扬声器会响进右耳(串音),需要交叉串音消除(Crosstalk Cancellation)才能模拟双耳效果——游戏里通常退化为声像(Panning):把声音按方位角分配到最近的两个扬声器。

UE 的默认路径:扬声器声像(Panning,引擎内置)为兜底,双耳渲染(HRTF)完全交给插件(第 5 章详述)——这是本 fork 的架构事实。

1.3 游戏音频四问题 ↔ UE 四件套(全书总纲)

图 F2 四件套总纲:玩家听到的每个"空间感"都由一个子系统负责
 玩家的问题                          引擎的答案                   章节
 ┌───────────────────┐   ┌──────────────────────────────┐
 │ "它离我多远?"      │ → │ Attenuation 衰减(距离→音量曲线) │ Ch4
 │ "它在哪个方向?"    │ → │ Spatialization 空间化(方位→声道)│ Ch5
 │ "中间有堵墙?"      │ → │ Occlusion 遮挡(射线+低通+音量)  │ Ch6
 │ "我在什么房间?"    │ → │ Reverb 混响(回声指纹)           │ Ch7
 └───────────────────┘   └──────────────────────────────┘
 外加两个全局系统:
 │ "太多声音了怎么办?" │ → │ Concurrency 并发(占线/偷声)     │ Ch8
 │ "怎么管理全部声音?" │ → │ SoundClass/Mix 分层(调音台推子) │ Ch9

记住这张表——后文所有章节都是它的展开。

1.4 UE 实现路径五站预告

1
2
3
4
5
① 游戏代码(PlaySoundAtLocation)          —— 事件出口(Ch3/Ch9)
② 距离门控 + 并发检查 —— 进不进场(Ch8)
③ 音频线程求值(衰减/遮挡/混响参数) —— 该多响多闷(Ch4/6/7)
④ 渲染线程执行(解码→滤波→空间化→混音) —— 变成电信号(Ch5/6/7)
⑤ 听者接收(Listener) —— 耳朵(Ch9/10)

1.5 一点历史与本 fork 事实

  • UE4 早期:平台专属音频(XAudio2/OpenAL 等 ~95% 平台差异代码);
  • AudioMixer(4.20+):统一跨平台渲染器(Engine/Source/Runtime/AudioMixer/),平台层只剩设备查询/缓冲提交;
  • 本 fork(5.9):无内置 HRTF/ITD DSP(双耳渲染全靠插件,见 au.EnableBinauralAudioForAllSpatialSoundsAudioDevice.cpp:121);fork 对音频零改动(git 验证)。

避坑:网上很多文章会教你”打开 HRTF”——本 fork 里没有内置 HRTFbSpatialize + SPATIALIZATION_HRTF 只是”请求走插件路径”,必须启用空间化插件(项目插件里开
Windows Spatial Audio / ResonanceAudio / Oculus 等)才有双耳效果。第 5.4 章详述。

1.6 小结

这一章记住三句话:

  1. 人耳四要素:ITD / ILD / 耳廓滤波 / 动态线索;
  2. 游戏四件套:衰减(距离)/ 空间化(方向)/ 遮挡(障碍)/ 混响(空间);
  3. 耳机走双耳渲染(插件),扬声器走声像(引擎内置)。

下一章,九大概念各配一个类比。

第 2 章 九大核心概念速览(每个概念 = 一个类比)

TL;DR|UE 音频的九个核心概念,用九个你早就懂的东西类比:衰减 = 音量地图
空间化 = 把声音放进耳朵遮挡 = 隔墙听声混响 = 房间的回声指纹
并发 = 电话占线SoundClass = 调音台推子AudioBus = 音频接线板
虚拟循环 = 远处声音放假AudioManager = 音频管家。这一章只立类比不给代码。

2.1 衰减(Attenuation)= 声音的”音量地图”

定义:声源与听者距离 → 音量的映射曲线。FSoundAttenuationSettingsSoundAttenuation.h:148)定义距离算法(线性/对数/逆距离等六种)、形状(球/胶囊/盒/锥)与衰减距离。衰减范围同时也是”听不见就跳过”的性能参数(SoundIsAudible 门控)。

类比实验室:音量地图
想象手机地图的”声音图层”:离声源越近音量越大,曲线形状决定了”多快变小声”。
线性 = 匀速衰减(直观);逆距离平方 = 真实物理(声音按距离平方衰减);
对数 = 可听距离长(远处还有声)。地图画多大,声音就能传多远——超出地图边界
的声音引擎根本不创建
(省 CPU)。

2.2 空间化(Spatialization)= 把声音”放”进耳朵里

定义:方位角(声源相对听者头部的方向)→ 左右声道增益的转换。默认走声像(Panning):方位角在相邻扬声器间插值(Get3DChannelMapAudioMixerDevice.cpp:2915);走插件则用 HRTF 模拟完整双耳滤波。

类比实验室:舞台上的音箱
声像是”舞台上的两只音箱”:声音在左音箱响一点、右音箱轻一点,你的大脑就以为
声源在左边。HRTF 是”直接塞进耳朵的耳机”:连”声音来自斜后方”这种细节都能模拟。
简单游戏用音箱(Panning)够了,听声辨位射击游戏才需要耳机(HRTF 插件)。

2.3 遮挡(Occlusion)= 隔墙听声

定义:声源与听者之间有墙时,声音变”闷”(低通滤波)变”轻”(音量衰减)。引擎用物理射线LineTraceTestByChannelActiveSound.cpp:1636)检测遮挡,结果平滑插值避免”咔哒”声。

类比实验室:隔墙听邻居
墙后的电视声:听得到”在响”但听不清”在说啥”(高频被墙吸收 = 低通滤波),
音量也小一截。遮挡就是把”墙”翻译成”低通 + 音量”两个旋钮。

2.4 混响(Reverb)= 房间的”回声指纹”

定义:声音在房间里的多次反射形成的尾音。引擎把混响发送到专用子混音(MasterReverbSubmixAudioMixerDevice.cpp:2082),用板式混响算法(FPlateReverbFast)模拟。

类比实验室:房间的回声指纹
山洞喊一声有长回声,卫生间拍手有清脆的短回声,客厅几乎没有——每个房间都有
自己的”回声指纹”(衰减时间、密度、高频吸收)。混响参数 = 给声音盖上房间的
指纹章。**玩家闭眼也能听出”我进山洞了”**,这就是玩法价值。

2.5 并发(Concurrency)= 电话占线

定义:同时播放的声音超过预算时的裁决规则。FSoundConcurrencySettingsSoundConcurrency.h:73)的 MaxCount + ResolutionRule(七种规则:阻止新的/停最老的/停最远的/停最低优先级的……)。

类比实验室:电话占线
同一时刻只能有 N 通电话(MaxCount)。第 N+1 通打进来怎么办?七种规则任选:
挂断新来的(PreventNew)、挂断最老的(StopOldest)、挂断最远的(StopFarthest)、
挂断最不重要的(StopLowestPriority)……还能”不挂断只调小”(Ducking 避让——
语音通话时把背景音乐调小,而不是挂断音乐)。

2.6 SoundClass = 调音台推子、SoundMix = 混音快照

定义USoundClassSoundClass.h:208)组成层级树(Master → SFX/音乐/语音/UI),每个推子管音量/音高/低通;USoundMixSoundMix.h:170)是”一组推子的预设快照”(进战斗时压低音乐、Boss 战时再加一层)。

类比实验室:调音台
游戏混音 = 电台调音台:主持人(语音)一档、背景音乐一档、音效一档,各有一个
推子。SoundMix 是”预录好的推子操作”:按一下”Boss 战”按钮,音乐推子自动
拉低 20%、音效推子抬高 10%,还带淡入淡出。

2.7 AudioBus = 音频接线板、MetaSound = 音频电路板

定义UAudioBusAudioBus.h:87,头注释原话 “An audio bus is an object which represents an audio patch cord”)是音频的”跳线”——声音送进总线,总线接进子混音;UMetaSoundSourceMetasoundSource.h:89)是程序化音频的 DSP 图——采样级实时计算(合成、调制、包络),与 SoundCue 的”波形编排”定位不同。

类比实验室:接线板与电路板
AudioBus = 电源接线板:所有设备插上去,统一供电(所有声音汇入总线统一处理)。
MetaSound = 自己焊电路板:不播放现成录音,而是实时”算”出声音——振荡器、
包络、滤波器按你连的线路发声。想合成引擎轰鸣?焊一块 MetaSound 电路板。

2.8 虚拟循环 = 远处声音”放假”、AudioManager = 音频管家

定义FAudioVirtualLoopAudioVirtualLoop.cpp)让远处循环声降频更新(更新间隔按距离在 0.1s~3.0s 插值)——声音还在逻辑上”播着”,但 CPU 不陪它每帧跑;”音频管理器”引擎没有内置(只有 FAudioDevice 设备层)——游戏自己封装(第 12 章)。

类比实验室:远处声音放假
远处的瀑布声:玩家听得到(音量小但存在),但引擎不会让它每帧精细计算——
放假模式:每 3 秒醒来检查一次”玩家靠近了吗?”,靠近了恢复全职工作。
100 个远处循环声 × 每帧计算 = CPU 灾难;100 个 × 每 3 秒一次 = 几乎免费。

2.9 概念 → 源码落点预告表

概念 类比 核心类/函数 章节
衰减 音量地图 FSoundAttenuationSettings / UpdateAttenuation Ch4
空间化 放进耳朵 Get3DChannelMap / IAudioSpatialization Ch5
遮挡 隔墙听声 CheckOcclusion(LineTraceTestByChannel) Ch6
混响 回声指纹 MasterReverbSubmix / FSubmixEffectReverbSettings Ch7
并发 电话占线 FSoundConcurrencySettings / EvaluateConcurrency Ch8
SoundClass/Mix 调音台推子 USoundClass / USoundMix / UGameplayStatics Ch9
AudioBus/MetaSound 接线板/电路板 UAudioBus / UMetaSoundSource Ch9
虚拟循环 远处放假 FAudioVirtualLoop Ch11
AudioManager 音频管家 FAudioDeviceManager / 游戏侧自建 Ch12

第 3 章 播放链路:一个声音的完整旅程

TL;DR|一个声音从游戏代码到耳朵走三线程四阶段:游戏线程触发(PlaySoundAtLocation
或 AudioComponent::Play → 距离门控 → 并发检查 → 入队)→ 音频线程求值(Update →
排序 → UpdateAttenuation 算出衰减/遮挡/混响参数 → Parse 生成波形实例 → new FMixerSource)
→ 渲染线程执行(每 10ms 级回调逐源处理:解码→滤波→空间化→混音→输出)→ 听者接收
类比:点外卖——下单(游戏线程)、厨房配菜(音频线程)、炒菜出餐(渲染线程)

3.1 两个入口:PlaySoundAtLocation vs AudioComponent::Play

UGameplayStatics::PlaySoundAtLocation UAudioComponent::Play
需要组件 否(纯静态函数) 是(场景组件)
生命周期 播完即毁(FActiveSound 自动清理) 跟随组件(可停止/淡出/换声)
参数控制 有限(音量/音高/衰减资产) 完整(SetVolumeMultiplier/FadeIn/参数接口)
适用 一次性事件(开火/拾取/爆炸) 持续声音(引擎/环境/对话)

两个入口最终都构造同一个 FActiveSound(活动的声音实例)并 AddNewActiveSound 入队——殊途同归

3.2 距离门控:听不见就不创建

FAudioDevice::PlaySoundAtLocationAudioDevice.cpp:6705)先调 SoundIsAudible(:6318,按衰减 MaxDistance 判定):听不见 → 直接返回,不创建实例

这是音频性能的第一道闸:世界里有 500 个声源事件,只有玩家周围的几十个真正创建了 FActiveSound。

3.3 FActiveSound:一个”活着的声音”

FActiveSoundActiveSound.h)是音频线程上的声音实例:记录声源位置、音量/音高倍率、衰减资产引用、并发组、SoundNode 实例状态等。它是音频系统的”灵魂对象”——游戏线程说”播这个声音”,音频线程操作的就是它。

3.4 音频线程四步:求值

FAudioDevice::UpdateAudioDevice.cpp:4725,音频线程每帧):

1
2
3
4
5
6
7
Update
→ GetSortedActiveWaveInstances(:4159):所有 WaveInstance 按音量×优先级排序
→ FActiveSound::UpdateWaveInstances(ActiveSound.cpp:1081):逐声音
→ UpdateAttenuation(:2053):衰减/遮挡/混响参数求值(第 4/6/7 章)
→ Sound->Parse(:1225):SoundCue 节点图展开 → 生成 FWaveInstance
→ StopSources / StartSources(AudioDevice.cpp:4890/:4541)
→ new FMixerSource(AudioMixerDevice.cpp:1546)→ FMixerSourceVoice(:2888)

FWaveInstance(波形实例)是”一个 FActiveSound 展开出的可播放流”——一个 SoundCue 可以展开出多个 WaveInstance(多节点同时响)。

3.5 渲染线程:逐源处理

FMixerDevice::OnProcessAudioStreamAudioMixerDevice.cpp:1599,每 10ms 级回调):

1
2
3
4
// Compute the next block of audio in the source manager
SourceManager->ComputeNextBlockOfSamples(); // 逐源:解码→衰减→滤波→空间化→send
...
MainSubmixPtr->ProcessAudio(Output); // 子混音图最终混合 → 平台输出

FMixerSourceManager::ComputeNextBlockOfSamplesAudioMixerSourceManager.cpp:3919)按 SourceId 逐个处理——每个声源每回调的 DSP 成本 = 解码 + 衰减增益 + 滤波器 + 空间化 + 混响发送,这是第 11 章性能预算的原子单位。

3.6 线程模型:GameThread / AudioThread / RenderThread

线程 干什么 同步方式
游戏线程 触发声音(Play/Stop)、改参数 FAudioThread::RunCommandOnAudioThreadAudioThread.h:58)命令队列
音频线程 FActiveSound 求值(衰减/遮挡/混响) 命令队列 + 事件
音频渲染线程 逐源 DSP + 混合输出 回调驱动(OnProcessAudioStream)

关键:游戏线程绝不直接碰音频数据——一切通过命令队列。所以”播放中改音量”是异步生效的(第 9 章的热更新机制)。

3.7 源码考古:PlaySoundAtLocation 全链

源码考古:想看真东西的人进。

① 静态入口GameplayStatics.cpp:1526(要点):

1
2
3
4
5
6
7
8
9
void UGameplayStatics::PlaySoundAtLocation(...)
{
FAudioDevice* AudioDevice = World->GetAudioDevice();
// 转发给音频设备;无设备(无音频)直接返回
if (AudioDevice)
{
AudioDevice->PlaySoundAtLocation(Sound, World, Location, Rotation, VolumeMultiplier, ...);
}
}

这段代码在干什么:蓝图/C++ 的音频静态入口是个薄转发——真正的逻辑全在
FAudioDevice。这也是”引擎把音频抽象成设备层”的体现:游戏代码不直接碰
平台音频 API。

② 设备层AudioDevice.cpp:6705(要点):

1
2
3
4
5
6
7
8
9
10
11
void FAudioDevice::PlaySoundAtLocation(...)
{
if (Sound == nullptr) return;
// 距离门控:听不见就不创建
if (!SoundIsAudible(Sound, World, Location, ...)) return;
// 构造活动声音实例
FActiveSound NewActiveSound;
NewActiveSound.Priority = Sound->Priority;
... // 填充位置/音量/衰减引用
AddNewActiveSound(MoveTemp(NewActiveSound), ...); // 入队到音频线程
}

这段代码在干什么:设备层的三步——空检查 → 距离门控 → 构造入队
SoundIsAudible 用衰减的 MaxDistance 算”玩家能不能听到”:听不到直接 return,
连 FActiveSound 都不建。这就是”500 个声源只有几十个真活着”的机制

③ 组件路径AudioComponent.cpp:499-673(要点):

1
2
3
4
5
6
7
8
void UAudioComponent::Play() { PlayInternal(...); }
void UAudioComponent::PlayInternal(...)
{
// 构造 FActiveSound 并填充:衰减(三级覆盖)、优先级(bOverridePriority)、
// 并发组、参数(SetFloatParameter 等)
...
AudioDevice->AddNewActiveSound(NewSharedActiveSound, ...);
}

这段代码在干什么:组件路径与静态路径的区别——组件携带完整上下文(衰减覆盖、
参数、生命周期),静态函数只带基本参数。**”需要控制的用组件,一次性的用静态函数”**
就是从这里来的。


第 4 章 衰减:声音的”音量地图”

TL;DR|衰减把”距离”翻译成”音量”:FSoundAttenuationSettings(改名自 FAttenuationSettings)
定义六种距离曲线(线性/对数/逆距离…)与四种形状(球/胶囊/盒/锥),FalloffDistance
是”多快衰减”,MaxDistance 同时是性能门控(听不见就跳过)。组件可以三级覆盖
(组件覆盖 > 资产 > 声音回退),播放中还能热更新。类比:音量地图——画多大、怎么
渐变,全在这张图上

4.1 结构与资产

  • FBaseAttenuationSettingsAttenuation.h:56):形状与曲线基类——DistanceAlgorithm(六种曲线)、AttenuationShape(四种形状)、FalloffDistanceCustomAttenuationCurve
  • FSoundAttenuationSettingsSoundAttenuation.h:148):在基类上加空间化/遮挡/混响开关(第 5/6/7 章的开关都在这里);
  • USoundAttenuation(:452):FSoundAttenuationSettings 的资产容器。

避坑FAttenuationSettings 是 UE 5.1 前的旧名——老教程的类名引用一律按
FSoundAttenuationSettings 处理。另外 FBaseAttenuationSettings 定义在
Classes/Engine/Attenuation.h(不是 Classes/Sound/ 下),源码地图容易找错。

4.2 六种距离曲线

DistanceAlgorithm 形状 适用
Linear 直线衰减 直观调试
Logarithmic 对数衰减(快) 短距离事件声
Inverse 逆距离(真实物理) 枪声/爆炸(近处巨响)
LogReverse 对数反向 远处仍有声
NaturalSound 自然声(带 dBAttenuationAtMax) 环境音
Custom 自定义曲线 精确控制

4.3 四种形状

形状 语义 适用
Sphere(球) 各向均匀 通用
Capsule(胶囊) 轴向拉长 走廊/道路
Box(盒) 矩形区域 房间
Cone(锥) 定向(声源朝向) 喇叭/枪口/对话

Cone 的玩法价值:对话只朝说话者前方传播(背后听不清)——这是”方向性”的一部分,与第 5 章的空间化互补(一个是音量、一个是声道)。

4.4 距离低通(空气吸收)

bAttenuateWithLPFSoundAttenuation.h:162)+ LPFRadiusMin/Max/LPFFrequencyAtMin/Max(:281-293):距离越远高频衰减越多(空气吸收低频穿透)——**远处的声音”闷”**,这是真实感的重要来源,也是遮挡之外第二个”低通”旋钮。

4.5 组件三级覆盖与热更新

UAudioComponent::GetAttenuationSettingsToApplyAudioComponent.cpp:1342):

1
2
3
① bOverrideAttenuation = true → 用组件的 AttenuationOverrides(结构体覆盖)
② 有 AttenuationSettings 资产 → 用资产
③ 都无 → 回退到 Sound 资产自带的衰减设置

热更新:播放中用 SetAttenuationOverrides 会通过 SendCommandToActiveSounds(:1367-1388)把新设置异步同步到正在播放的 FActiveSound——**”声音播着改衰减”是支持的**(比如角色进入水下把衰减整体调小)。

4.6 监听器与 Focus

  • 监听器SetListenerFocusParametersGameplayStatics.cpp:1390)——“音频变焦”:把注意力集中在目标上,其他声音降权(第 10 章的玩法应用);
  • 距离优先级衰减bEnablePriorityAttenuation(SoundAttenuation.h:383-401)——远处声音的优先级也降(与第 8 章并发联动)。

4.7 源码考古:UpdateAttenuation 求值

源码考古:想看真东西的人进。

ActiveSound.cpp:2053-2213(要点):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void FActiveSound::UpdateAttenuation(...)
{
// 形状求值:球走 AttenuationEval(快速路径),其他形状走 Evaluate(通用)
if (AttenuationSettings->AttenuationShape == EAttenuationShape::Sphere)
VolumeMultiplier *= AttenuationSettings->AttenuationEval(...);
else
VolumeMultiplier *= AttenuationSettings->Evaluate(Origin, Location, ...);

// 混响发送:按距离算 ReverbSendLevel(EReverbSendMethod)
// 优先级衰减:远处降 Priority(与并发联动)
// Focus:听者聚焦目标时调整
// 遮挡:CheckOcclusion(第 6 章)
...
}

这段代码在干什么衰减求值 = 把”距离”翻译成四个参数(音量倍率、混响发送量、
优先级、遮挡量)。注意球形状走专门的快速路径(AttenuationEval)——球是最常用
形状,引擎为它优化。这一函数是第 4/6/7 章的交汇点:衰减、遮挡、混响、优先级
都在这里算好,渲染线程只消费结果。


第 5 章 空间化:把声音”放”进耳朵里

TL;DR|空间化把”方位角”变成”左右耳的信号差”:默认走声像(Panning)——方位角在
相邻扬声器间插值(EqualPower 等功率/Linear 线性);双耳渲染(HRTF)完全交给插件
(本 fork 无内置 HRTF DSP)。立体声声源用 StereoSpread 在空间里分开左右发射器;
近处声音用 NonSpatializedRadius 转 2D 防晕。类比:舞台音箱(Panning)vs
直接塞进耳朵的耳机(HRTF)

5.1 从位置到方位角

FSoundSource::GetSpatializationParamsAudio.cpp:724)把声源世界坐标 + 听者位置/朝向 → 算出 EmitterPosition(声源相对听者的位置)、EmitterAzimuth(方位角)、Distance——空间化的全部输入

5.2 Panning:相邻扬声器插值

FMixerDevice::Get3DChannelMapAudioMixerDevice.cpp:2915):

1
2
3
4
5
6
7
8
9
10
11
12
// 方位角 → 相邻扬声器索引 + 插值比例
...
// 等功率(EqualPower):SinCos 插值,保证总功率恒定(声像移动不忽大忽小)
if (PanningMethod == EPanningMethod::EqualPower)
{
FMath::SinCos(&NextChannelPan, &PrevChannelPan, Fraction * 0.5f * PI);
}
else // Linear:线性插值(简单但声像移动时有"哑点")
{
NextChannelPan = Fraction;
PrevChannelPan = 1.0f - Fraction;
}

扬声器方位角表InitializeChannelAzimuthMapAudioMixerChannelMaps.cpp:147)——立体声:左声道 270°、右声道 90°(从听者头部朝向算);5.1/7.1 有各自的表。声像 = 按方位角在表里找相邻扬声器、按比例分增益

5.3 HRTF 路线:插件体系

IAudioSpatializationIAudioExtensionPlugin.h:245-388)是空间化插件接口:ProcessAudio(InputData, OutputData) 接收”单声道 + 位置参数”,输出”双声道”。什么时候走插件(FMixerSource::UseObjectBasedSpatializationAudioMixerSource.cpp:2337):

1
2
3
① 声道数 <= 插件支持上限(默认 1,可扩展 stereo)
② 空间化插件已启用(项目插件)
③ SpatializationMethod == SPATIALIZATION_HRTF(衰减设置里选)

本 fork 事实au.EnableBinauralAudioForAllSpatialSounds / au.DisableBinauralSpatializationAudioDevice.cpp:121/128)只是开关——没有内置 HRTF DSP,双耳渲染完全由插件(Windows Spatial Audio / ResonanceAudio / Oculus 等)提供。没装插件 = 选了 HRTF 也白选(回退 Panning)。

5.4 立体声 3D:StereoSpread

FSoundSource::UpdateStereoEmitterPositionsAudio.cpp:625):立体声声源的左右声道在空间里分开StereoSpread 决定分离宽度,叉积求左右偏移 :644-651),然后左右各算一次方位角——**立体声不是”两个单声道”,而是”空间里左右分开的两个发射器”**。

5.5 近距转 2D:NonSpatializedRadius

NonSpatializedRadiusStart/EndSoundAttenuation.h:254/258)→ NonSpatializedAmountAudio.cpp:739-779)→ Get3DChannelMapLerp(pan, omni, OmniAmount)(:3051-3055):声音离耳朵太近时,声像定位反而让人头晕(”声音贴脸转脑袋”)——近距转 2D 让声音”充满”双耳,防晕。

5.6 Ambisonics:环境声场

FWaveInstance::bIsAmbisonicsAudio.h:315):仅支持 FOA(一阶,4 声道);编码走球坐标转换(AudioMixerSourceOutputBuffer.cpp:385),DSP 在 SignalProcessing/DSP/Ambisonics.h,解码由 SoundFieldRendering 模块完成。用途:环境底床(风/雨/人群)——用 Ambisonics 录制的声场比一堆单声道声源更真实。

5.7 源码考古:Get3DChannelMap

源码考古:想看真东西的人进。

AudioMixerDevice.cpp:2915-3010(要点):

1
2
3
4
5
6
7
8
9
10
11
12
void FMixerDevice::Get3DChannelMap(const FWaveInstance* InWaveInstance, ...)
{
// 1. 从方位角表找相邻扬声器(表在 InitializeChannelAzimuthMap)
// 2. 算插值比例 Fraction(方位角在两扬声器之间的位置)
// 3. 等功率/线性插值出两扬声器的增益
if (PanningMethod == EPanningMethod::EqualPower)
{
FMath::SinCos(&NextChannelPan, &PrevChannelPan, Fraction * 0.5f * PI);
}
// 4. 非空间化半径混合:Lerp(pan, omni, OmniAmount)
// 5. 输出声道增益图(每个输出声道一个增益)
}

这段代码在干什么:声像的全部数学——方位角 → 相邻扬声器 → 增益对
EqualPower 的 SinCos 插值保证”声音从左到右移动时总响度不变”(线性插值在
45° 处会有-3dB 哑点)。这个函数是”空间化的默认实现”——HRTF 插件不存在时
它就是 UE 空间音频的全部。


第 6 章 遮挡:隔墙听声

TL;DR|遮挡让”墙后的声音”变(低通滤波)变(音量衰减):CheckOcclusion
物理射线(LineTraceTestByChannel)检测声源与听者之间是否有墙,每 0.1 秒查一次,
结果用插值平滑(防”咔哒”)。类比:隔墙听邻居的电视——听得到在响,听不清在说啥

6.1 双通道:闷 + 轻

遮挡的效果 = 两个旋钮(SoundAttenuation.h:345-353):

字段 作用
OcclusionLowPassFilterFrequency 低通频率(闷的程度)
OcclusionVolumeAttenuation 音量衰减(轻的程度)
OcclusionInterpolationTime 过渡时间(遮挡进出平滑)

6.2 检测:物理射线

FActiveSound::CheckOcclusionActiveSound.cpp:1599):

1
2
3
4
5
6
7
8
9
// 每 OcclusionCheckInterval(默认 0.1s)做一次射线检测
if (!bAsyncOcclusionPending && (bNeedsInitialOcclusionCheck || bOcclusionCheckDue))
{
LastOcclusionCheckTime = PlaybackTime;
FCollisionQueryParams Params(SCENE_QUERY_STAT(SoundOcclusion), bUseComplexCollisionForOcclusion);
if (OwnerID > 0) Params.AddIgnoredActor(OwnerID); // 忽略自己
bIsOccluded = WorldPtr->LineTraceTestByChannel(
SoundLocation, ListenerLocation, OcclusionTraceChannel, Params);
}

要点OcclusionTraceChannel(默认 Visibility 或 Camera)决定”什么算墙”;bUseComplexCollisionForOcclusion 决定用复杂碰撞还是简单盒;忽略属主 Actor(别被自己的身体挡住)。

6.3 异步路径与平滑

  • 异步au.UseAsyncOcclusionTraces=1ActiveSound.cpp:27)时走 AsyncLineTraceByChannel(:1659)——射线在物理线程算,结果回调后应用到音频(避免音频线程被物理拖住);
  • 平滑CurrentOcclusionFilterFrequency/CurrentOcclusionVolumeAttenuationOcclusionInterpolationTime 插值(:1674-1694)——**没有插值的话,声音进出墙会”咔哒咔哒”**;
  • 落地FMixerSource::UpdateVolumeSetDistanceAttenuationAudioMixerSource.cpp:1509)+ 低通 SetLPFFrequencyAudioMixerSourceManager.cpp:2053)。

6.4 避坑与性能

  1. 被自己/头顶遮挡:射线从声源位置到听者位置,如果声源在玩家自己身上(第一人称枪声),射线会被玩家自己的碰撞挡住——AddIgnoredActor(OwnerID) 解决了一半,另一半是把声源位置放在枪口而非玩家中心
  2. 射线频率:0.1s 一次是默认——遮挡剧烈的场景(墙多)可以调 OcclusionCheckInterval 或组件级覆盖;
  3. 关闭au.DisableOcclusion=1ActiveSound.cpp:20)——移动端性能兜底。

6.5 源码考古:CheckOcclusion 三件事

源码考古:想看真东西的人进。

ActiveSound.cpp:1599-1694(要点已在 6.2 展示)。补充插值段:

1
2
3
4
// 用插值时间平滑遮挡结果(防咔哒)
const float InterpTime = OcclusionInterpolationTime > 0.0f ? OcclusionInterpolationTime : 1.0f;
CurrentOcclusionFilterFrequency = FMath::FInterpTo(CurrentOcclusionFilterFrequency, TargetFrequency, DeltaTime, InterpTime);
CurrentOcclusionVolumeAttenuation = FMath::FInterpTo(CurrentOcclusionVolumeAttenuation, TargetVolume, DeltaTime, InterpTime);

这段代码在干什么:遮挡不是”开/关”而是”渐变”——FInterpTo 让低通频率和音量
平滑过渡到目标值。**”进墙 0.3 秒内声音逐渐变闷变轻”就是这里实现的**。注意
InterpTime 的兜底:设 0 时用 1 秒,防止除零。


第 7 章 混响与室内感

TL;DR|混响把”房间”翻译成”回声指纹”:声音按距离算出一个 ReverbSendLevel 发送到
MasterReverbSubmix(板式混响 FPlateReverbFast),AAudioVolume 按玩家所在的声学区域
覆盖混响参数(Priority 仲裁重叠区),FInteriorSettings 让室内外过渡带时间渐变。
类比:房间的回声指纹——山洞长回声、卫生间短回声、客厅没回声

7.1 混响是什么:早反射 + 晚反射

混响 = 声源停止后房间里的声音残留:早反射(前 50ms,几面墙的直接反射,给人”房间大小”感)+ 晚反射(多次反射的尾音,给”材质”感)。UE 用 Audio::FPlateReverbFast(板式混响算法)模拟。

7.2 发送链:从声源到混响子混音

1
2
3
4
5
6
bEnableReverbSend(SoundAttenuation.h:182)
→ UpdateAttenuation 按距离算 ReverbSendLevel(EReverbSendMethod: Linear/CustomCurve/Manual)
→ FMixerSource::UpdateEffects(AudioMixerSource.cpp:1636):
MixerSourceVoice->SetSubmixSendInfo(GetMasterReverbSubmix(), ReverbSendLevel);
→ MasterReverbSubmix(AudioMixerDevice.cpp:2082,默认资产 "MasterReverbSubmixDefault")
→ FSubmixEffectReverbSettings 驱动 DSP(DecayTime=1.49s/Density=0.85 等)

关键:混响是发送(Send)——原始声音直接进主混音,副本进混响子混音,混合后输出。WetLevel(干湿比)由 ReverbSendLevel 控制。

7.3 AAudioVolume:声学区域

AAudioVolumeAudioVolume.h:152)是声学区域(体积内生效):

成员 作用
Priority 重叠区域仲裁(高优先级胜出)
FReverbSettings(ReverbSettings.h:48) 混响参数(ReverbEffect/Volume/FadeTime)
FInteriorSettings(:75) 室内外音量/LPF 渐变
SubmixSendSettings 区域内子混音发送切换

FAudioDevice::UpdateAudioVolumeEffectsAudioDevice.cpp:4923)每帧按听者位置查询所在体积并插值。

避坑:老教程的 AReverbVolume(UE4 时代)在本 fork 已删除——功能并入
AAudioVolumeFReverbSettings 管混响、FInteriorSettings 管室内外、Priority
管仲裁)。UAudioTrigger 也已删除。见到这两个旧类名按”旧资料”处理。

7.4 室内外过渡

FActiveSound::HandleInteriorVolumesActiveSound.cpp:1708):声源与听者是否同体积 → InteriorVolume/ExteriorVolume/InteriorLPF 带时间渐变(InteriorTime/ExteriorTime)——**”出门瞬间声音从闷变亮”是渐变的,不是跳变的**。

7.5 房间”指纹”配方表(供第 10 章玩法用)

空间 DecayTime Density 效果
小房间/卫生间 0.3~0.6s 清脆短回声
大房间/大厅 1.0~1.5s 明显尾音
山洞/隧道 2.0~3.0s 长而空旷
室外(默认) 0.2s 几乎无回声

7.6 源码考古:发送点与室内外

源码考古:想看真东西的人进。

① 混响发送点AudioMixerSource.cpp:1636-1637

1
2
// Send the source audio to the master reverb
MixerSourceVoice->SetSubmixSendInfo(MixerDevice->GetMasterReverbSubmix(), WaveInstance->ReverbSendLevel);

这段代码在干什么:每个声源在渲染线程把自己的副本发送到混响子混音——
“发送量(ReverbSendLevel)”是音频线程在 UpdateAttenuation 算好的(按距离/曲线)。
**混响不改变原始声音,只叠加一个”回声副本”**。

② 室内外渐变ActiveSound.cpp:1708-1769(要点):

1
2
3
// 声源与听者是否在同一音量区域 → 决定用 InteriorVolume 还是 ExteriorVolume
// 带 InteriorTime/ExteriorTime 渐变(FInterpTo)
// 跨区时低通取更小者(最闷的规则生效)

这段代码在干什么:”室内外”不是开关而是两组渐变参数——进门时音量与低通
平滑过渡到室内值。跨区取”更闷”的规则保证从亮处看暗处时声音不会突然变亮。


第 8 章 并发与优先级:电话占线

TL;DR|混音器有预算(MaximumConcurrentStreams),超预算时要裁决:FSoundConcurrencySettings
MaxCount + 七种 ResolutionRule(阻止/停最老/停最远/停最低优先级…)+ 四种作用域
(Group/Owner/OwnerPerSound/Sound);Ducking 是”不挂断只调小”(语音压 BGM)。
全局兜底:GetVolumeWeightedPriority(音量×优先级)排序截断。类比:电话占线——
挂谁由规则定

8.1 问题:混音器预算

AudioSettings->MaximumConcurrentStreamsAudioSettings.h:199)是全局同时发声上限(默认约 32-128 视平台)。GetSortedActiveWaveInstances(AudioDevice.cpp:4159)按 GetVolumeWeightedPriority()Audio.h:482,音量 × 优先级)排序,超出预算的低优先级 WaveInstance 直接不启动。

8.2 FSoundConcurrencySettings:并发组

FSoundConcurrencySettingsSoundConcurrency.h:73):

字段 含义
MaxCount / PlatformMaxCount 组内最大并发数(平台缩放)
bLimitToOwner 按发声 Actor 限流(每人只能有 N 个脚步声)
ResolutionRule 七种裁决规则(见 8.3)
RetriggerTime 拒播后的冷却(防连点刷屏)
VolumeScale Ducking 音量(老声音避让)
VoiceStealReleaseTime 被偷声时的淡出时间(防咔哒)

8.3 七种裁决规则(决策表)

ResolutionRule 行为 适用
PreventNew 新声音直接拒播 关键声音保护(不打断)
StopOldest 停最老的 循环声(旧的停掉新的接上)
StopFarthestThenPreventNew 先停最远的,还满就拒新的 通用
StopFarthestThenOldest 停最远的(默认) 脚步声/枪声
StopLowestPriority 停最低优先级的 优先级体系完善时
StopQuietest 停最安静的 保持”响的声音”
StopLowestPriorityThenPreventNew 停低的,还满就拒 混合策略

8.4 四种作用域

EConcurrencyModeSoundConcurrency.h:233):Group(全局一组)/ Owner(每个发声 Actor 一组)/ OwnerPerSound(每个 Actor × 每个声音)/ Sound(每个声音全局)。**”每人最多 3 个脚步声”= Owner 作用域**。

8.5 Ducking:不挂断只调小

VolumeScale(+ EConcurrencyVolumeScaleMode:Default/Distance/Priority):同组声音达到上限时,老声音不停止而是被调小(Ducking)——“语音通话时 BGM 自动变小”的标准实现(第 10 章玩法)。

8.6 求值链

FSoundConcurrencyManager::EvaluateConcurrencySoundConcurrency.cpp:818)在 AddNewActiveSound 时求值:CanPlaySound(:654,MaxCount + RetriggerTime)→ 超限则 CreateAndEvictActiveSounds(:901,按规则挑选被驱逐者 + VoiceStealReleaseTime 淡出)。

8.7 全局预算与优先级

  • PriorityUSoundBase::PrioritySoundBase.h:218)+ 组件 bOverridePriority(AudioComponent.cpp:776-783)+ 距离优先级衰减(第 4.6 章)——优先级 = 被保住的资格
  • au.EnableRelativeRenderCostVoiceLimitAudioDevice.cpp:205):按渲染成本(解码格式/声道数)而非纯数量计预算——移动端关键优化项(ADPCM 便宜、PCM 贵,同样预算能装更多 ADPCM);
  • au.SetAudioChannelCount(:44):运行时改全局通道数(测试用)。

8.8 源码考古:EvaluateConcurrency

源码考古:想看真东西的人进。

SoundConcurrency.cpp:818-901(要点):

1
2
3
4
5
6
7
8
9
bool FSoundConcurrencyManager::EvaluateConcurrency(FActiveSound& NewActiveSound, ...)
{
// 1. 找到/创建并发组(按 EConcurrencyMode 作用域)
// 2. CanPlaySound:MaxCount 未满 → 直接通过
// 3. 满了 → 按 ResolutionRule 挑被驱逐者:
// StopOldest → 最老的;StopFarthest → 最远的;
// StopLowestPriority → FCompareActiveSounds 排序取最低
// 4. CreateAndEvictActiveSounds:停掉被驱逐者(带淡出)+ 新声音入组
}

这段代码在干什么:并发裁决 = “组”(作用域)+”规则”(ResolutionRule)+”执行”
(偷声淡出)
三段式。注意偷声带 VoiceStealReleaseTime 淡出——直接掐断会咔哒。
这就是”同屏 50 个枪声只响 20 个”的机制:谁响由优先级+距离+新旧共同决定。

第 9 章 使用篇:从零开始配置一个 3D 声源

TL;DR|把一个声音变成”听声辨位”的 3D 声源需要五步:素材准备(单声道优先、压缩选型)
资产三件套(SoundWave → SoundCue → SoundAttenuation)→ 衰减配置(曲线/形状/范围)
空间化配置(bSpatialize/近距转 2D)→ 摆放播放(组件 or 静态函数)。配完衰减后
再配遮挡与混响。本章末尾给一个”篝火声”完整配方。类比贯穿:音量地图 → 调音台推子 →
接线板

9.1 素材准备:单声道是空间化的前提

空间化 = 把一个位置变成左右声道差——所以 3D 声源素材必须是单声道(Mono)。立体声素材做 3D 会用 StereoSpread 拆左右(第 5.4 章),效果不如单声道干净。多声道环绕素材不适合做 3D 声源。

压缩格式选型(内存估算公式:采样率 × 位深 × 声道 × 时长 / 8,标注:社区实测数据,非本仓库实测):

格式 体积 质量 适用
未压缩 PCM 10.5MB/分钟(48kHz 16bit 单声道) 最高 极短关键音(枪声)
ADPCM 1/4 体积 可接受 中短音效(脚步/UI)
Vorbis 1/10 体积 通用压缩
Opus 更小 好(流送友好) 长环境音流送

流送开关USoundWave::bStreamingSoundWave.h:455,注释 “Here for legacy code”)——长音频(环境底床/音乐)开流送,内存只占一小块缓冲;短音效预载(第 11 章详述)。

9.2 资产三件套

1
USoundWave(波形数据)──引用──▶ USoundCue(播放编排:节点图)──引用──▶ USoundAttenuation(衰减/空间化/遮挡/混响配置)
  • USoundCueSoundCue.h:90):FirstNode(:95)是节点图入口——放一个 WavePlayer 节点就成;
  • USoundAttenuationSoundAttenuation.h:453):纯配置资产,可被多个声音共享。

分层资产策略通用衰减资产(”通用枪声衰减”)供同类声音共享——改一处,全局生效;特殊声音用专属资产。这比每声音手调强得多(第 12 章数据驱动的雏形)。

9.3 衰减资产配置

设置 推荐 理由
DistanceAlgorithm 枪声/爆炸:Inverse;环境音:NaturalSound;通用:Logarithmic 见第 4.2 章曲线表
AttenuationShape 通用 Sphere;定向声源 Cone Cone 给喇叭/对话
FalloffDistance 按玩法定(射击 5000-10000;对话 1000-2000) 衰减过渡带
MaxDistance = FalloffDistance 终点 同时是性能门控边界

避坑MaxDistance 不只是”听不到就不响”——它同时决定 SoundIsAudible
性能门控(听不见的声音引擎直接跳过创建)。衰减范围画大了,性能开销也大
(更多声音进入”可听”范围)。大世界请给”远处也能听到”的声音单独设计(如
枪声用 Inverse 曲线天然远处小声,而不是把 MaxDistance 拉爆)。

9.4 空间化配置

设置 推荐 说明
bSpatializeSoundAttenuation.h:158 true 3D 声源总开关
SpatializationAlgorithm(:210) Panning(默认)或 HRTF HRTF 需要插件(第 5.3 章)
BinauralRadius(:218) 视玩法 HRTF 生效半径
NonSpatializedRadiusStart/End(:254/:258) 50/100 近处转 2D 防晕(贴脸声音不转脑袋)
StereoSpread(:266) 0.2~0.5 立体声素材的左右分离宽度

避坑:选了 SPATIALIZATION_HRTF 但没启用空间化插件 → 静默回退 Panning(第 5.3 章)——调试时用 au.3dVisualize.SpatialSources(AudioDebug.cpp:79)看哪些声源真的走了 HRTF。

9.5 遮挡与混响配置

  • 遮挡SoundAttenuation.h:345-353):bEnableOcclusion=true + OcclusionTraceChannel(选 Visibility 或自定义通道)+ 低通频率(2000Hz)+ 音量衰减(0.3)+ 插值时间(0.3s);
  • 混响bEnableReverbSend=true(:182)——声音自动按距离发送到 MasterReverbSubmix(第 7 章)。

避坑:遮挡射线通道别用 ECC_WorldStatic 以外的奇怪通道——射线只测配置的通道,墙的碰撞通道必须匹配,否则”穿了墙还清脆”。

9.6 摆放与播放:组件 vs 静态函数

场景 原因
一次性事件(开火/拾取) PlaySoundAtLocation 无需持有,播完自动清理
持续声音(引擎/环境/对话) UAudioComponent 需要停止/淡出/换声/参数
需要参数化(音量/音高/自定义参数) UAudioComponent 完整参数接口

组件挂法:附加到发声 Actor(枪口组件、引擎位置)——Sound 属性 + bAutoActivate;代码用 Play/Stop/FadeIn/FadeOutAudioComponent.h:500-583)。

9.7 SoundCue 节点图:播放编排

USoundNodeSoundNode.h:57)节点体系(ChildNodes + DECLARE_SOUNDNODE_ELEMENT 每实例状态 :26-54):

1
2
3
4
5
6
7
8
WavePlayer(基础播放)
+ Random(随机选变体) → 脚步声多样化
+ Looping(循环) → 环境声/引擎
+ DistanceCrossFade(距离交叉淡化)→ 脚步声音量随距离渐变而非硬切
+ Attenuation(节点级衰减覆盖) → 同一 Cue 不同场景用不同衰减
+ ParamCrossFade(参数驱动交叉淡化)→ 转速参数切换引擎低/高转速声
+ Doppler(多普勒) → 高速经过的"嗡——"
+ Modulator(随机音量/音高) → 每次播放略不同

玩法技巧USoundNodeDistanceCrossFadeSoundNodeDistanceCrossFade.h:82)——脚步声的近/远两个变体交叉淡化,比单一声音+衰减真实得多。

9.8 参数化:运行时的”旋钮”

ISoundParameterControllerInterfaceSoundParameterControllerInterface.h:24)是统一的参数接口:

1
2
3
4
// AudioComponent 上直接用:
AudioComp->SetFloatParameter("EngineRPM", CurrentRPM); // AudioComponent.h:548
AudioComp->SetIntParameter("Gear", 3); // :541
AudioComp->SetTriggerParameter("ShiftUp"); // :534

FAudioParameterAudioParameter.h:92,5.9 已迁到 AudioExtensions 模块)是参数的传输结构。同一套参数通路:SoundCue 的 WaveParam/ParamCrossFade 节点消费它,MetaSound 的 FRuntimeInput 也消费它——换引擎系统不改玩法代码

9.9 音量分层:调音台推子

SoundClass 树SoundClass.h:208):

1
2
3
4
5
Master
├─ SFX(音效) ← 3D 声源都挂这里
├─ Music(音乐) ← bIsMusic=true(:107)
├─ Voice(语音) ← 对话/语音聊天
└─ UI(界面) ← bIsUISound=true(:103,不衰减不空间化)

动态混音GameplayStatics.h:881-937):SetBaseSoundMix / PushSoundMixModifier / PopSoundMixModifier / SetSoundMixClassOverride(带 bApplyToChildren=true)——进战斗推一个 Mix(音乐 -50%),出战斗 Pop。

9.10 动手实验室:篝火声(五步完整配方)

步骤 资产/设置 验收标准
① 素材 火苗 loop(单声道、Opus、流送)+ 2~3 个噼啪 one-shot(单声道、Vorbis) 素材可独立播放
② Cue Cue_Fire:Looping 包火苗;Cue_Crackle:Random 包噼啪 两 Cue 均响
③ 衰减 Atten_Fire:Sphere、NaturalSound、FalloffDistance=1500、bSpatialize=true、NonSpatializedRadius 50/100、bEnableOcclusion=true、bEnableReverbSend=true 走近变响、绕圈方向感正确、隔墙变闷
④ 摆放 篝火 Actor 上挂 AudioComponent(Sound=Cue_Fire,bAutoActivate);噼啪用蓝图定时 PlaySoundAtLocation(Cue_Crackle, 火堆位置) 火苗持续响,噼啪随机响
⑤ 验收 玩家绕篝火走一圈:音量渐变、方向清晰、隔墙闷、室内回声 全部符合 = 空间音频四件套全生效

检查清单au.3dVisualize.Attenuation=1 看衰减球;stat audio 看这个声音占了几个 WaveInstance;确认没被并发规则吞掉(第 8 章)。


第 10 章 玩法结合篇:事件驱动音频、多人同步、环境与区域

TL;DR|音频与玩法结合的三原则:事件驱动(玩法发事件,音频响应——不轮询)、
多人同步(UAudioComponent 无复制——用 unreliable NetMulticast RPC 播声音,服务器
只裁剪广播,不占游戏带宽)、环境区域(AAudioVolume 让”进山洞声音就变”自动发生)。
类比:音频是玩法的”信号灯”——玩家靠耳朵判断局势,你的代码负责让信号灯亮对

10.1 事件驱动哲学:谁触发声音

原则:玩法逻辑发事件,音频系统响应事件——而不是音频自己轮询状态。

1
2
✅ 推荐:伤害结算 → OnDamaged 事件 → PlaySoundAtLocation(命中音, 位置)
❌ 避免:音频每帧检查"玩家血量 < 50 了吗"→ 决定播不播心跳声

PlaySound2D / PlaySoundAtLocationGameplayStatics.h:629/681)就是”事件出口”。一音多用:同一个枪声 Cue 被开火/命中/敌人/队友共用——资产复用是音频的带宽(这里指内容量)。

10.2 音频与玩法解耦:接口层

玩法代码不应该贴资产名(”PlaySoundAtLocation 传那个枪声资产”)——玩法发语义事件,音频层映射资产(第 12 章封装详述)。中间层方案:事件分发器(GameplayMessage 模式)、接口(IAbilityAudioEvent)、或简单的枚举参数。

“音频即信号”:玩家凭声音判断局势——脚步渐近(方向+距离)、枪声方位(听声辨位)、Boss 技能前摇音(预警)。空间音频做得好的游戏 = 玩家可以闭眼打。Friendslop 这类多人射击正是这个理念的极致。

10.3 多人同步:UAudioComponent 没有复制(本章核心)

已实测UAudioComponent 没有 GetLifetimeReplicatedProps/DOREPLIFETIME——声音不会自动同步。标准做法:

图 F3 多人音频同步:服务器发事件,各客户端本地播放(unreliable NetMulticast RPC)
 服务器(事件发生:玩家 A 开火)            客户端 B(听到)
    │                                        │
    │ ① Multicast_PlaySound(unreliable RPC)│
    │ ────────────────────────────────────▶  │
    │   参数:Cue 引用 + 世界位置             │ ② 本地 PlaySoundAtLocation
    │                                        │    → 衰减/空间化/遮挡全套本地计算
    │   (服务器本地也播——Multicast 语义)     │    → 音画同步(各自本地计算延迟最低)
1
2
UFUNCTION(NetMulticast, Unreliable)
void Multicast_PlaySoundAtLocation(USoundBase* Sound, FVector Location);

要点(与《UE 网络架构完全教程》Ch6 呼应):

  1. Unreliable:声音事件丢了就丢了(下一秒可能又来一个)——别用 Reliable 占游戏可靠带宽;
  2. 只传”事件 + 位置”:声音内容各客户端本地有(资产)——不占游戏带宽(声音数据不走网络,走资产分发);
  3. OwningActor 参数PlaySoundAtLocation 的 OwningActor 影响服务器相关性裁剪(第 7 章 Relevancy)——传对 OwningActor,不然服务器会漏广播
  4. 策略差异:一次性事件(枪声)用 RPC;持续声音(引擎轰鸣)用”拥有者的 AudioComponent + 位置属性复制”或本地预测——别把循环声也 RPC 每帧发

10.4 听者管理

  • Listener 绑玩家 Pawn:默认监听器跟随主玩家——切观战/死亡时 SetListener 切到目标;
  • 2D UI 音bIsUISound=true(SoundClass.h:103)的声音不空间化不衰减——菜单点击、血条警告走 UI SoundClass。

10.5 环境与区域:AAudioVolume

AAudioVolumeAudioVolume.h:152)是”声学区域”:

1
2
3
山洞体积(Priority=10):ReverbSettings=长回声(DecayTime 2.5s)+ InteriorSettings=闷
室外默认(Priority=0): 无混响、无遮挡
重叠区:Priority 高者胜出(山洞 > 室外)

玩法价值:玩家进洞瞬间声音自动”变山洞”——空间感是自动的(UpdateAudioVolumeEffects 每帧按听者位置插值),不需要代码。水下(全部声音低通+慢速)、密闭舱室(强混响)、走廊(中混响+轻微遮挡)都是同一个配方。

10.6 空间化的玩法价值:听声辨位

玩法 音频实现
敌我定位 脚步声/枪声走完整空间化(HRTF 插件效果最佳)
注意力引导 SetGlobalListenerFocusParametersGameplayStatics.cpp:1390):聚焦目标 → 其他声音降权(”音频变焦”)
预警 远处枪声 = Inverse 衰减 + 低通(先听到闷响,近了才清脆)
状态提示 心跳声(低血量)用 2D 但带节奏变化——“状态信号”与”空间信号”分开

10.7 动态混音:Boss 战压低 BGM

1
2
3
进战斗:PushSoundMixModifier(Mix_Combat)   → Music -50%,SFX +20%
Boss 战:SetSoundMixClassOverride(Mix_Combat, MusicClass, Volume=-80%, bApplyToChildren=true)
结束: PopSoundMixModifier(Mix_Combat)

语音 Ducking(第 8.5 章):语音 SoundClass 与 BGM 同组 Concurrency + VolumeScale——语音一响,BGM 自动变小(不挂断只调小)。

10.8 语音聊天:多人游戏的”最后一层空间音频”

  • 网络层UVoiceChannelVoiceChannel.h:19)——网络文档里”Voice 通道”的实体;
  • 采集/编解码FVoiceModule::CreateVoiceCapture/Encoder/DecoderVoiceModule.h:70-91,Opus 实现);
  • 音频侧VoiPSoundClassAudioSettings.h:156)——语音独立 SoundClass,可单独调音量。

玩法:队友语音做空间化(队友在左边说话,声音从左边来)——Friendslop 类游戏的沉浸感关键。

10.9 音乐系统

  • bIsMusic(SoundClass.h:107):音乐独立 SoundClass 层(不衰减不遮挡);
  • 2D 音乐 + 3D 环境音分层:音乐永远 2D(全景),环境音 3D(空间)——混音台思路;
  • 状态驱动切换:低血量→紧张音乐、Boss→战斗音乐——用 SoundMix 渐变而非硬切(硬切 = 音乐”啪”地换,出戏)。

10.10 动手实验室:多人射击脚步声方案

需求 方案
脚步声事件 移动组件在”落地/步进”事件时触发(不轮询)
多样化 Cue:Random 包 3~4 个脚步变体
距离与方向 完整衰减(FalloffDistance 按步频调)+ 空间化(HRTF 插件)
并发控制 Concurrency:Owner 作用域、MaxCount=2、StopFarthestThenOldest(每人最多 2 个脚步同时响)
蹲伏/奔跑 SetFloatParameter(“StepSpeed”) 驱动 ParamCrossFade 切换变体
多人同步 NetMulticast Unreliable RPC(第 10.3 章)
队友/敌人区分 敌人脚步 Focus 高亮(SetGlobalListenerFocusParameters)、队友脚步降权
区域 门内门外 AAudioVolume:遮挡+混响自动

第 11 章 优化篇:并发预算、流送、虚拟循环、移动端

TL;DR|音频优化的三道闸:并发预算(设计期 Concurrency + 运行时通道上限,按渲染
成本计权重)、流送(长音流送短音预载,au.streamcaching 缓存)、虚拟循环(远处
循环声降频更新——“放假”)。移动端还有中断处理与低延迟路径。类比:电话占线 →
仓库管理 → 远处声音放假

11.1 性能账本:音频 CPU 三花销

花销 内容 看什么
解码 压缩音频 → PCM(CPU 最重项) Opus/Vorbis 解码开销
逐源 DSP 衰减增益 + 滤波 + 空间化 + 混响发送 每源固定成本
插件 HRTF 等双耳渲染 每源 3-5ms 预算(社区数据,非本仓库实测)

stat audioAudio.h:32-39):ActiveSounds(活动声音数)/ Sources(实际发声数)/ VirtualLoops(虚拟循环数)/ MaxChannels(上限)——第一眼就看这四个数

11.2 并发预算(第一道闸)

1
2
3
4
设计期:每个声音资产配 Concurrency(MaxCount + 规则)——"枪声最多 8 个同时"
运行期:MaximumConcurrentStreams(AudioSettings.h:199)全局上限
+ au.EnableRelativeRenderCostVoiceLimit(AudioDevice.cpp:205)按渲染成本计权重
→ 超预算:GetVolumeWeightedPriority 排序截断(第 8.7 章)

实践:50 个敌人同时开枪 = 50 个枪声请求 → 并发组截到 8 个 + 全局预算再截——**玩家听到的永远是”最近的 + 最重要的”**。

11.3 流送(第二道闸)

  • 原则长环境音全部流送、短事件音全部预载(社区实测经验,非本仓库实测);
  • bStreamingSoundWave.h:455)开流送;
  • FAudioChunkCacheAudioStreamingCache.cpp)管理流送缓存,au.streamcaching.* CVar 家族(StreamCacheSizeOverrideMB :139 等)控制缓存大小;
  • 避坑:流送声音有”首次播放延迟”(读盘)——关键音效(枪声)千万别流送,开局爆音。

11.4 虚拟循环(第三道闸)

FAudioVirtualLoopAudioVirtualLoop.cpp):

1
2
3
4
5
6
远处循环声(瀑布/风声/远处引擎):
→ Virtualize(:60):逻辑上继续"播着"
→ CalculateUpdateInterval(:96):更新间隔按距离插值
(DistanceToListener - MaxDistance) / PerfDistance
→ au.VirtualLoops.UpdateRate.Min(0.1s)~ Max(3.0s)
→ 玩家靠近 → 恢复全职更新(VirtualizeInactiveLoops,AudioDevice.cpp:3250)

收益:100 个远处循环声 × 每帧更新 = 灾难;100 个 × 每 3 秒一次 = 几乎免费。虚拟循环是”大世界环境音”的默认解

11.5 内存:预载 vs 流送

  • 预载FAudioDevice::PrecacheAudioDevice.h:625)——启动/关卡加载时把短音效解压进内存;
  • FActiveSound 无对象池new FMixerSourceAudioMixerDevice.cpp:1546)直接堆分配——引擎不做池(生命周期差异大/线程交接复杂),高频创建销毁(枪声连发)时自建池(第 12.10 章的 AudioManager 里做)。

11.6 遮挡与混响开销

  • OcclusionCheckIntervalActiveSound.cpp:197,默认 0.1s):墙多场景调大到 0.2-0.3s;
  • au.UseAsyncOcclusionTraces=1(:27):射线走异步(物理线程算,不卡音频);
  • 混响发送是”每源一次 SetSubmixSendInfo”——批量不会太贵,但别给装饰声开混响

11.7 移动端

  • 中断处理:来电/挂断自动静音(FCoreDelegates::AudioInterruptionDelegateCoreDelegates.h:394au.DisableAudioSuspendOnAudioInterrupt 可关);
  • 预算:移动端 HRTF 每源 3-5ms(社区数据)——移动端优先 Panning,HRTF 只给关键声源(队友语音);
  • 渲染成本计权au.EnableRelativeRenderCostVoiceLimit=1 让 ADPCM 声源”便宜”(同预算装更多)。

11.8 调试工具链

工具 看什么
au.3dVisualize.ActiveSoundsAudioDebug.cpp:55 场景里活动声音的 3D 球体(5 种显示模式)
au.3dVisualize.Attenuation(:744) 衰减形状(配合 SoundBase::bDebug 标记单个声音)
au.3dVisualize.SpatialSources(:79) 空间化声源
au.DumpActiveSounds(:676) 全部活动声音清单(并发/预算诊断)
stat audio 核心计数器(11.1)
Audio Insights 插件 UE_TRACE 时间线(声音生命周期/线程分布)

11.9 症状 → 定位表

症状 大概率原因 检查
声音被吞 并发规则(PreventNew)或预算截断 au.DumpActiveSounds + Concurrency 设置
爆音/卡顿 流送饥饿(流送缓存太小/磁盘慢) au.streamcaching.* + 日志
掉帧 插件 DSP 太重(HRTF 全开) 统计插件耗时 + 降级 Panning
内存上涨 流送覆盖不全(长音没流送) 检查 bStreaming 覆盖
方向感丢失 bSpatialize 没开 / HRTF 插件没启用 au.3dVisualize.SpatialSources
遮挡不生效 射线通道不匹配 OcclusionTraceChannel vs 墙的碰撞通道

11.10 动手实验室:开放世界 500 声源预算分解

类别 数量 策略 预算
关键事件(枪声/命中/语音) ~20 预载 + 高优先级 + HRTF 全量保
近处环境(脚步/道具) ~50 预载 + Concurrency 组 正常
中距(敌人动作) ~100 预载 + 并发截断 截到 40
远处循环(瀑布/风声) ~300 流送 + 虚拟循环 几乎免费

性能测试五步法:① 基线(空场景 stat audio)→ ② 加压(500 声源场景)→ ③ 定位(DumpActiveSounds 找吞声)→ ④ 调参(并发/流送/虚拟循环)→ ⑤ 回归(对比基线)。


第 12 章 封装篇:AudioManager 设计模式、数据驱动、接口抽象

TL;DR|引擎没有”音频管理器”——只有设备层(FAudioDeviceManager/FAudioDevice/FAudioThread)
三件套。游戏需要自己的封装,分三层:配置数据层(UDeveloperSettings/DataAsset 存
“事件 → 资产”映射)、运行时调度层(WorldSubsystem 管生命周期与混音)、接口抽象层
(玩法代码只调 Play2D/PlayAtLocation/StopByTag,不贴资产名)。参考 Lyra 的
ULyraAudioSettings + ULyraAudioMixEffectsSubsystem。类比:**音频管家——玩法说
“我要脚步声”,管家负责”放哪个资产、多大声、什么时候停”**。

12.1 引擎里没有”音频管理器”

引擎组件 角色
FAudioDeviceManagerAudioDeviceManager.h 多音频设备生命周期管理
FAudioDevice(非 UObject) 单设备的音频引擎(播放/求值/混合)
FAudioThreadAudioThread.h:21 线程命令队列(RunCommandOnAudioThread :58)

**引擎给的是”设备”不是”服务”**——就像网络篇说”引擎没有网关服务器”:设备层管”怎么响”,”什么时候响、响哪个、多大声”是游戏的责任。

12.2 为什么游戏需要封装

三个理由(对应三层):

  1. 玩法代码不该贴资产名(”PlaySoundAtLocation(枪声_Cue)”在 50 个地方 = 换枪声改 50 处)→ 配置数据层
  2. 生命周期要管理(场景切换清场、Travel 清场、音量分层状态)→ 运行时调度层
  3. 玩法只要语义(”我要脚步声”而不是”播这个资产”)→ 接口抽象层

12.3 配置数据层:事件 → 资产映射

Lyra 实证ULyraAudioSettingsLyraAudioSettings.h:31UDeveloperSettings,config=Game)——开发者设置存软引用(延迟加载,不拖慢启动)。

通用设计

1
2
3
4
5
6
7
8
9
10
UCLASS(config=Game, defaultconfig)
class UGameAudioSettings : public UDeveloperSettings
{
// 事件名 → 声音资产(软引用,延迟加载)
UPROPERTY(EditAnywhere, config)
TMap<FGameplayTag, TSoftObjectPtr<USoundBase>> EventSounds;
// 事件名 → 衰减资产
UPROPERTY(EditAnywhere, config)
TMap<FGameplayTag, TSoftObjectPtr<USoundAttenuation>> EventAttenuations;
};

注意:引擎没有 UDataAsset 音频资产先例(USoundConcurrency/USoundAttenuation 是纯 UObject 配置资产)——数据驱动用 DeveloperSettings + 软引用(Lyra 模式),或自建 UDataAsset。

12.4 运行时调度层:WorldSubsystem

Lyra 实证ULyraAudioMixEffectsSubsystemLyraAudioMixEffectsSubsystem.h:41UWorldSubsystem)——世界生命周期里应用 Mix。

通用设计UGameAudioSubsystem : UWorldSubsystem注意:引擎没有 AudioSubsystem 基类,用 UWorldSubsystem)——持有当前 Mix 状态、管理全局听者、处理场景切换清场。

12.5 统一事件 API:玩法只调语义

1
2
3
4
5
6
// 接口层:玩法代码只认识这些
void UGameAudioSubsystem::Play2D(FGameplayTag Event);
void UGameAudioSubsystem::PlayAtLocation(FGameplayTag Event, FVector Location, AActor* OwningActor = nullptr);
void UGameAudioSubsystem::PlayAttached(FGameplayTag Event, USceneComponent* AttachTo, FName Socket = NAME_None);
void UGameAudioSubsystem::StopByTag(FGameplayTag Event, AActor* Owner = nullptr);
void UGameAudioSubsystem::FadeAll(float Duration, float TargetVolume);

内部路由:查配置层映射 → UGameplayStatics::PlaySoundAtLocationUAudioComponent——换资产只改配置,不动玩法代码

12.6 参数路由:事件参数 → FAudioParameter

1
2
3
4
5
玩法:PlayAtLocation(Event_Footstep, Pos) + SetParam("StepSpeed", 2.0f)
→ 配置层查资产
→ ISoundParameterControllerInterface(SoundParameterControllerInterface.h:24)
→ FAudioParameter(AudioParameter.h:92)
→ SoundCue 的 ParamCrossFade 或 MetaSound 的 FRuntimeInput

玩法语义与音频参数解耦:玩法说”奔跑”,音频层翻译成”StepSpeed=2.0”。

12.7 生命周期管理

时机 动作
场景切换/ServerTravel FadeAll(0.5s 淡出)+ 停止非持久声音
玩家死亡/观战 听者切到目标 + 停止玩家专属声音
暂停菜单 音乐继续(2D)+ 音效暂停(按 SoundClass 分组控制)
世界清理 Subsystem 的 OnWorldBeginPlay/Deinitialize 挂钩

12.8 混音层封装:场景函数

1
2
3
进战斗:ApplyCombatMix()   → PushSoundMixModifier(Mix_Combat)
Boss: ApplyBossMix() → SetSoundMixClassOverride(Mix_Combat, Music, -80%)
退出: RevertCombatMix() → PopSoundMixModifier(Mix_Combat)

音量分级表(社区经验,非本仓库实测):整体/音乐/音效/语音四档独立音量(存 ini)——对应 SoundClass 树的四个推子。

12.9 数据驱动与热更新

  • 热更新(引擎机制):播放中 SetAttenuationOverridesSendCommandToActiveSoundsAudioComponent.cpp:1367-1388)——游戏侧复刻:改配置 → 通知 AudioManager → 广播到活动声音;
  • 开发工具:控制台命令 Audio.Debug.Event <Event> 显示”这个事件 → 哪个资产 → 哪个 Concurrency”——配置层可查性是音频封装的隐藏价值。

12.10 动手实验室:200 行 AudioManager 骨架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// ===== 配置数据层 =====
UCLASS(config=Game, defaultconfig)
class UGameAudioSettings : public UDeveloperSettings
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, config, Category="Audio")
TMap<FGameplayTag, TSoftObjectPtr<USoundBase>> EventSounds;
UPROPERTY(EditAnywhere, config, Category="Audio")
TMap<FGameplayTag, TSoftObjectPtr<USoundAttenuation>> EventAttenuations;
};

// ===== 运行时调度层 =====
UCLASS()
class UGameAudioSubsystem : public UWorldSubsystem
{
GENERATED_BODY()
public:
void PlayAtLocation(FGameplayTag Event, FVector Location, AActor* OwningActor = nullptr);
void Play2D(FGameplayTag Event);
void StopByTag(FGameplayTag Event, AActor* Owner = nullptr);
void FadeAll(float Duration, float TargetVolume);

private:
USoundBase* ResolveSound(FGameplayTag Event) const; // 配置层查表 + 软引用加载
USoundAttenuation* ResolveAttenuation(FGameplayTag Event) const;
};

// ===== 实现:接口层路由到引擎 API =====
void UGameAudioSubsystem::PlayAtLocation(FGameplayTag Event, FVector Location, AActor* OwningActor)
{
USoundBase* Sound = ResolveSound(Event);
if (!Sound) { UE_LOG(LogAudio, Warning, TEXT("Audio event %s not configured"), *Event.ToString()); return; }
UGameplayStatics::PlaySoundAtLocation(GetWorld(), Sound, Location,
FRotator::ZeroRotator, 1.0f, 1.0f, 0.0f,
ResolveAttenuation(Event), nullptr, OwningActor);
}

与 Lyra 结构对照:配置层 ↔ ULyraAudioSettings;调度层 ↔ ULyraAudioMixEffectsSubsystem;接口层 ↔ Lyra 的玩法侧调用点。

反模式清单:① God Object(一个类管所有音频逻辑);② 字符串拼资产路径(LoadObject<USoundBase>(nullptr, TEXT("/Game/Audio/..."))——用软引用映射);③ 玩法代码直接 new AudioComponent 不回收(生命周期泄漏);④ 每个声音单独手调衰减(不建共享资产)。

第 13 章 平台支持与性能调优

TL;DR|音频 CVar 分七组:空间化 / 遮挡 / 混响 / 流送 / 虚拟循环 / 并发 / 调试。
调优三板斧:stat audio 看数字 → au.3dVisualize 看画面 → 单旋钮调参。
常见坑十条(从”听不见”到”CPU 爆炸”)。

13.1 CVar 速查表(七组,写作时已 grep 核实默认值)

空间化au.EnableBinauralAudioForAllSpatialSounds=0(AudioDevice.cpp:121)、au.DisableBinauralSpatialization=0(:128)、au.spatialization.SetCurrentSpatialPlugin(控制台命令,:179)

遮挡au.DisableOcclusion=0(ActiveSound.cpp:20)、au.UseAsyncOcclusionTraces=false(:27)

混响au.DisableReverbSubmix=0(AudioMixerSubmixEffectReverb.cpp:24)

流送au.streamcaching.StreamCacheSizeOverrideMB=0(AudioStreamingCache.cpp:139)、au.streamcache.SoundWaveDefaultLoadingBehavior(SoundWave.cpp:65)、au.DisableAutomaticPrecache(AudioDevice.cpp:75)

虚拟循环au.VirtualLoops.UpdateRate.Min/Max=0.1/3.0(AudioVirtualLoop.cpp:30-40)

并发/预算au.SetAudioChannelCount=0(AudioDevice.cpp:44)、au.EnableRelativeRenderCostVoiceLimit=1(:205)、au.DisableStoppingVoices=0(:59)

调试au.3dVisualize.Enabled=0(AudioDeviceManager.cpp:49)、au.3dVisualize.ActiveSounds=0(AudioDebug.cpp:55)、au.3dVisualize.Attenuation=0(:744)、au.DumpActiveSounds(:676,命令)

13.2 调优工作流:三板斧

1
2
3
4
5
6
7
8
9
1. 看数字    stat audio(ActiveSounds/Sources/VirtualLoops/MaxChannels)
+ au.DumpActiveSounds(谁在响、谁被吞)
2. 看画面 au.3dVisualize.Enabled 1 + ActiveSounds 1(3D 球体)
+ 单个声音 bDebug 标记看衰减形状
3. 调参数 每次只动一个旋钮:
被吞声 → 并发规则/优先级(第 8 章)
远处循环贵 → 虚拟循环(第 11.4 章)
内存涨 → 流送覆盖(第 11.3 章)
掉帧 → HRTF 降级 Panning / 遮挡异步(第 11.6 章)

13.3 常见坑清单

  1. 听不见MaxDistance 太小 / 并发 PreventNew 吞了 / 音量被 SoundClass 层叠压低——三步排查(先 au.DumpActiveSounds 看有没有创建);
  2. 方向感丢失bSpatialize 没开,或 HRTF 选了但插件没启用(第 5.3 章静默回退);
  3. 爆音:流送缓存不足 / 大量同帧预载——枪声别流送;
  4. **遮挡”穿了墙”**:射线通道与墙碰撞通道不匹配(第 6.4 章);
  5. 进房间没混响:MasterReverbSubmix 默认资产没建 / bEnableReverbSend 没开 / AAudioVolume 没放;
  6. 立体声素材空间化效果差:3D 声源用单声道素材(第 9.1 章);
  7. 多人声音不同步:UAudioComponent 不复制——忘了 RPC 广播(第 10.3 章);
  8. 切场景声音残留:AudioManager 没清场(第 12.7 章);
  9. 每帧改音量SetVolumeMultiplier 高频调用走命令队列——批量/用插值(引擎有 Fade 内置);
  10. 移动端爆 CPU:HRTF 全开 + 流送不足——移动端 Panning 优先 + ADPCM(第 11.7 章)。

第 14 章 局限、替代方案与展望

TL;DR|UE 空间音频的局限:无内置 HRTF/ITD(双耳渲染靠插件)、单一听者(分屏/观战
需手动切换)、Ambisonics 仅 FOA无引擎级音频管理器(游戏自建)、FActiveSound
无池
。替代路径:Wwise/FMOD 中间件、ResonanceAudio、Windows Spatial Audio。方向:
MetaSound 程序化音频 + Audio Gameplay Volumes 区域化。

14.1 局限与对策

局限 表现 对策
无内置 HRTF/ITD 双耳渲染依赖插件 启用 Windows Spatial Audio/ResonanceAudio 插件;移动端 Panning 兜底
单一听者 分屏/观战只跟主玩家 SetListener 手动切换(第 10.4 章)
Ambisonics 仅 FOA 环境声场只有一阶 高阶声场用中间件
无引擎级音频管理器 生命周期/数据驱动要自建 第 12 章三层封装
FActiveSound 无池 高频创建销毁有分配开销 AudioManager 里自建池(第 11.5 章)

14.2 替代路径

方案 定位 与 UE 内置的关系
Wwise / FMOD 专业音频中间件(音频管线/混音/素材管理) 通过 AudioLink 接入(引擎无内置集成)
ResonanceAudio 谷歌空间音频插件(HRTF/反射) UE 插件(Plugins/Runtime/ResonanceAudio)
MicrosoftSpatialAudio Windows 空间音频(HRTF) UE 插件
AudioModulation 参数调制(引擎插件) 声音参数的数据驱动调制

14.3 演进时间线与阅读进阶

时间 里程碑
UE4 早期 平台专属音频(XAudio2/OpenAL 等)
4.20+ AudioMixer 统一渲染器
5.0~5.3 MetaSound 引入、Audio Gameplay Volumes 起步
5.4~5.9 音频流缓存、Audio Insights、区域音频成熟
未来 MetaSound 转正、空间音频插件生态(Oculus/PS5 3D Audio)

进阶路线:Epic 官方文档(Sound Concurrency → MetaSounds → Audio Gameplay Volumes → 流缓存)→ 中文深挖(附录 C)→ 本仓库源码(附录 A 指路)→ 中间件对比(Wwise 的 Spatial Audio 文档)。


附录 A 源码地图

全部路径基于本仓库快照(2026-08-26,HEAD 6cea9bd20f8f)。行号只用于定位;代码演进后以函数名为准。
阅读顺序建议:先 ActiveSound.cpp(求值核心)→ AudioDevice.cpp(播放链路)→ AudioMixerDevice.cpp(渲染线程)→ 按章节深入。

A.1 引擎核心(Runtime/Engine)

文件 关键内容
Private/ActiveSound.cpp FActiveSound 求值核心:UpdateWaveInstances :1081、UpdateAttenuation :2053、CheckOcclusion :1599、HandleInteriorVolumes :1708、OcclusionCheckInterval :197、au.DisableOcclusion :20、au.UseAsyncOcclusionTraces :27
Private/AudioDevice.cpp 播放链路:PlaySoundAtLocation :6705、Update :4725、GetSortedActiveWaveInstances :4159、SoundIsAudible :6318、LocationIsAudible :6380、StartSources :4541、UpdateAudioVolumeEffects :4923、au.* CVar 区 :44-205
Public/AudioDevice.h FAudioDevice 声明、AddNewActiveSound :763、Precache :625/:639
Public/ActiveSound.h FActiveSound 类(音频线程声音实例)
Public/Audio.h FWaveInstance :315(bIsAmbisonics)、GetVolumeWeightedPriority :482、STAT_* 计数器 :32-39
Public/AudioThread.h FAudioThread :21、RunCommandOnAudioThread :58、STATGROUP_AudioThreadCommands :19
Public/AudioDeviceManager.h FAudioDeviceManager(多设备管理)、au.3dVisualize.Enabled :49
Private/Audio.cpp FSoundSource:GetSpatializationParams :724、UpdateStereoEmitterPositions :625、NonSpatializedAmount :739-779
Private/AudioVirtualLoop.cpp FAudioVirtualLoop:Virtualize :60、CalculateUpdateInterval :96、UpdateRate CVar :30-40
Private/AudioStreamingCache.cpp FAudioChunkCache、au.streamcaching.* :32-91(StreamCacheSizeOverrideMB :139)
Private/AudioDebug.cpp au.3dVisualize.ActiveSounds :55 / SpatialSources :79 / Attenuation :744、au.DumpActiveSounds :676

A.2 组件与游戏性(Runtime/Engine)

文件 关键内容
Classes/Components/AudioComponent.h UAudioComponent :167:Play :517、Stop :583、FadeIn :500、FadeOut :511、SetVolumeMultiplier :626、SetPitchMultiplier :630、SetFloatParameter :548、SetIntParameter :541、SetTriggerParameter :534、UInitialActiveSoundParams :146
Private/Components/AudioComponent.cpp PlayInternal :673、GetAttenuationSettingsToApply :1342(三级覆盖)、SetAttenuationOverrides 热更新 :1367-1388、bOverridePriority :776-783
Private/GameplayStatics.cpp PlaySoundAtLocation :1526、LocationIsAudible :1316、SetListenerFocusParameters :1390、SetBaseSoundMix :1752、SetSoundMixClassOverride :1877、PushSoundMixModifier :1771、PopSoundMixModifier :1915
Classes/Kismet/GameplayStatics.h 音频静态入口:PlaySound2D :629、PlaySoundAtLocation :681、PlayDialogue2D :746、SetBaseSoundMix :881、SetSoundMixClassOverride :917、Push/PopSoundMixModifier :931/:937
Classes/Sound/SoundBase.h USoundBase :108:SoundClassObject :117、Priority :218、AttenuationSettings :222、ConcurrencySet :188、VirtualizationMode :170、bDebug :127
Classes/Sound/SoundWave.h USoundWave、bStreaming :455、au.streamcache.SoundWaveDefaultLoadingBehavior :65
Classes/Sound/SoundCue.h USoundCue :90、FirstNode :95
Classes/Sound/SoundNode.h USoundNode :57(ChildNodes :64、DECLARE_SOUNDNODE_ELEMENT :26-54);子类:WavePlayer/Attenuation/Random/Looping/Modulator/Oscillator/Concatenator/DistanceCrossFade :82/ParamCrossFade/Switch/Branch/Enveloper/Doppler/QualityLevel
Classes/Sound/SoundAttenuation.h FSoundAttenuationSettings :148(bAttenuate :154、bSpatialize :158、bAttenuateWithLPF :162、bEnableOcclusion :174、bEnableReverbSend :182、SpatializationAlgorithm :210、BinauralRadius :218、OcclusionTraceChannel :234、NonSpatializedRadiusStart/End :254/:258、StereoSpread :266、LPF :281-293、Occlusion :345-353、ReverbSend :365-377、PriorityAttenuation :383-401)、USoundAttenuation :452
Classes/Engine/Attenuation.h FBaseAttenuationSettings :56(DistanceAlgorithm :63、AttenuationShape :68、FalloffDistance :95)
Classes/Sound/SoundConcurrency.h FSoundConcurrencySettings :73(MaxCount :89、bLimitToOwner :93、ResolutionRule :109、RetriggerTime :113、VolumeScale :139、VoiceStealReleaseTime :170)、EMaxConcurrentResolutionRule :30-58、EConcurrencyMode :233
Private/SoundConcurrency.cpp FSoundConcurrencyManager:EvaluateConcurrency :818、CanPlaySound :654、CreateAndEvictActiveSounds :901
Classes/Sound/SoundClass.h USoundClass :208、FSoundClassProperties :54(Volume :60、bIsUISound :103、bIsMusic :107、bReverb :119)
Classes/Sound/SoundMix.h USoundMix :170、FSoundClassAdjuster :129(bApplyToChildren :151)
Classes/Sound/AudioVolume.h AAudioVolume :152(Priority :142、FReverbSettings :170、FInteriorSettings :174、SubmixSendSettings :178、SubmixOverrideSettings :182)
Classes/Sound/ReverbSettings.h FReverbSettings :48
Classes/Sound/AudioBus.h UAudioBus :87
Classes/Sound/SoundSubmix.h USoundSubmixBase :173、USoundSubmix :339
Classes/Sound/AudioSettings.h MaximumConcurrentStreams :199、DefaultSoundClassName :140、VoiPSoundClass :156、MasterSubmix :160、ReverbSubmix :168
Classes/Sound/AmbientSound.h AAmbientSound :12
Classes/Engine/VoiceChannel.h UVoiceChannel :19(网络语音通道)
Public/Audio/SoundParameterControllerInterface.h ISoundParameterControllerInterface :24(SetTriggerParameter :32、SetFloatParameter :42 等)

A.3 音频混合器(Runtime/AudioMixer)

文件 关键内容
Private/AudioMixerDevice.cpp FMixerDevice:OnProcessAudioStream :1599、new FMixerSource :1546、FMixerSourceVoice :2888、Get3DChannelMap :2915(EqualPower :2997/Linear :3007、NonSpatialized Lerp :3051-3055)、PanningMethod :1173、MasterReverbSubmix :2082(默认资产 “MasterReverbSubmixDefault” :1891)
Private/AudioMixerChannelMaps.cpp InitializeChannelAzimuthMap :147(扬声器方位角表)
Private/AudioMixerSource.cpp FMixerSource:UseObjectBasedSpatialization :2337、UpdateEffects :1636(SetSubmixSendInfo)、UpdateVolume :1509
Private/AudioMixerSourceManager.cpp FMixerSourceManager:ComputeNextBlockOfSamples :3919、SetLPFFrequency :2053、au.* :42-106
Private/AudioMixerSourceOutputBuffer.cpp EncodeToSoundfieldFormats :385(Ambisonics 编码)
Classes/SubmixEffects/AudioMixerSubmixEffectReverb.h FSubmixEffectReverbSettings :23(DecayTime=1.49s 等)
Private/AudioMixerSubmix.cpp FMixerSubmix、au.BypassAllSubmixEffects :37

A.4 扩展与插件

文件 关键内容
Runtime/AudioExtensions/Public/IAudioExtensionPlugin.h IAudioSpatialization :245-388(ProcessAudio :357)、FSpatializationParams :68-124
Runtime/AudioExtensions/Public/AudioParameter.h FAudioParameter :92(5.9 迁至此模块)
Runtime/Online/Voice/Public/VoiceModule.h FVoiceModule:CreateVoiceCapture :70、CreateVoiceEncoder :81、CreateVoiceDecoder :91
Plugins/Runtime/Metasound/Source/MetasoundEngine/Public/MetasoundSource.h UMetaSoundSource :89
Plugins/Runtime/Metasound/Source/MetasoundEngine/Public/MetasoundBuilderBase.h UMetaSoundBuilderBase:AddNode :174、ConnectNodes :197
Samples/Games/Lyra/Source/LyraGame/Audio/ ULyraAudioSettings :31、ULyraAudioMixEffectsSubsystem :41(封装范例)

附录 B 术语表(中英对照)

英文 中文(本文用词) 一句话解释 首次出现
Attenuation 衰减 距离 → 音量映射 Ch2
Spatialization 空间化 方位角 → 声道增益 Ch2
Panning 声像 扬声器之间的增益分配 Ch2
Occlusion 遮挡 隔墙:低通 + 音量 Ch2
Reverb 混响 房间回声(早反射+晚反射) Ch2
Decay 衰变 混响衰减时间(勿与 Attenuation 混) Ch7
Concurrency 并发 同时发声裁决 Ch2
ResolutionRule 裁决规则 超限时挂谁(七种) Ch8
Ducking 避让 不挂断只调小 Ch8
Listener 听者 耳朵的位置与朝向 Ch2
SoundClass (不译) 音量分层推子树 Ch2
SoundMix (不译) 推子预设快照 Ch2
Submix 子混音 音频处理链节点 Ch2
Bus 总线 音频接线板 Ch2
HRTF (不译) 头部相关传输函数(双耳滤波) Ch1
ITD / ILD (不译) 双耳时间差 / 响度差 Ch1
Ambisonics (不译) 球谐声场(环境底床) Ch5
FOA (不译) 一阶 Ambisonics(4 声道) Ch5
Focus (不译) 听者聚焦(音频变焦) Ch4
Virtualization 虚拟化 远处循环声降频(放假) Ch2
Streaming 流送 音频按需加载 Ch9
Precache 预载 启动/加载时解压进内存 Ch11
FActiveSound (不译) 音频线程上的声音实例 Ch3
FWaveInstance (不译) 展开出的可播放流 Ch3
Voice 语音 语音聊天(VoiceChannel + Opus) Ch10
MetaSound (不译) 程序化音频 DSP 图 Ch2
AudioManager 音频管理器 游戏侧封装(引擎无内置) Ch2
bIsUISound (不译) UI 音(不衰减不空间化) Ch9
bIsMusic (不译) 音乐(独立分层) Ch9

附录 C 参考资料

Epic 官方(dev.epicgames.com)

  1. Sound Concurrency Reference Guide(5.6)—— 并发资产权威说明;
  2. Audio in Unreal Engine 5(5.8)—— 音频总览(Mixer 管线/SoundCue/Class/Mix/MetaSound);
  3. MetaSounds: The Next Generation Sound Sources(5.8)—— DSP 渲染图而非播放器;
  4. Audio Gameplay Volumes Overview(5.4)—— 新一代区域音频(组件化行为/优先级仲裁);
  5. Sound Attenuation in Unreal Engine(5.0)—— 衰减/非空间化半径/3D 立体声扩展/空气吸收;
  6. 音频流缓存概述(5.5 中文)—— 流缓存架构与 PrimeSoundForPlayback;
  7. *Console Variables Reference — au.*(5.8)—— 全部 au. CVar 索引;
  8. Audio Mixer Overview(4.27 中文)—— 统一跨平台渲染器背景。

中文社区(标注:以下数据非本仓库实测,写作时未能逐一访问核实):

  1. ixueyouxi《UE5.8 音频性能与内存优化:从一次池溢出说起》—— SoundCue 实例池溢出案例、内存估算公式、ADPCM/Vorbis/Opus 选型、长环境音流送;
  2. CSDN《UE5 Lyra 源码分析——Audio_Analysis 音频模块》(chouchouxia)—— 配置层+调度层分离的音频管理器范本(本文第 12 章参考);
  3. 语雀《游戏项目音频管理系统代码框架的认知与思考》—— 预加载/延迟释放/音量分级/SoundBank;
  4. k7dj《空间音频技术在游戏音效设计中的应用》—— HRTF 方向精度、移动端 3-5ms 预算、Ambisonics 环境+HRTF 对象混合方案;
  5. 知乎《Object-Based Audio 音频设计与混音案例》—— OBA 管线(资源制作与实时定位分离)。

附录 D 自测练习

入门级

  1. 用三句话 + 一个类比向同事解释空间音频四件套(衰减/空间化/遮挡/混响各管什么)。
  2. 立体声素材和单声道素材做 3D 声源的区别是什么?为什么推荐单声道?
  3. MaxDistance 在音频里有两个作用——分别是什么?

进阶级

  1. 画出从 PlaySoundAtLocation 到扬声器的完整链路,标注三个线程各干了什么。
  2. 解释”衰减范围 = 性能门控”:为什么 MaxDistance 拉大性能会变差?
  3. 多人游戏里枪声怎么同步?为什么用 Unreliable 而不是 Reliable 的 RPC?
  4. 远处 300 个循环声怎么优化?虚拟循环的更新间隔怎么算?
  5. 为什么游戏需要自建 AudioManager?三层封装各解决什么问题?

源码级

  1. ActiveSound.cpp:1599(CheckOcclusion)找到射线检测的三个参数,解释 AddIgnoredActor(OwnerID) 的作用。
  2. AudioMixerDevice.cpp:2915(Get3DChannelMap)推导:立体声(FL=270°/FR=90°)下方位角 0°(正前方)的增益对。
  3. SoundConcurrency.cpp:818(EvaluateConcurrency)找到七种裁决规则的实现分支,解释 StopFarthestThenOldest 的”最远”怎么判定。
  4. 实验:配一个 3D 声源,用 au.3dVisualize.Attenuation=1 看衰减形状;把它放进 AAudioVolume 后听混响变化;开 50 个并发声音用 au.DumpActiveSounds 观察被吞的规则。