资讯动态

Unity3D音频系统优化:AudioMixer与对象池实战

发布时间:2026/10/29 17:32:12 来源:尧图企业网站定制
1. 先搞清楚一条声音在Unity里走过的链路很多项目做到最后一周才开始补音效结果就是一堆AudioSource挂在预制体上PlayOneShot到处乱飞手机上一开枪就掉帧切场景时还有“啪”的一声爆音。这类问题的根子几乎都不在音效文件本身而在于没搞清楚一条声音在 Unity3D 游戏开发里到底经过哪几道关卡。搞清楚这条链路之后音量管理、性能优化、3D 定位这些问题会一下子变得有迹可循而不是靠试。我在这里把这条链路拆成三段声源AudioSource产生信号 → 监听器AudioListener决定听到什么 → 混音器AudioMixer决定最终输出成什么样。绝大多数“声音听不见”“只有一边耳朵响”“切场景就没了”的怪毛病都能对应到这三段里的某一段。下面按这个顺序讲并且每一步都给出实际会踩到的点。适合读这篇的人正在做 Unity 项目、音效是临时拼凑上去、或者准备把音效系统重做一遍的开发者。代码示例用 C#不依赖任何第三方插件。1.1 AudioSource、AudioListener与Mixer的三段式结构AudioSource是“喇叭”它持有一个AudioClip按自己的音量、音调、空间参数把声音送到音频线程。AudioListener是“耳朵”场景里通常只有一只默认在主摄像机身上它决定哪些 3D 声音能被听到、听到的方向和距离关系。AudioMixer处在最后一段所有输出到某个AudioMixerGroup的声源都会汇入这个组再经过组的音量、效果器和快照处理最后到硬件。这里第一个高频坑AudioListener 必须唯一。相机切换、多相机分屏、UI 相机和主相机都挂着 ListenerUnity 会在 Console 里刷一条警告实际表现是 3D 声音定位混乱或者声音忽大忽小。我的做法是在启动脚本里主动扫描一遍把多余的 Listener 关掉using UnityEngine; public class ListenerGuard : MonoBehaviour { private void Awake() { var listeners FindObjectsOfTypeAudioListener(); for (int i 1; i listeners.Length; i) { listeners[i].enabled false; Debug.LogWarning($多余的 AudioListener 已关闭{listeners[i].name}); } } }第二个坑更隐蔽UI 音效用了 3D 声源。按钮点击音如果 Spatial Blend 是 1而 Listener 离它“很远”玩家就听不到或听到极小的声音。规则很简单UI、BGM、全局提示音一律 2DSpatial Blend 0只有场景内发声的物体才用 3D。这条定死以后能省掉大量“这个音效怎么没声”的排查时间。第三个点关于生命周期AudioSource.PlayClipAtPoint会临时创建一个 GameObject 播完再销毁。写起来爽但它每次调用都产生 GC 分配战斗特效密集的场景里频繁调用会在 profiler 里看到明显的 GC Alloc 尖峰。这类一次性音效我全部交给对象池第 4 节会给出完整实现。1.2 3D声音的三个旋钮Spatial Blend、距离衰减与多普勒3D 声音的听感好不好八成取决于三个参数的搭配而不是音频文件质量。Spatial Blend决定 2D 与 3D 的混合比例。实际项目里我很少用 0 和 1 这两个极端环境氛围声常用 0.6~0.8让它有方位感但不至于走出范围就完全消失武器、脚步这类必须精确听声辨位的用 1。距离衰减由Min Distance、Max Distance和Rolloff Mode三者共同决定。三种模式的差别很大选错了会出现“远处声音反而变大”这种荒谬情况衰减模式曲线形状适用场景注意事项Logarithmic靠近时音量骤升之后快速衰减大部分现实感音效脚步、枪声默认值Min Distance 设太小会导致近距离爆音Linear线性下降可预测性强需要精确控制听感范围的机关、音乐盒范围外突变明显需要配合 Max Distance 调Custom自定义曲线特殊效果如“渐近的风声”曲线必须从左上到右下单调递减否则会出现音量反弹音量的实际计算公式是attenuation 1 / (1 k * (distance - minDistance))其中 k 由曲线在 Min 和 Max 之间的斜率决定。这就解释了一个常见现象Min Distance 设成 0.1 时玩家靠近到 1 米音量就掉了一大截。我的经验值是脚步类 Min 1~2、Max 15~20环境类 Min 3~5、Max 40远处爆炸类 Min 10、Max 120。多普勒Doppler Level默认是 1对赛车、飞行、抛射物这类高速移动物体很有用。但它依赖物体在音频帧之间的位置差如果物体用Transform.position硬设比如瞬移、跟随相机的物体会算出巨大的速度导致音调被扯得离谱。瞬移物体和挂在自己身上的音效多普勒必须设 0我在这上面吃过一次亏一个传送技能的音效听起来像磁带快进。另外还有Spread扩散角和Reverb Zone Mix。Spread 在 0 时是点声源适合精确的脚步声设到 60~90 度会让声音“包围”过来适合大面积的环境声。Reverb Zone 则是配合场景内的混响区域使用进入洞穴时自动切换混响参数这个功能配合 Mixer 的 Snapshot 用起来很顺手第 3 节细讲。2. 音频文件进项目之前先把导入设置定下来音效性能问题里有一半是导入设置埋的雷。音频文件拖进工程后Inspector 里那几项默认值对 PC 勉强够用对移动端基本是灾难。我见过一个项目把 200 多个 wav 全部用默认设置打进包里结果包体多了 40MB运行时内存里躺着 200 多份解压后的 PCM 数据。这一节按“内存在哪里花掉、CPU 在哪里花掉”的思路来讲每个选项都说明为什么这么选。先给一个基准换算方便你算账44.1kHz、16bit、单声道每秒占 88.2KB立体声翻倍到 176.4KB。也就是说一段 30 秒的立体声 BGM 解压后是 5.3MB。这个数字乘以你项目里同时加载的音效数量就是内存账单。2.1 Load Type三种模式的内存与CPU账Load Type是这个面板上影响最大的一项三个选项的代价完全不同Load Type内存占用播放时 CPU适合的素材Decompress On Load高完整解压进内存低直接读 PCM短音效、高频触发的脚步/枪声Compressed In Memory低保留压缩数据中高播放时实时解码中等长度、触发不密集的语音、环境声Streaming极低边播边从磁盘读低但受 IO 限制长 BGM、长环境铺底对短音效Decompress On Load 是唯一正确的选择。一次解压的代价换来的是零解码开销脚步、打击这类一秒可能触发十几次的声音用 Compressed In Memory 会让音频线程持续解码移动端直接掉帧。代价是内存所以短音效必须配合第 2.2 节的压缩格式与采样率下调一起用。对 1~2 分钟以内的 BGMCompressed In Memory比较均衡。对超过 2 分钟的长音乐尤其是循环播放的环境铺底用Streaming但注意两点一是流式播放对磁盘 IO 敏感低端机上可能出现开头几百毫秒的延迟二是流式加载的 clip 不要在切场景瞬间被销毁否则会听到明显的断音。实践做法是把 BGM 挂在常驻单例上切场景时淡出而不是直接 Stop。还有一个容易忽略的勾选项Preload Audio Data。勾上会在场景加载时预加载好处是第一声不延迟坏处是拖慢加载速度、增加常驻内存。我的原则是高频短音效勾上长音乐和大文件取消勾选。至于Load In Background只对上面那个流式的长文件有意义。2.2 压缩格式与强制单声道3D音效最容易被忽略的一条格式选择看两个维度时长和是否循环。PCM是无损体积最大只有在内存极其充裕且对音质有苛刻要求时才用实际项目里几乎不用。ADPCM体积约是 PCM 的 3.5 分之一解码开销极低特别适合短促的打击音、UI 音。Vorbis压缩率高音质可调是音乐和长音效的首选但它的解码开销比 ADPCM 高且循环点不精确编码器会在头尾填充数据做无缝循环的 BGM 时容易听到接缝。MP3现在基本不推荐主要是循环和延迟上的问题。采样率下调是最省事的一招。人耳对脚步声、风声这类音效的高频细节并不敏感把 Override Sample Rate 从 44100 降到 22050文件体积和内存直接砍一半听感几乎没差别。UI 点击音甚至可以降到 11025。有这个习惯的项目音频内存能省出三分之一。然后是那条几乎所有人第一遍都会踩的3D 音效必须 Force To Mono。立体声文件用于 3D 声源时Unity 无法正确做方位计算表现是“声音永远从正前方来”或者方位感很怪异。判别方法很直接——只要这个音效的 Spatial Blend 大于 0就在导入面板勾上Force To Mono。BGM 和 UI 音效保持立体声没问题。最后一个细节是音量归一化。不同来源的音效素材响度差别很大导入后如果整体音量差 10dB 以上调代码时会很痛苦。我的做法是在导入前统一做一次响度对齐把峰值归到 -3dB 左右运行时只做微调。这件事在素材进项目时花一个小时后面能省掉几十次“这个音效怎么这么小”的调试。3. AudioMixer不是可选项是音量管理的骨架大部分小项目最初的音量控制就是直接改AudioSource.volume做设置界面时才发现要遍历场景里所有声源还得记住哪些是音乐哪些是音效。这套做法撑不过第一个版本。AudioMixer提供了分组、快照、效果器、侧链闪避这一整套能力用起来并不复杂但需要一开始就把层级定好。一句话概括它的价值AudioMixer 让“音量”从一个分散在每个声源上的属性变成一个集中的、可插值、可被游戏逻辑驱动的参数。设置界面改一个滑条、过场动画压低音乐、玩家进入水下切换闷音效果全都靠它。3.1 Group层级与Exposed参数的组织方式我一般建这样一棵树Master ├── Music │ ├── BGM │ └── Stinger过场短乐 ├── SFX │ ├── Player │ ├── Enemy │ ├── Ambient │ └── UI └── Voice分组的粒度以“设置界面会单独暴露给玩家的滑条数量”为准不要为了整齐分太细。三层已经足够再细就只是增加维护成本。关键动作是给 Group 的 Volume 参数右键 Expose 到脚本然后在 Mixer 面板顶部能看到它被重命名建议改成MasterVolume、MusicVolume、SFXVolume这种一眼能认的名字。之后代码里改音量必须走 dB 换算这是新手最容易搞错的一步using UnityEngine; using UnityEngine.Audio; public class VolumeControl : MonoBehaviour { [SerializeField] private AudioMixer mixer; // UI 滑条给的是 0~1 的线性值Mixer 要的是 dB public void SetVolume(string exposedParam, float linear01) { float clamped Mathf.Clamp(linear01, 0.0001f, 1f); // 避免 log10(0) -Infinity mixer.SetFloat(exposedParam, Mathf.Log10(clamped) * 20f); } public float GetVolume(string exposedParam) { if (mixer.GetFloat(exposedParam, out float db)) { return Mathf.Pow(10f, db / 20f); } return 0f; } }为什么必须换算因为 Mixer 的 Volume 单位是分贝。UI 滑条拉到一半0.5时如果直接把 0.5 塞进去实际听到的音量大约是线性值的 0.7 倍左右玩家的直觉是“滑条过半声音反而更大”体验很怪。用log10换算之后滑条位置和主观响度才基本对齐。还有一条硬性提醒AudioMixer 资源必须在 Assets 里存在并被引用不能在运行时 new。同时要确认 Mixer 资源被打进了包体放在 Resources 或 Addressables 里或通过场景引用我见过打包后设置界面滑条失效的情况就是因为 Mixer 引用在构建时被裁掉了。3.2 Snapshot切换与Duck侧链做“音乐让路”和“水下闷音”Snapshot 是 Mixer 里最实用的功能之一。它记录当前所有 Group 的音量、效果器参数切换时按设定的过渡时间插值过去。典型用法有三种过场压低音乐做一个Ducking快照把 Music 组压到 -12dBSFX 保持不变切换时间设 0.3 秒。播剧情对话时TransitionToSnapshots结束后切回Normal比手写插值协程稳得多。水下/洞穴闷音新建一个快照在 Master 或 SFX 组上挂一个 Lowpass 效果器Cutoff 拉到 800Hz 左右进入水下触发。同时可以给一个 Reverb 效果器加一点空间感。暂停菜单做一个Paused快照把 SFX 组压下去但保留 UI 音效避免暂停后按钮音效也被一起压掉——这种做法比用AudioListener.pause粗暴地全停要细致得多。Duck 侧链Sidechain是另一个高频需求。做法是在 Voice 组上加一个Send效果器目标选Ducking再给 Music 组加一个Receive效果器Receive 的 Duck Volume 设 -8dB、Threshold 设 -30dB 左右。效果是只要有语音或重要音效在播音乐自动往下让播完自动恢复。// 用代码短暂压低音乐适用于播报、Boss 出场等 public void DuckMusic(float duration 1.5f) { StartCoroutine(DuckRoutine(duration)); } private System.Collections.IEnumerator DuckRoutine(float duration) { mixer.SetFloat(MusicVolume, -12f); yield return new WaitForSecondsRealtime(duration); mixer.SetFloat(MusicVolume, Mathf.Log10(0.7f) * 20f); // 恢复到约 0.7 }注意这里用WaitForSecondsRealtime。如果游戏里Time.timeScale被改成 0比如暂停或子弹时间用普通的WaitForSeconds会让这段压低永远不恢复音乐一直是闷的。这个坑很典型而且很难在编辑器里复现因为通常是有时间缩放的场景才会暴露。4. 用对象池接管播放请求别让PlayOneShot满天飞先说结论正式项目里除了极少数确定只播一次的声音其余全部走统一的播放入口。原因不是“架构好看”而是三个具体的痛点无法统一限流、无法统一做随机化、无法在设置里关掉某一类音效。Unity 默认的Max Real Voices是 32在 Project Settings 的 Audio 里可调。这意味着同时超过 32 个声源时超出的会被“虚拟化”——听起来就是某些音效莫名其妙不响了。战斗密集时如果每个子弹、每次命中都各占一个AudioSource32 个额度会在半秒内被吃干净玩家的脚步声反而没了。这不是 bug是资源竞争。4.1 一个可用的轻量AudioManager骨架思路是预先创建固定数量的AudioSource池子每次播放请求从池里找一个空闲的找不到就丢弃或按优先级抢占最低优先级的那一个。丢弃比抢占更安全因为抢占会导致正在播的重要音效被切断听感上很突兀。using System.Collections.Generic; using UnityEngine; using UnityEngine.Audio; public class AudioManager : MonoBehaviour { public static AudioManager Instance { get; private set; } [System.Serializable] public class SfxDef { public string key; public AudioClip[] clips; // 同一语义的多个素材随机选 [Range(0f, 1f)] public float volume 1f; [Range(0f, 0.3f)] public float pitchJitter 0.06f; public float cooldown 0.05f; // 同一 key 的最小触发间隔 } [SerializeField] private SfxDef[] defs; [SerializeField] private AudioMixerGroup sfxGroup; [SerializeField] private int poolSize 24; private readonly Dictionarystring, SfxDef _map new Dictionarystring, SfxDef(); private readonly Dictionarystring, float _lastPlay new Dictionarystring, float(); private AudioSource[] _pool; private int _cursor; private void Awake() { if (Instance ! null Instance ! this) { Destroy(gameObject); return; } Instance this; DontDestroyOnLoad(gameObject); _pool new AudioSource[poolSize]; for (int i 0; i poolSize; i) { var go new GameObject($Sfx_{i}); go.transform.SetParent(transform); var src go.AddComponentAudioSource(); src.playOnAwake false; src.spatialBlend 1f; src.rolloffMode AudioRolloffMode.Logarithmic; src.minDistance 1.5f; src.maxDistance 25f; src.dopplerLevel 0f; src.outputAudioMixerGroup sfxGroup; _pool[i] src; } foreach (var d in defs) _map[d.key] d; } public void Play(string key, Vector3 worldPos, float volumeScale 1f) { if (!_map.TryGetValue(key, out var def) || def.clips null || def.clips.Length 0) return; float now Time.unscaledTime; if (_lastPlay.TryGetValue(key, out float last) now - last def.cooldown) return; _lastPlay[key] now; AudioSource src GetFreeSource(); if (src null) return; // 池满直接丢弃 src.transform.position worldPos; src.clip def.clips[Random.Range(0, def.clips.Length)]; src.volume def.volume * volumeScale; src.pitch 1f Random.Range(-def.pitchJitter, def.pitchJitter); src.Play(); } private AudioSource GetFreeSource() { for (int i 0; i _pool.Length; i) { int idx (_cursor i) % _pool.Length; if (!_pool[idx].isPlaying) { _cursor (idx 1) % _pool.Length; return _pool[idx]; } } return null; } }池子大小怎么定经验公式是常驻并发音效数量 × 1.5向上取整。动作类项目 16~24 个够用策略类、休闲类 8~12 个足够。设太大只是浪费内存每个空AudioSource本身的开销很小但被占用时音频线程的混合成本会上去。还有一个细节池子里的AudioSource挂在AudioManager下面而AudioManager是DontDestroyOnLoad的。这意味着切场景时这些声源不会跟着被销毁正在播的声音会继续播——这正好是我们要的效果切场景的余音不会突兀切断但也要求你在切场景时主动调用一次淡出第 5 节会讲。播放时的pitch随机化是我最推荐的一个小改动。同一个脚步 clip 连播三四次玩家会明显听出“机关枪感”加上 ±6% 的音调抖动后几乎听不出来是同一份素材。代价是pitch修改会影响播放速度所以对节奏类音效比如需要严格对齐节拍的声音不能抖。提示如果某个声音必须精确控制起始时间音乐、节拍音不要走池子用独立AudioSource配合PlayScheduled。4.2 随机音调、音效轮换与冷却消除“机关枪感”上节的代码里有三个抗重复的手段值得单独说一下为什么这么配。素材轮换是最有效的。同一语义准备 3~5 个变体脚步声准备 4 个、命中音准备 3 个随机挑选。这要求美术/音频侧在交付时就按组打包命名规范统一前缀比如sfx_footstep_01~04。这件事在素材交付阶段沟通一次后面全是收益。音调抖动的比例要克制。±5% 到 ±10% 比较自然超过 15% 就会变成“变声器”效果。注意随机范围要上下对称否则长期平均值会偏移听感上整体音调被拉高或压低。冷却时间解决的是“同一帧多个触发源”的问题。比如一次范围伤害命中 10 个敌人如果每个都播一次命中音声音会叠加成一片噪音音量还会因为相位叠加而突变。给这种 key 设 0.05~0.1 秒冷却实际听感反而更结实。反过来如果是需要体现“连击数”的音效冷却就不该用而应该按连击等级切换不同的 clip。表格对照一下这三者的配置建议音效类型变体数量音调抖动冷却时间脚步4~6±8%0.08s命中/打击3~4±10%0.03sUI 点击1~200.05s技能释放2~3±4%0.2s环境点缀5~8±6%2s 以上最后再补一个“优先级”的概念。上面代码里池子满了就丢弃但更好的做法是给每个 key 一个优先级池满时与正在播的最低优先级比较只有更高才抢占。不过说实话只要池子大小设得合理、冷却配得当抢占逻辑在中小项目里几乎不会触发反而增加了调试复杂度。我现在的做法是先用最简单的丢弃策略等真的观测到重要音效丢失再补优先级。5. 实测踩过的坑切场景爆音、移动端功耗与节奏对齐前面四节是“该怎么搭”这一节是“搭完之后会遇到什么”。这几个问题都有共同点在编辑器里跑得好好的打包到真机上或者在特定流程里才暴露排查起来很费时间。我把现象、原因和修法都列出来方便直接对照。5.1 切场景、暂停与销毁阶段的残留与爆音爆音pop的成因基本是波形被硬切。一段声音在振幅非零的位置被Stop()扬声器会收到一个突变听感上就是“啪”。解法是淡出在停止前用 0.05~0.1 秒把音量插值到 0再 Stop。AudioSource没有内置淡出得自己写协程或者用 Mixer 的 Snapshot 过渡更平滑推荐。private System.Collections.IEnumerator FadeOutAndStop(AudioSource src, float duration 0.08f) { float start src.volume; float t 0f; while (t duration) { t Time.unscaledDeltaTime; src.volume Mathf.Lerp(start, 0f, t / duration); yield return null; } src.Stop(); src.volume start; // 复原供池子复用 }切场景后声音残留如果AudioManager是常驻的切场景时正在播的环境声会一直播下去直到播完。表现是新场景里还听得到旧场景的风声。修法是在场景加载前主动调用一次“淡出所有非音乐声源”。暂停时声音没停AudioListener.pause true会暂停所有声音但它有两个副作用——一是它影响全局连 UI 音效也一起停了暂停菜单的按钮音效就没了二是恢复时要记得置回 false。更可控的做法是用 Mixer 快照把 SFX 和 Music 组压到 -80dBUI 组保持不变。销毁时爆音直接Destroy(gameObject)一个正在播放的声源会立即切断波形。场景切换、对象回收时很容易出现。稳妥的做法是给需要在销毁时淡出的对象挂一个小组件在OnDisable里先淡出再销毁或者干脆让音效由常驻的 AudioManager 播与对象生命周期解耦——这也是我一直推荐走统一入口的原因之一。5.2 移动端与后台切换的真实损耗移动端的音频问题集中在三点功耗、内存、后台行为。功耗这块最耗电的不是解码而是同时活跃的声源数量和每帧的音频线程混合。所以在移动端池子大小应该比 PC 小我把 24 降到 12 是常见配置同时用AudioSource.ignoreListenerPause这类细节控制好哪些声音在暂停时继续。内存上前面说的采样率下调、Force To Mono、短音效用 ADPCM这三招在移动端的收益非常明显。一个实测数据是50 个 1 秒的立体声 44.1kHz 音效全部改单声道 22050Hz ADPCM 后内存占用从约 8.8MB 降到约 0.6MB差距一个数量级。后台切换必须处理。切到后台时游戏通常会暂停但如果音频还在播用户会听到游戏声音从后台传出来体验很差。标准做法是private void OnApplicationFocus(bool hasFocus) { if (!Application.isMobilePlatform) return; AudioListener.pause !hasFocus; } private void OnApplicationPause(bool paused) { AudioListener.pause paused; }要注意的是AudioListener.pause只是暂停不释放音频资源恢复时如果发现某些短音效在暂停期间被触发过可能会出现“一恢复就同时放出一堆”的情况。解决办法是把播放入口在暂停期间直接返回在AudioManager.Play开头加一个if (IsPaused) return;从源头掐断。另外还有一个低端机上常见的现象首次播放某个音效有 100~300ms 的延迟。原因是 clip 采用的是 Compressed In Memory 且没勾 Preload第一次解码时要从内存读压缩数据。把高频音效设为 Decompress On Load Preload 就能解决代价是内存这个权衡自己按项目情况定。5.3 节奏类玩法必须用DSP时间而不是deltaTime如果项目里有任何需要“卡拍”的玩法——音乐节奏、QTE 跟着节拍、Boss 技能的节奏提示——那就不能用Time.deltaTime累加来算节拍。原因是主线程时间受帧率波动影响而音频是在独立的音频线程上按固定采样率走的两者不同步。帧率掉到 30 时按 deltaTime 算出来的“节拍点”会明显漂移玩家会觉得判定不准。正确的做法是用AudioSettings.dspTime它是音频系统的时钟单位是秒精度高且与音频线程同步using UnityEngine; public class BeatTimer : MonoBehaviour { private double _nextBeatDsp; private double _beatInterval; public void StartClock(float bpm) { _beatInterval 60.0 / bpm; _nextBeatDsp AudioSettings.dspTime 0.1; // 留一点预热时间 } private void Update() { if (AudioSettings.dspTime 0.05 _nextBeatDsp) { OnBeat(); _nextBeatDsp _beatInterval; } } private void OnBeat() { // 触发节拍逻辑判定、提示、特效 } }这里有个小技巧判断条件用dspTime 0.05 下一拍也就是提前 50ms 触发。因为音频输出本身有延迟下一段会讲提前一点触发视觉表现玩家看到和听到的才是一致的。延迟的根源在音频缓冲。Project Settings 的 Audio 里有个DSP Buffer Size四档对应不同的缓冲区大小采样率 44100 下的换算关系大致是设置缓冲区采样数单次延迟约说明Best Latency2565.8ms延迟最低CPU 压力最大低端机易爆音Good Latency51211.6ms大多数节奏类项目的选择Default102423.2ms默认值稳妥Best Performance204846.4ms省 CPU延迟明显实际输出延迟还要加上双缓冲和系统层的开销通常是最低延迟的两倍左右。所以追求精准判定的项目设成 Good Latency 之后还要在游戏内提供一个“延迟校准”让玩家自己调偏移量这几乎是所有节奏游戏的标配功能。如果要用音乐作为节拍基准那就更该用PlayScheduled把第一拍精确对齐到某个 dsp 时刻double startTime AudioSettings.dspTime 1.0; // 1 秒后开始 musicSource.PlayScheduled(startTime);用Play()直接起播的话实际发声时刻取决于调用时主线程的帧位置误差可能达到一帧16~33ms。而PlayScheduled是把时刻交给音频线程调度误差在一个采样级别。最后分享一个我在调音效时养成的习惯每加一个音效都先在真机上听一遍而不是只在编辑器里听。编辑器的音频路径和真机不一样采样率、缓冲、系统音效处理都可能有差异很多“编辑器里好好的、手机上很刺耳”的问题只有在真机上第一耳朵就能发现。项目周期里我会专门留出两个下午做这个动作——把所有音效按触发场景在真机上过一遍记下刺耳的、听不清的、重复感强的一次性调整参数。这比后期零散地改要高效得多而且能避免很多上线后才被玩家指出来的听感问题。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑