Phaser 3.60.0 音频系统深度解析:Sound Manager 新特性、自动恢复机制与兼容性修复
【免费下载链接】phaserPhaser is a fun, free and fast 2D game framework for making HTML5 games for desktop and mobile web browsers, supporting Canvas and WebGL rendering.项目地址: https://gitcode.com/gh_mirrors/ph/phaser
本文聚焦 Phaser 3.60.0 版本对 Sound Manager(声音管理器)的一次系统性升级,涵盖新增的gameLostFocus状态追踪、getAllPlaying批量查询方法、Web Audio 上下文自动恢复机制、Device.Audio能力检测模块重写,以及 NoAudio 回退管理器的完整补齐。读完本文,你将理解这些改动背后的实现原理、对应的源码位置,以及如何在新版本中正确使用这些 API 来构建跨设备、抗中断的健壮音频系统。
本文内容基于 changelog/v3/3.60/Sound.md 中的变更记录,并对照仓库源码逐一印证。
一、版本背景:一次以"稳定性"为核心的音频升级
Phaser 3.60.0 的 Sound Manager 改动没有引入颠覆性的新玩法,而是围绕三个核心目标展开:更可靠的失焦/恢复处理、更强的音频能力探测、更完整的 API 一致性。从变更条目来看,绝大多数改动都直接指向移动端 Safari、Android 13 等真实场景下的音频中断问题——这些正是 H5 游戏音频最棘手的痛点。
在深入新特性之前,先明确 Sound Manager 的三套实现(源码位于 src/sound/):
| 实现类 | 源码路径 | 适用场景 |
|---|---|---|
WebAudioSoundManager | src/sound/webaudio/WebAudioSoundManager.js | 设备支持 Web Audio API 时的默认实现 |
HTML5AudioSoundManager | src/sound/html5/HTML5AudioSoundManager.js | Web Audio 不可用时的降级实现 |
NoAudioSoundManager | src/sound/noaudio/NoAudioSoundManager.js | 设备完全不支持音频,或游戏配置中显式禁用音频时的空实现 |
三者共享 src/sound/BaseSoundManager.js 这个基类,本次 3.60.0 的大多数新增特性都落在基类上,从而让三套实现同步受益。
二、新特性:gameLostFocus与getAllPlaying
1.BaseSoundManager.gameLostFocus:可靠追踪游戏焦点
变更记录:BaseSoundManager.gameLostFocus是一个新的布尔属性,当游戏失去焦点时为true,重新获得焦点时为false。
该属性在基类构造函数中初始化(见 src/sound/BaseSoundManager.js):
/** * Flag used to track if the game has lost focus. * @name Phaser.Sound.BaseSoundManager#gameLostFocus * @type {boolean} * @default false * @since 3.60.0 */ this.gameLostFocus = false;关键在于它由基类统一挂接的两个内部事件处理器来维护(src/sound/BaseSoundManager.js):
onGameBlur: function () { this.gameLostFocus = true; if (this.pauseOnBlur) { this.onBlur(); } }, onGameFocus: function () { this.gameLostFocus = false; if (this.pauseOnBlur) { this.onFocus(); } }这两个处理器在构造函数中分别订阅了Phaser.Core.Events.BLUR与Phaser.Core.Events.FOCUS游戏级事件。从源码结构可以推断,gameLostFocus的引入价值在于:它把"游戏是否失焦"这一状态从"是否执行暂停动作"中解耦出来——即使pauseOnBlur为false(不主动暂停),你也依然可以通过this.sound.gameLostFocus感知到焦点状态,为自定义的降级策略(如降低音量、停止密集音效)提供依据。
2.BaseSoundManager.getAllPlaying:一键获取所有正在播放的声音
变更记录:BaseSoundManager.getAllPlaying是一个新方法,返回 Sound Manager 中所有当前正在播放的声音。
实现非常简洁(src/sound/BaseSoundManager.js),本质是对sounds数组按isPlaying === true过滤:
getAllPlaying: function () { return GetAll(this.sounds, 'isPlaying', true); }它复用了工具函数GetAll(来自 src/utils/array/GetAll.js)。典型使用场景:
// 在 Scene 中,停止所有正在播放的音效 const playing = this.sound.getAllPlaying(); playing.forEach(sound => sound.stop());配合原有的getAll(key),现在可以同时实现"按 key 查"与"按播放状态查"两种批量维度,在管理大量战斗音效、UI 音效时非常实用。
三、更新:从能力探测到自动恢复
1. 无匹配音频 URL 时输出控制台警告
变更记录:如果没有 Audio URL 能匹配当前设备,现在会在控制台显示一条警告(感谢 @samme)。
该逻辑位于音频加载器的工厂函数 src/loader/filetypes/AudioFile.js:
var urlConfig = AudioFile.getAudioURL(game, urls); if (!urlConfig) { console.warn('No audio URLs for "%s" can play on this device', key); return null; }底层探测函数getAudioURL会遍历你提供的 URL 列表,逐个用game.device.audio[audioType]检查当前设备是否支持对应格式(src/loader/filetypes/AudioFile.js),其中blob:与data:开头的 URI 会被无条件接受。当所有格式都不支持时,加载器不再"静默失败",而是直接提示你:该设备没有任何一个备选格式能播。这提醒开发者在配置音频加载时尽量提供多格式回退列表:
this.load.audio('title', [ 'music/Title.ogg', 'music/Title.mp3', 'music/Title.m4a' ]);2.getAll方法的key参数改为可选
变更记录:BaseSoundManager.getAll过去必须传入key参数才能返回匹配的声音;现在该参数可选,不传则返回全部 Sound 实例。
实现见 src/sound/BaseSoundManager.js:
getAll: function (key) { if (key) { return GetAll(this.sounds, 'key', key); } else { return GetAll(this.sounds); } }两种调用方式均合法:
const allSounds = this.sound.getAll(); // 全部声音 const bgmOnly = this.sound.getAll('background'); // 仅 key 为 'background' 的声音3. Web Audio 上下文进入suspended/interrupted时自动恢复(修复 #5353)
变更记录:WebAudioSoundManager现在会在其更新循环中检测 Audio Context 是否进入suspended或interrupted状态,如果是则尝试恢复。这常见于以下场景:更换/禁用音频设备(例如插入带音频驱动的耳机后又拔掉)、在 iOS 上切换标签页等。
这是本次更新中技术上最值得关注的一条。核心实现在 src/sound/webaudio/WebAudioSoundManager.js 的onFocus方法:
onFocus: function () { var context = this.context; if (context && !this.locked && (context.state === 'suspended' || context.state === 'interrupted')) { context.resume(); } }而onFocus的调用时机从基类的"仅在焦点事件触发时调用一次",升级为在update循环中每帧检测(src/sound/webaudio/WebAudioSoundManager.js):
BaseSoundManager.prototype.update.call(this, time, delta); // Resume interrupted audio on iOS only if the game has focus if (!this.gameLostFocus) { this.onFocus(); }这段代码与新增的gameLostFocus属性形成了漂亮的配合:每帧都会检查 AudioContext 的状态,只要游戏保持焦点、上下文处于suspended/interrupted,就尝试resume()。这修复了拔插音频设备、iOS 切换标签页后音频静默"卡死"的问题。对开发者而言,这意味着大多数音频中断场景现在可以无需任何业务代码即可自愈。
4.Device.Audio模块重写:CanPlay函数与 aac/flac 支持
变更记录:
Device.Audio模块被重写,使用新的内部CanPlay函数,大幅精简了代码量;Device.Audio.aac是新布尔属性,表示浏览器能否播放 aac 音频文件,使其可通过 Loader 加载(感谢 @Ariorh1337);Device.Audio.flac是新布尔属性,表示浏览器能否播放 flac 音频文件,使其可通过 Loader 加载(感谢 @Ariorh1337)。
重写后的 src/device/Audio.js 将此前针对每种格式的重复探测逻辑收敛为一个内部CanPlay辅助函数:
var CanPlay = function (type1, type2) { var canPlayType1 = audioElement.canPlayType('audio/' + type1).replace(/^no$/, ''); if (type2) { return Boolean(canPlayType1 || audioElement.canPlayType('audio/' + type2).replace(/^no$/, '')); } else { return Boolean(canPlayType1); } }; Audio.ogg = CanPlay('ogg; codecs="vorbis"'); Audio.opus = CanPlay('ogg; codecs="opus"', 'opus'); Audio.mp3 = CanPlay('mpeg'); Audio.wav = CanPlay('wav'); Audio.m4a = CanPlay('x-m4a'); Audio.aac = CanPlay('aac'); Audio.flac = CanPlay('flac', 'x-flac'); Audio.webm = CanPlay('webm; codecs="vorbis"');注意flac探测时同时尝试了audio/flac与audio/x-flac两种 MIME 类型,以覆盖不同浏览器的实现差异。完整的属性清单定义在 src/device/Audio.js,包括audioData、dolby、m4a、mp3、ogg、opus、wav、webAudio、webm以及新增的aac、flac。
这套能力探测结果在游戏启动阶段填充,你可以在任意 Scene 中通过this.sys.game.device.audio访问。新增的aac/flac属性直接打通了 Loader 对这两种格式的支持链路——getAudioURL会依据这些属性自动挑选可播放的 URL。这意味着你现在可以放心地在加载配置中把.aac、.flac文件加入回退列表:
this.load.audio('theme', [ 'theme.flac', 'theme.ogg', 'theme.mp3' ]);5.NoAudioSoundManager补齐全部缺失方法
变更记录:NoAudioSoundManager现在拥有了全部缺失的方法,例如removeAll和get,使其可以作为 HTML5 与 Web Audio Sound Manager 的直接替代品(感谢 @orjandh @samme)。
此前无音频模式下的管理器只保留了最基本的方法,任何在无音频设备上运行的代码只要调用了removeAll等方法就会直接报错。3.60.0 在 src/sound/noaudio/NoAudioSoundManager.js 中补齐了接口,其中相当一部分直接委托给基类实现,例如:
get: function (key) { return BaseSoundManager.prototype.get.call(this, key); }, removeAll: function () { return BaseSoundManager.prototype.removeAll.call(this); }, removeByKey: function (key) { return BaseSoundManager.prototype.removeByKey.call(this, key); }而play、playAudioSprite则保持"始终返回false"的语义,其余pauseAll、resumeAll、stopAll、update、unlock等方法统一使用NOOP(空操作)占位(见 src/sound/noaudio/NoAudioSoundManager.js),注释中明确说明这是"为了与其他 Sound Manager 保持兼容"。这种设计让业务代码在无音频设备上可以无差别运行——调用存在、返回安全值、绝不抛错,体现了 Phaser 在"优雅降级"上的工程态度。
四、Bug 修复:三个真实场景的根因剖析
1.pauseOnBlur在部分浏览器上失效(修复 #6354)
变更记录:在部分浏览器(如 Android 13 上的 Firefox 与 Chrome)中,游戏失焦时设置SoundManager.pauseOnBlur = true无法停止音频。现在通过新的gameLostFocus标志强制执行。感谢 @klaritan 与 @michalfialadev。
该问题的根因在于:某些浏览器在标签页后台化后,blur事件可能不被可靠派发,或派发时机过晚。3.60.0 中gameLostFocus作为独立的状态标志被维护,并配合WebAudioSoundManager.update中"每帧仅当!this.gameLostFocus时才执行onFocus()"的守卫(src/sound/webaudio/WebAudioSoundManager.js),从"事件驱动"转向"事件 + 状态驱动"双重保障。
2. WebAudioSound 销毁时的节点重复断开错误
变更记录:在同一个游戏步内同时销毁WebAudioSound与 Game 本身,会在尝试断开已断开的 Web Audio 节点时报错。WebAudioSound现在会在销毁序列开始前检查是否已处于"待移除"状态。
对应的守卫逻辑在 src/sound/webaudio/WebAudioSound.js:
destroy: function () { if (this.pendingRemove) { return; } BaseSound.prototype.destroy.call(this); // ... 随后断开 muteNode / volumeNode / pannerNode / spatialNode }pendingRemove标志在基类 src/sound/BaseSound.js 的destroy中被置为true。同时,基类的update循环会每帧清理带有pendingRemove标记的声音(src/sound/BaseSoundManager.js),形成"标记 → 清理"的安全生命周期。这一修复对频繁切换场景、动态销毁大量音频对象的游戏尤为重要。
3. iOS 14+ Safari 音频解锁回归(修复 #5696)
变更记录:iOS 14 及以上版本的 Safari 中,音频现在可以正确解锁了。感谢 @laineus。
iOS 的 Web Audio 自动播放策略一直是最复杂的兼容性问题之一。结合上文提到的interrupted状态检测(该状态正是 iOS 上音频被系统打断时的典型表现),可以推断此次修复的核心思路是:在update循环中持续检测context.state,一旦发现interrupted/suspended且游戏持有焦点,便主动调用context.resume(),从而绕开此前"必须在用户手势中解锁"的单点失效问题。配合gameLostFocus标志,即使页面在 Safari 中被切到后台再切回,也能在聚焦后第一时间恢复音频。
五、总结:如何把这批改动用起来
将本次 3.60.0 的改动落地的推荐做法:
- 升级后无需迁移代码——
getAll、getAllPlaying、gameLostFocus均为纯增量 API,旧代码完全兼容; - 利用
gameLostFocus实现自定义降级,例如失焦时降低音量而非彻底静音:this.sound.on('gameblur', () => { /* 自定义处理 */ }); // 或直接读取 this.sound.gameLostFocus - 在音频资源列表中混入 aac/flac 格式,让 Loader 自动为支持的设备挑选更优格式;
- 信任自动恢复机制,
suspended/interrupted状态的检测与恢复已内置,无需再手写context.resume()的轮询逻辑。
本次变更的完整对照源码均可追溯:基类行为见 src/sound/BaseSoundManager.js,Web Audio 实现见 src/sound/webaudio/WebAudioSoundManager.js 与 src/sound/webaudio/WebAudioSound.js,能力探测见 src/device/Audio.js,加载警告见 src/loader/filetypes/AudioFile.js。相关单元测试覆盖了基类行为(tests/sound/BaseSoundManager.test.js)与无音频模式(tests/sound/noaudio/NoAudioSoundManager.test.js),可作为进一步研读的入口。回到 变更日志索引 可查看同一版本其他模块的更新。
【免费下载链接】phaserPhaser is a fun, free and fast 2D game framework for making HTML5 games for desktop and mobile web browsers, supporting Canvas and WebGL rendering.项目地址: https://gitcode.com/gh_mirrors/ph/phaser
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考